From 0c18e83e8dc31aeced073ba3382d0f4d7b953010 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 16 Aug 2021 07:41:09 +0100 Subject: [PATCH] Import akonadi_21.08.0.orig.tar.xz [dgit import orig akonadi_21.08.0.orig.tar.xz] --- .clang-tidy | 61 + .clang-tidy-ignore | 13 + .git-blame-ignore-revs | 2 + .gitignore | 26 + .gitlab-ci.yml | 35 + .kateconfig | 1 + .krazy | 3 + AUTHORS | 88 + CMakeLists.txt | 395 +++ CMakePresets.json | 128 + CMakePresets.json.license | 2 + CTestConfig.cmake | 13 + CTestCustom.cmake | 22 + ExtraDesktop.sh | 4 + INSTALL | 60 + Info.plist.template | 36 + KF5AkonadiConfig.cmake.in | 41 + KF5AkonadiMacros.cmake | 142 + LICENSES/BSD-3-Clause.txt | 26 + LICENSES/CC0-1.0.txt | 119 + LICENSES/GPL-2.0-only.txt | 319 ++ LICENSES/GPL-2.0-or-later.txt | 319 ++ LICENSES/GPL-3.0-only.txt | 625 ++++ LICENSES/LGPL-2.0-only.txt | 446 +++ LICENSES/LGPL-2.0-or-later.txt | 446 +++ LICENSES/LGPL-2.1-only.txt | 467 +++ LICENSES/LGPL-2.1-or-later.txt | 468 +++ LICENSES/LicenseRef-KDE-Accepted-GPL.txt | 12 + LICENSES/MIT.txt | 19 + LICENSES/Qt-LGPL-exception-1.1.txt | 21 + NEWS | 529 +++ README.md | 27 + README.sqlite | 54 + akonadi-mime.xml | 32 + akonadifull-version.h.cmake | 7 + apparmor/CMakeLists.txt | 2 + apparmor/mariadbd_akonadi | 39 + apparmor/mysqld_akonadi | 39 + apparmor/postgresql_akonadi | 41 + apparmor/usr.bin.akonadiserver | 80 + autotests/CMakeLists.txt | 48 + autotests/akonadicontrol/CMakeLists.txt | 27 + autotests/akonadicontrol/agenttypetest.cpp | 72 + .../data/akonaditestresource.desktop | 95 + autotests/libs/CMakeLists.txt | 122 + autotests/libs/actionstatemanagertest.cpp | 626 ++++ autotests/libs/attributefactorytest.cpp | 91 + autotests/libs/attributefactorytest.h | 19 + autotests/libs/autoincrementtest.cpp | 114 + autotests/libs/autoincrementtest.h | 32 + autotests/libs/cachepolicytest.cpp | 30 + autotests/libs/cachepolicytest.h | 20 + autotests/libs/cachetest.cpp | 142 + autotests/libs/changerecordertest.cpp | 184 + autotests/libs/collectionattributetest.cpp | 251 ++ autotests/libs/collectionattributetest.h | 23 + .../libs/collectioncolorattributetest.cpp | 56 + autotests/libs/collectioncopytest.cpp | 119 + autotests/libs/collectioncreatetest.cpp | 53 + autotests/libs/collectioncreator.cpp | 73 + autotests/libs/collectionjobtest.cpp | 895 +++++ autotests/libs/collectionjobtest.h | 44 + autotests/libs/collectionmodifytest.cpp | 66 + autotests/libs/collectionmovetest.cpp | 135 + autotests/libs/collectionpathresolvertest.cpp | 53 + autotests/libs/collectionpathresolvertest.h | 20 + autotests/libs/collectionsynctest.cpp | 446 +++ autotests/libs/collectionutilstest.cpp | 66 + autotests/libs/conflictresolvedialogtest.cpp | 50 + autotests/libs/conflictresolvedialogtest.h | 21 + autotests/libs/entitycachetest.cpp | 157 + autotests/libs/entitydisplayattributetest.cpp | 60 + autotests/libs/entitytreemodeltest.cpp | 662 ++++ autotests/libs/etmpopulationtest.cpp | 337 ++ autotests/libs/fakeakonadiservercommand.cpp | 492 +++ autotests/libs/fakeakonadiservercommand.h | 389 +++ autotests/libs/fakeentitycache.cpp | 7 + autotests/libs/fakeentitycache.h | 174 + autotests/libs/fakemonitor.cpp | 35 + autotests/libs/fakemonitor.h | 25 + autotests/libs/fakeserverdata.cpp | 140 + autotests/libs/fakeserverdata.h | 73 + autotests/libs/fakesession.cpp | 84 + autotests/libs/fakesession.h | 36 + autotests/libs/favoriteproxytest.cpp | 236 ++ autotests/libs/firstrunner.cpp | 28 + autotests/libs/gidtest.cpp | 185 + autotests/libs/gidtest.h | 38 + autotests/libs/inspectablechangerecorder.cpp | 23 + autotests/libs/inspectablechangerecorder.h | 77 + autotests/libs/inspectablemonitor.cpp | 42 + autotests/libs/inspectablemonitor.h | 77 + autotests/libs/invalidatecachejobtest.cpp | 75 + autotests/libs/itemappendtest.cpp | 389 +++ autotests/libs/itemappendtest.h | 30 + autotests/libs/itembenchmark.cpp | 179 + autotests/libs/itemcopytest.cpp | 121 + autotests/libs/itemdeletetest.cpp | 220 ++ autotests/libs/itemfetchtest.cpp | 264 ++ autotests/libs/itemfetchtest.h | 24 + autotests/libs/itemhydratest.cpp | 389 +++ autotests/libs/itemhydratest.h | 34 + autotests/libs/itemmovetest.cpp | 174 + autotests/libs/itemsearchjobtest.cpp | 89 + autotests/libs/itemserializertest.cpp | 54 + autotests/libs/itemserializertest.h | 19 + autotests/libs/itemstoretest.cpp | 500 +++ autotests/libs/itemstoretest.h | 30 + autotests/libs/itemsynctest.cpp | 696 ++++ autotests/libs/itemtest.cpp | 111 + autotests/libs/itemtest.h | 22 + autotests/libs/jobtest.cpp | 236 ++ autotests/libs/lazypopulationtest.cpp | 358 ++ autotests/libs/linktest.cpp | 108 + autotests/libs/mimetypecheckertest.cpp | 291 ++ autotests/libs/mimetypecheckertest.h | 34 + autotests/libs/modelspy.cpp | 189 ++ autotests/libs/modelspy.h | 123 + autotests/libs/monitorfiltertest.cpp | 369 ++ autotests/libs/monitornotificationtest.cpp | 303 ++ autotests/libs/monitortest.cpp | 399 +++ autotests/libs/monitortest.h | 20 + autotests/libs/protocolhelpertest.cpp | 324 ++ autotests/libs/proxymodelstest.cpp | 114 + autotests/libs/relationtest.cpp | 164 + autotests/libs/resourceschedulertest.cpp | 359 ++ autotests/libs/resourceschedulertest.h | 34 + autotests/libs/resourcetest.cpp | 84 + autotests/libs/searchjobtest.cpp | 116 + autotests/libs/searchjobtest.h | 19 + autotests/libs/searchquerytest.cpp | 148 + autotests/libs/servermanagertest.cpp | 79 + autotests/libs/sharedvaluepooltest.cpp | 77 + autotests/libs/statisticsproxymodeltest.cpp | 237 ++ autotests/libs/subscriptiontest.cpp | 78 + autotests/libs/tagmodeltest.cpp | 325 ++ autotests/libs/tagsynctest.cpp | 251 ++ autotests/libs/tagtest.cpp | 968 ++++++ autotests/libs/tagtest_simple.cpp | 61 + autotests/libs/test_model_helpers.h | 65 + autotests/libs/testattribute.h | 40 + autotests/libs/testenvironmenttest.cpp | 52 + autotests/libs/testresource/CMakeLists.txt | 38 + .../libs/testresource/Info.plist.template | 36 + autotests/libs/testresource/Messages.sh | 3 + autotests/libs/testresource/knut-template.xml | 3 + autotests/libs/testresource/knutresource.cpp | 370 ++ .../libs/testresource/knutresource.desktop | 56 + autotests/libs/testresource/knutresource.h | 67 + autotests/libs/testresource/knutresource.kcfg | 21 + autotests/libs/testresource/settings.kcfgc | 8 + .../libs/testresource/tests/CMakeLists.txt | 6 + .../libs/testresource/tests/knut-empty.xml | 3 + .../libs/testresource/tests/knut-step1.xml | 28 + .../libs/testresource/tests/knut-step2.xml | 28 + autotests/libs/testresource/tests/knutdemo.js | 65 + .../libs/testresource/tests/knutdemo.xml | 72 + .../libs/testresource/tests/testmail.mbox | 19 + autotests/libs/testrunner/CMakeLists.txt | 28 + autotests/libs/testrunner/config.cpp | 149 + autotests/libs/testrunner/config.h | 47 + autotests/libs/testrunner/config.xml | 7 + autotests/libs/testrunner/main.cpp | 139 + autotests/libs/testrunner/setup.cpp | 442 +++ autotests/libs/testrunner/setup.h | 84 + autotests/libs/testrunner/shellscript.cpp | 93 + autotests/libs/testrunner/shellscript.h | 28 + .../libs/testrunner/testrunner-config.xsd | 35 + autotests/libs/testrunner/testrunner.cpp | 70 + autotests/libs/testrunner/testrunner.h | 40 + .../libs/testsearchplugin/CMakeLists.txt | 7 + .../akonadi_test_searchplugin.json | 7 + .../testsearchplugin/testsearchplugin.cpp | 32 + .../libs/testsearchplugin/testsearchplugin.h | 23 + autotests/libs/transactiontest.cpp | 86 + autotests/libs/transactiontest.h | 18 + autotests/libs/unittestenv/config.xml | 7 + .../unittestenv/xdgconfig/akonadi-firstrunrc | 4 + .../xdgconfig/akonadi_knut_resource_0rc | 4 + .../xdgconfig/akonadi_knut_resource_1rc | 3 + .../xdgconfig/akonadi_knut_resource_2rc | 3 + .../unittestenv/xdglocal/testdata-res1.xml | 78 + .../unittestenv/xdglocal/testdata-res2.xml | 6 + .../unittestenv/xdglocal/testdata-res3.xml | 4 + autotests/private/CMakeLists.txt | 33 + autotests/private/akdbustest.cpp | 138 + autotests/private/akstandarddirstest.cpp | 46 + autotests/private/compressionstreamtest.cpp | 152 + autotests/private/externalpartstoragetest.cpp | 304 ++ autotests/private/imapparsertest.cpp | 591 ++++ autotests/private/imapparsertest.h | 37 + autotests/private/imapsettest.cpp | 63 + autotests/private/imapsettest.h | 18 + autotests/private/notificationmessagetest.cpp | 90 + autotests/private/notificationmessagetest.h | 20 + autotests/private/protocoltest.cpp | 656 ++++ autotests/private/protocoltest.h | 79 + autotests/server/CMakeLists.txt | 103 + autotests/server/aggregatedfetchscopetest.cpp | 158 + .../server/collectioncreatehandlertest.cpp | 161 + .../server/collectionfetchhandlertest.cpp | 577 ++++ .../server/collectionmodifyhandlertest.cpp | 186 ++ autotests/server/collectionschedulertest.cpp | 123 + autotests/server/collectionstatisticstest.cpp | 184 + autotests/server/collectiontreecachetest.cpp | 134 + autotests/server/dbconfigtest.cpp | 98 + autotests/server/dbdeadlockcatchertest.cpp | 63 + autotests/server/dbinitializer.cpp | 221 ++ autotests/server/dbinitializer.h | 31 + autotests/server/dbinitializertest.cpp | 177 + autotests/server/dbinitializertest.h | 24 + autotests/server/dbintrospectortest.cpp | 84 + autotests/server/dbpopulator.xsl | 417 +++ autotests/server/dbtest_data/dbdata.xml | 70 + autotests/server/dbtest_data/dbinit_mysql | 271 ++ .../dbtest_data/dbinit_mysql_incremental | 209 ++ autotests/server/dbtest_data/dbinit_psql | 232 ++ .../dbtest_data/dbinit_psql_incremental | 210 ++ autotests/server/dbtest_data/dbinit_sqlite | 748 +++++ .../dbtest_data/dbinit_sqlite_incremental | 745 +++++ autotests/server/dbtest_data/dbtest_data.qrc | 14 + .../server/dbtest_data/unittest_dbupdate.xml | 318 ++ .../server/dbtest_data/unittest_schema.xml | 246 ++ autotests/server/dbtypetest.cpp | 50 + autotests/server/dbupdatertest.cpp | 91 + autotests/server/dbupdatertest.h | 21 + autotests/server/fakeakonadiserver.cpp | 324 ++ autotests/server/fakeakonadiserver.h | 119 + autotests/server/fakeclient.cpp | 219 ++ autotests/server/fakeclient.h | 52 + autotests/server/fakeconnection.cpp | 29 + autotests/server/fakeconnection.h | 33 + autotests/server/fakedatastore.cpp | 234 ++ autotests/server/fakedatastore.h | 119 + autotests/server/fakeentities.h | 74 + autotests/server/fakeintervalcheck.cpp | 47 + autotests/server/fakeintervalcheck.h | 40 + autotests/server/fakeitemretrievalmanager.cpp | 27 + autotests/server/fakeitemretrievalmanager.h | 25 + autotests/server/fakesearchmanager.cpp | 50 + autotests/server/fakesearchmanager.h | 40 + autotests/server/fetchhandlertest.cpp | 259 ++ autotests/server/handlertest.cpp | 191 ++ .../inspectablenotificationcollector.cpp | 22 + .../server/inspectablenotificationcollector.h | 34 + autotests/server/itemcreatehandlertest.cpp | 887 +++++ autotests/server/itemlinkhandlertest.cpp | 277 ++ autotests/server/itemmovehandlertest.cpp | 126 + autotests/server/itemretrievertest.cpp | 348 ++ autotests/server/mockobjects.h | 46 + autotests/server/notificationmanagertest.cpp | 152 + .../server/notificationsubscribertest.cpp | 293 ++ autotests/server/parthelpertest.cpp | 114 + autotests/server/partstreamertest.cpp | 308 ++ autotests/server/parttypehelpertest.cpp | 78 + autotests/server/querybuildertest.cpp | 358 ++ autotests/server/querybuildertest.h | 34 + autotests/server/relationhandlertest.cpp | 495 +++ autotests/server/searchtest.cpp | 125 + autotests/server/taghandlertest.cpp | 446 +++ autotests/shared/CMakeLists.txt | 20 + autotests/shared/akrangestest.cpp | 455 +++ autotests/shared/akscopeguardtest.cpp | 84 + autotests/widgets/CMakeLists.txt | 8 + autotests/widgets/subscriptiondialogtest.cpp | 253 ++ autotests/widgets/tageditwidgettest.cpp | 356 ++ .../widgets/tagselectioncomboboxtest.cpp | 225 ++ autotests/widgets/tagwidgettest.cpp | 195 ++ autotests/widgets/unittestenv/config.xml | 7 + .../unittestenv/xdgconfig/akonadi-firstrunrc | 4 + .../xdgconfig/akonadi_knut_resource_0rc | 4 + .../xdgconfig/akonadi_knut_resource_1rc | 3 + .../xdgconfig/akonadi_knut_resource_2rc | 3 + .../unittestenv/xdglocal/testdata-res1.xml | 78 + .../unittestenv/xdglocal/testdata-res2.xml | 6 + .../unittestenv/xdglocal/testdata-res3.xml | 4 + cmake/modules/AkonadiMacros.cmake | 87 + cmake/modules/FindSqlite.cmake | 118 + config-akonadi.h.cmake | 9 + docs/client_libraries.md | 684 ++++ docs/history.md | 46 + docs/images/bufferedcaching1.png | Bin 0 -> 37509 bytes docs/images/bufferedcaching2.png | Bin 0 -> 41430 bytes docs/images/bufferedcaching3.png | Bin 0 -> 47782 bytes docs/images/bufferedcaching4.png | Bin 0 -> 50736 bytes docs/images/bufferedcaching6.png | Bin 0 -> 49369 bytes ...descendantentitiesproxymodel-colfilter.png | Bin 0 -> 6144 bytes ...ndantentitiesproxymodel-withansecnames.png | Bin 0 -> 10346 bytes docs/images/descendantentitiesproxymodel.png | Bin 0 -> 8783 bytes docs/images/entitytreemodel-collections.png | Bin 0 -> 4292 bytes docs/images/entitytreemodel-showroot.png | Bin 0 -> 9304 bytes .../entitytreemodel-showrootwithname.png | Bin 0 -> 9663 bytes docs/images/entitytreemodel.png | Bin 0 -> 9035 bytes docs/images/mailmodelapp.png | Bin 0 -> 13528 bytes docs/images/selectionproxymodel-ordered.png | Bin 0 -> 16296 bytes ...ymodelmultipleselection-withdescendant.png | Bin 0 -> 10232 bytes .../selectionproxymodelmultipleselection.png | Bin 0 -> 8826 bytes .../selectionproxymodelsimpleselection.png | Bin 0 -> 7434 bytes docs/images/treeandlistapp.png | Bin 0 -> 19079 bytes docs/images/treeandlistappwithdesclist.png | Bin 0 -> 18041 bytes docs/internals.md | 33 + docs/kontact.svg | 88 + docs/server.md | 244 ++ docs/tags.md | 163 + icons/128-apps-akonadi.png | Bin 0 -> 22934 bytes icons/16-apps-akonadi.png | Bin 0 -> 939 bytes icons/22-apps-akonadi.png | Bin 0 -> 1487 bytes icons/256-apps-akonadi.png | Bin 0 -> 67747 bytes icons/32-apps-akonadi.png | Bin 0 -> 2530 bytes icons/48-apps-akonadi.png | Bin 0 -> 4730 bytes icons/64-apps-akonadi.png | Bin 0 -> 7355 bytes icons/CMakeLists.txt | 14 + icons/sc-apps-akonadi.svgz | Bin 0 -> 10294 bytes logo.png | Bin 0 -> 22934 bytes metainfo.yaml | 45 + metainfo.yaml.license | 2 + po/ar/akonadi_knut_resource.po | 93 + po/ar/libakonadi5.po | 2798 ++++++++++++++++ po/az/akonadi_knut_resource.po | 84 + po/az/libakonadi5.po | 2580 ++++++++++++++ po/be/libakonadi5.po | 2617 +++++++++++++++ po/bs/akonadi_knut_resource.po | 87 + po/bs/libakonadi5.po | 2853 ++++++++++++++++ po/ca/akonadi_knut_resource.po | 90 + po/ca/libakonadi5.po | 2622 +++++++++++++++ po/ca@valencia/akonadi_knut_resource.po | 90 + po/ca@valencia/libakonadi5.po | 2622 +++++++++++++++ po/cs/akonadi_knut_resource.po | 84 + po/cs/libakonadi5.po | 2517 ++++++++++++++ po/da/akonadi_knut_resource.po | 90 + po/da/libakonadi5.po | 2818 ++++++++++++++++ po/de/akonadi_knut_resource.po | 88 + po/de/libakonadi5.po | 2838 ++++++++++++++++ po/el/akonadi_knut_resource.po | 91 + po/el/libakonadi5.po | 2813 ++++++++++++++++ po/en_GB/akonadi_knut_resource.po | 89 + po/en_GB/libakonadi5.po | 2660 +++++++++++++++ po/eo/akonadi_knut_resource.po | 83 + po/eo/libakonadi5.po | 2557 ++++++++++++++ po/es/akonadi_knut_resource.po | 87 + po/es/libakonadi5.po | 2632 +++++++++++++++ po/et/akonadi_knut_resource.po | 91 + po/et/libakonadi5.po | 2805 ++++++++++++++++ po/eu/akonadi_knut_resource.po | 87 + po/eu/libakonadi5.po | 2601 +++++++++++++++ po/fi/akonadi_knut_resource.po | 93 + po/fi/libakonadi5.po | 2662 +++++++++++++++ po/fr/akonadi_knut_resource.po | 90 + po/fr/libakonadi5.po | 2855 ++++++++++++++++ po/ga/akonadi_knut_resource.po | 90 + po/ga/libakonadi5.po | 2785 ++++++++++++++++ po/gl/akonadi_knut_resource.po | 84 + po/gl/libakonadi5.po | 2790 ++++++++++++++++ po/hu/akonadi_knut_resource.po | 84 + po/hu/libakonadi5.po | 2778 +++++++++++++++ po/ia/akonadi_knut_resource.po | 84 + po/ia/libakonadi5.po | 2739 +++++++++++++++ po/it/akonadi_knut_resource.po | 90 + po/it/libakonadi5.po | 2823 ++++++++++++++++ po/ja/akonadi_knut_resource.po | 82 + po/ja/libakonadi5.po | 2588 ++++++++++++++ po/kk/akonadi_knut_resource.po | 90 + po/kk/libakonadi5.po | 2781 +++++++++++++++ po/km/akonadi_knut_resource.po | 82 + po/km/libakonadi5.po | 2765 +++++++++++++++ po/ko/akonadi_knut_resource.po | 83 + po/ko/libakonadi5.po | 2600 +++++++++++++++ po/lt/akonadi_knut_resource.po | 85 + po/lt/libakonadi5.po | 2631 +++++++++++++++ po/lv/akonadi_knut_resource.po | 95 + po/lv/libakonadi5.po | 2969 +++++++++++++++++ po/mr/akonadi_knut_resource.po | 84 + po/mr/libakonadi5.po | 2531 ++++++++++++++ po/nb/akonadi_knut_resource.po | 86 + po/nb/libakonadi5.po | 2554 ++++++++++++++ po/nds/akonadi_knut_resource.po | 92 + po/nds/libakonadi5.po | 2739 +++++++++++++++ po/nl/akonadi_knut_resource.po | 84 + po/nl/libakonadi5.po | 2819 ++++++++++++++++ po/nn/akonadi_knut_resource.po | 87 + po/nn/libakonadi5.po | 2516 ++++++++++++++ po/pa/akonadi_knut_resource.po | 84 + po/pa/libakonadi5.po | 2744 +++++++++++++++ po/pl/akonadi_knut_resource.po | 92 + po/pl/libakonadi5.po | 2689 +++++++++++++++ po/pt/akonadi_knut_resource.po | 85 + po/pt/libakonadi5.po | 2613 +++++++++++++++ po/pt_BR/akonadi_knut_resource.po | 85 + po/pt_BR/libakonadi5.po | 2623 +++++++++++++++ po/ro/akonadi_knut_resource.po | 90 + po/ro/libakonadi5.po | 2867 ++++++++++++++++ po/ru/akonadi_knut_resource.po | 96 + po/ru/libakonadi5.po | 2888 ++++++++++++++++ po/se/libakonadi5.po | 2466 ++++++++++++++ po/sk/akonadi_knut_resource.po | 82 + po/sk/libakonadi5.po | 2701 +++++++++++++++ po/sl/akonadi_knut_resource.po | 86 + po/sl/libakonadi5.po | 2647 +++++++++++++++ po/sq/akonadi_knut_resource.po | 92 + po/sr/akonadi_knut_resource.po | 85 + po/sr/libakonadi5.po | 2645 +++++++++++++++ po/sv/akonadi_knut_resource.po | 91 + po/sv/libakonadi5.po | 2800 ++++++++++++++++ po/tr/akonadi_knut_resource.po | 87 + po/tr/libakonadi5.po | 2839 ++++++++++++++++ po/ug/akonadi_knut_resource.po | 84 + po/ug/libakonadi5.po | 2531 ++++++++++++++ po/uk/akonadi_knut_resource.po | 87 + po/uk/libakonadi5.po | 2660 +++++++++++++++ po/zh_CN/akonadi_knut_resource.po | 89 + po/zh_CN/libakonadi5.po | 2500 ++++++++++++++ po/zh_TW/akonadi_knut_resource.po | 85 + po/zh_TW/libakonadi5.po | 2578 ++++++++++++++ sanitizers.supp | 16 + src/.krazy | 1 + src/CMakeLists.txt | 19 + src/Messages.sh | 3 + src/agentbase/CMakeLists.txt | 119 + src/agentbase/accountsintegration.cpp | 106 + src/agentbase/accountsintegration.h | 53 + src/agentbase/agentbase.cpp | 1280 +++++++ src/agentbase/agentbase.h | 798 +++++ src/agentbase/agentbase_p.h | 144 + src/agentbase/agentfactory.cpp | 59 + src/agentbase/agentfactory.h | 90 + src/agentbase/agentsearchinterface.cpp | 136 + src/agentbase/agentsearchinterface.h | 82 + src/agentbase/agentsearchinterface_p.h | 40 + src/agentbase/preprocessorbase.cpp | 50 + src/agentbase/preprocessorbase.h | 173 + src/agentbase/preprocessorbase_p.cpp | 84 + src/agentbase/preprocessorbase_p.h | 44 + src/agentbase/recursivemover.cpp | 214 ++ src/agentbase/recursivemover_p.h | 72 + src/agentbase/resourcebase.cpp | 1600 +++++++++ src/agentbase/resourcebase.h | 880 +++++ src/agentbase/resourcebase.kcfg | 13 + src/agentbase/resourcebasesettings.kcfgc | 9 + src/agentbase/resourcescheduler.cpp | 698 ++++ src/agentbase/resourcescheduler_p.h | 297 ++ src/agentbase/resourcesettings.cpp | 29 + src/agentbase/resourcesettings.h | 27 + src/agentbase/transportresourcebase.cpp | 65 + src/agentbase/transportresourcebase.h | 88 + src/agentbase/transportresourcebase_p.h | 52 + src/agentserver/CMakeLists.txt | 56 + src/agentserver/TODO | 3 + src/agentserver/agentlauncher.cpp | 48 + src/agentserver/agentpluginloader.cpp | 38 + src/agentserver/agentpluginloader.h | 34 + src/agentserver/agentserver.cpp | 114 + src/agentserver/agentserver.h | 49 + src/agentserver/agentthread.cpp | 40 + src/agentserver/agentthread.h | 47 + src/agentserver/main.cpp | 35 + src/akonadicontrol/CMakeLists.txt | 76 + src/akonadicontrol/accountsintegration.cpp | 159 + src/akonadicontrol/accountsintegration.h | 46 + src/akonadicontrol/agentbrokeninstance.cpp | 42 + src/akonadicontrol/agentbrokeninstance.h | 29 + src/akonadicontrol/agentinstance.cpp | 191 ++ src/akonadicontrol/agentinstance.h | 176 + src/akonadicontrol/agentmanager.cpp | 908 +++++ src/akonadicontrol/agentmanager.h | 375 +++ src/akonadicontrol/agentprocessinstance.cpp | 88 + src/akonadicontrol/agentprocessinstance.h | 38 + src/akonadicontrol/agentthreadinstance.cpp | 77 + src/akonadicontrol/agentthreadinstance.h | 36 + src/akonadicontrol/agenttype.cpp | 102 + src/akonadicontrol/agenttype.h | 48 + src/akonadicontrol/controlmanager.cpp | 28 + src/akonadicontrol/controlmanager.h | 36 + src/akonadicontrol/main.cpp | 84 + ....freedesktop.Akonadi.Control.service.cmake | 3 + src/akonadicontrol/processcontrol.cpp | 261 ++ src/akonadicontrol/processcontrol.h | 127 + src/akonadictl/CMakeLists.txt | 38 + src/akonadictl/akonadistarter.cpp | 63 + src/akonadictl/akonadistarter.h | 23 + src/akonadictl/main.cpp | 278 ++ src/asapcat/CMakeLists.txt | 17 + src/asapcat/main.cpp | 36 + src/asapcat/session.cpp | 132 + src/asapcat/session.h | 47 + src/core/CMakeLists.txt | 365 ++ src/core/abstractdifferencesreporter.h | 132 + src/core/agentconfigurationbase.cpp | 99 + src/core/agentconfigurationbase.h | 156 + src/core/agentconfigurationfactorybase.cpp | 14 + src/core/agentconfigurationfactorybase.h | 44 + src/core/agentconfigurationmanager.cpp | 103 + src/core/agentconfigurationmanager_p.h | 39 + src/core/agentinstance.cpp | 174 + src/core/agentinstance.h | 203 ++ src/core/agentinstance_p.h | 51 + src/core/agentmanager.cpp | 407 +++ src/core/agentmanager.h | 196 ++ src/core/agentmanager_p.h | 96 + src/core/agenttype.cpp | 85 + src/core/agenttype.h | 138 + src/core/agenttype_p.h | 47 + src/core/akonaditests_export.h.in | 2 + src/core/asyncselectionhandler.cpp | 82 + src/core/asyncselectionhandler_p.h | 58 + src/core/attributefactory.cpp | 145 + src/core/attributefactory.h | 77 + src/core/attributes/attribute.cpp | 11 + src/core/attributes/attribute.h | 167 + .../attributes/collectioncolorattribute.cpp | 47 + .../attributes/collectioncolorattribute.h | 47 + .../collectionidentificationattribute.cpp | 130 + .../collectionidentificationattribute.h | 71 + .../attributes/collectionquotaattribute.cpp | 87 + .../attributes/collectionquotaattribute.h | 97 + .../attributes/collectionrightsattribute.cpp | 138 + .../attributes/collectionrightsattribute_p.h | 71 + .../attributes/entityannotationsattribute.cpp | 96 + .../attributes/entityannotationsattribute.h | 53 + .../attributes/entitydeletedattribute.cpp | 102 + src/core/attributes/entitydeletedattribute.h | 88 + .../attributes/entitydisplayattribute.cpp | 142 + src/core/attributes/entitydisplayattribute.h | 112 + src/core/attributes/entityhiddenattribute.cpp | 42 + src/core/attributes/entityhiddenattribute.h | 90 + .../favoritecollectionattribute.cpp | 29 + .../attributes/favoritecollectionattribute.h | 27 + src/core/attributes/indexpolicyattribute.cpp | 68 + src/core/attributes/indexpolicyattribute.h | 63 + .../attributes/persistentsearchattribute.cpp | 149 + .../attributes/persistentsearchattribute.h | 166 + .../attributes/specialcollectionattribute.cpp | 72 + .../attributes/specialcollectionattribute.h | 63 + src/core/attributes/tagattribute.cpp | 201 ++ src/core/attributes/tagattribute.h | 93 + src/core/attributestorage.cpp | 135 + src/core/attributestorage_p.h | 53 + src/core/braveheart.cpp | 63 + src/core/cachepolicy.cpp | 113 + src/core/cachepolicy.h | 156 + src/core/changemediator_p.cpp | 93 + src/core/changemediator_p.h | 43 + src/core/changenotification.cpp | 88 + src/core/changenotification.h | 69 + .../changenotificationdependenciesfactory.cpp | 64 + .../changenotificationdependenciesfactory_p.h | 44 + src/core/changerecorder.cpp | 115 + src/core/changerecorder.h | 109 + src/core/changerecorder_p.cpp | 223 ++ src/core/changerecorder_p.h | 53 + src/core/changerecorderjournal.cpp | 1011 ++++++ src/core/changerecorderjournal_p.h | 84 + src/core/collection.cpp | 427 +++ src/core/collection.h | 588 ++++ src/core/collection_p.h | 109 + src/core/collectionfetchscope.cpp | 189 ++ src/core/collectionfetchscope.h | 271 ++ src/core/collectionpathresolver.cpp | 220 ++ src/core/collectionpathresolver.h | 95 + src/core/collectionstatistics.cpp | 79 + src/core/collectionstatistics.h | 146 + src/core/collectionsync.cpp | 859 +++++ src/core/collectionsync_p.h | 120 + src/core/collectionutils.h | 122 + src/core/commandbuffer_p.h | 140 + src/core/config.cpp | 47 + src/core/config_p.h | 38 + src/core/conflicthandler.cpp | 129 + src/core/conflicthandler_p.h | 106 + src/core/connection.cpp | 324 ++ src/core/connection_p.h | 77 + src/core/control.cpp | 154 + src/core/control.h | 96 + src/core/differencesalgorithminterface.h | 45 + src/core/entitycache.cpp | 22 + src/core/entitycache_p.h | 520 +++ src/core/exception.cpp | 95 + src/core/exceptionbase.h | 97 + src/core/firstrun.cpp | 212 ++ src/core/firstrun_p.h | 74 + src/core/gidextractor.cpp | 36 + src/core/gidextractor_p.h | 38 + src/core/gidextractorinterface.h | 47 + src/core/item.cpp | 547 +++ src/core/item.h | 937 ++++++ src/core/item_p.h | 294 ++ src/core/itemchangelog.cpp | 74 + src/core/itemchangelog_p.h | 47 + src/core/itemfetchscope.cpp | 218 ++ src/core/itemfetchscope.h | 422 +++ src/core/itemfetchscope_p.h | 80 + src/core/itemmonitor.cpp | 74 + src/core/itemmonitor.h | 138 + src/core/itemmonitor_p.h | 74 + src/core/itempayloadinternals_p.h | 435 +++ src/core/itemserializer.cpp | 238 ++ src/core/itemserializer_p.h | 128 + src/core/itemserializerplugin.cpp | 64 + src/core/itemserializerplugin.h | 218 ++ src/core/itemsync.cpp | 565 ++++ src/core/itemsync.h | 256 ++ src/core/jobs/agentinstancecreatejob.cpp | 190 ++ src/core/jobs/agentinstancecreatejob.h | 108 + ...collectionattributessynchronizationjob.cpp | 142 + .../collectionattributessynchronizationjob.h | 70 + src/core/jobs/collectioncopyjob.cpp | 75 + src/core/jobs/collectioncopyjob.h | 72 + src/core/jobs/collectioncreatejob.cpp | 115 + src/core/jobs/collectioncreatejob.h | 73 + src/core/jobs/collectiondeletejob.cpp | 67 + src/core/jobs/collectiondeletejob.h | 80 + src/core/jobs/collectionfetchjob.cpp | 411 +++ src/core/jobs/collectionfetchjob.h | 179 + src/core/jobs/collectionmodifyjob.cpp | 126 + src/core/jobs/collectionmodifyjob.h | 105 + src/core/jobs/collectionmovejob.cpp | 79 + src/core/jobs/collectionmovejob.h | 58 + src/core/jobs/collectionstatisticsjob.cpp | 84 + src/core/jobs/collectionstatisticsjob.h | 89 + src/core/jobs/invalidatecachejob.cpp | 119 + src/core/jobs/invalidatecachejob_p.h | 38 + src/core/jobs/itemcopyjob.cpp | 84 + src/core/jobs/itemcopyjob.h | 84 + src/core/jobs/itemcreatejob.cpp | 236 ++ src/core/jobs/itemcreatejob.h | 124 + src/core/jobs/itemdeletejob.cpp | 115 + src/core/jobs/itemdeletejob.h | 135 + src/core/jobs/itemfetchjob.cpp | 282 ++ src/core/jobs/itemfetchjob.h | 251 ++ src/core/jobs/itemmodifyjob.cpp | 414 +++ src/core/jobs/itemmodifyjob.h | 196 ++ src/core/jobs/itemmodifyjob_p.h | 65 + src/core/jobs/itemmovejob.cpp | 133 + src/core/jobs/itemmovejob.h | 96 + src/core/jobs/itemsearchjob.cpp | 269 ++ src/core/jobs/itemsearchjob.h | 248 ++ src/core/jobs/job.cpp | 417 +++ src/core/jobs/job.h | 220 ++ src/core/jobs/job_p.h | 118 + src/core/jobs/kjobprivatebase.cpp | 35 + src/core/jobs/kjobprivatebase_p.h | 37 + src/core/jobs/linkjob.cpp | 45 + src/core/jobs/linkjob.h | 81 + src/core/jobs/linkjobimpl_p.h | 69 + src/core/jobs/recursiveitemfetchjob.cpp | 122 + src/core/jobs/recursiveitemfetchjob.h | 134 + src/core/jobs/relationcreatejob.cpp | 64 + src/core/jobs/relationcreatejob.h | 47 + src/core/jobs/relationdeletejob.cpp | 63 + src/core/jobs/relationdeletejob.h | 47 + src/core/jobs/relationfetchjob.cpp | 120 + src/core/jobs/relationfetchjob.h | 60 + src/core/jobs/resourceselectjob.cpp | 55 + src/core/jobs/resourceselectjob_p.h | 92 + src/core/jobs/resourcesynchronizationjob.cpp | 165 + src/core/jobs/resourcesynchronizationjob.h | 102 + src/core/jobs/searchcreatejob.cpp | 152 + src/core/jobs/searchcreatejob.h | 173 + src/core/jobs/searchresultjob.cpp | 116 + src/core/jobs/searchresultjob_p.h | 40 + .../jobs/specialcollectionsdiscoveryjob.cpp | 65 + .../jobs/specialcollectionsdiscoveryjob.h | 60 + .../jobs/specialcollectionshelperjobs.cpp | 652 ++++ .../jobs/specialcollectionshelperjobs_p.h | 221 ++ .../jobs/specialcollectionsrequestjob.cpp | 347 ++ src/core/jobs/specialcollectionsrequestjob.h | 119 + src/core/jobs/subscriptionjob.cpp | 88 + src/core/jobs/subscriptionjob_p.h | 61 + src/core/jobs/tagcreatejob.cpp | 84 + src/core/jobs/tagcreatejob.h | 56 + src/core/jobs/tagdeletejob.cpp | 58 + src/core/jobs/tagdeletejob.h | 44 + src/core/jobs/tagfetchjob.cpp | 158 + src/core/jobs/tagfetchjob.h | 123 + src/core/jobs/tagmodifyjob.cpp | 80 + src/core/jobs/tagmodifyjob.h | 48 + src/core/jobs/transactionjobs.cpp | 86 + src/core/jobs/transactionjobs.h | 122 + src/core/jobs/transactionsequence.cpp | 251 ++ src/core/jobs/transactionsequence.h | 121 + src/core/jobs/trashjob.cpp | 381 +++ src/core/jobs/trashjob.h | 118 + src/core/jobs/trashrestorejob.cpp | 352 ++ src/core/jobs/trashrestorejob.h | 74 + src/core/jobs/unlinkjob.cpp | 46 + src/core/jobs/unlinkjob.h | 81 + src/core/kcfg2dbus.xsl | 91 + src/core/mimetypechecker.cpp | 173 + src/core/mimetypechecker.h | 254 ++ src/core/mimetypechecker_p.h | 58 + src/core/models/agentfilterproxymodel.cpp | 146 + src/core/models/agentfilterproxymodel.h | 87 + src/core/models/agentinstancemodel.cpp | 243 ++ src/core/models/agentinstancemodel.h | 89 + src/core/models/agenttypemodel.cpp | 153 + src/core/models/agenttypemodel.h | 79 + .../models/collectionfilterproxymodel.cpp | 165 + src/core/models/collectionfilterproxymodel.h | 108 + src/core/models/entitymimetypefiltermodel.cpp | 235 ++ src/core/models/entitymimetypefiltermodel.h | 140 + src/core/models/entityorderproxymodel.cpp | 320 ++ src/core/models/entityorderproxymodel.h | 96 + src/core/models/entityrightsfiltermodel.cpp | 116 + src/core/models/entityrightsfiltermodel.h | 100 + src/core/models/entitytreemodel.cpp | 1113 ++++++ src/core/models/entitytreemodel.h | 717 ++++ src/core/models/entitytreemodel_p.cpp | 1859 +++++++++++ src/core/models/entitytreemodel_p.h | 396 +++ src/core/models/favoritecollectionsmodel.cpp | 485 +++ src/core/models/favoritecollectionsmodel.h | 148 + .../recursivecollectionfilterproxymodel.cpp | 147 + .../recursivecollectionfilterproxymodel.h | 102 + src/core/models/selectionproxymodel.cpp | 75 + src/core/models/selectionproxymodel.h | 109 + src/core/models/statisticsproxymodel.cpp | 322 ++ src/core/models/statisticsproxymodel.h | 96 + src/core/models/subscriptionmodel.cpp | 209 ++ src/core/models/subscriptionmodel_p.h | 77 + src/core/models/tagmodel.cpp | 167 + src/core/models/tagmodel.h | 77 + src/core/models/tagmodel_p.cpp | 235 ++ src/core/models/tagmodel_p.h | 52 + src/core/models/trashfilterproxymodel.cpp | 64 + src/core/models/trashfilterproxymodel.h | 67 + src/core/monitor.cpp | 376 +++ src/core/monitor.h | 813 +++++ src/core/monitor_p.cpp | 1395 ++++++++ src/core/monitor_p.h | 398 +++ src/core/notificationsource_p.cpp | 101 + src/core/notificationsource_p.h | 46 + src/core/notificationsubscriber.cpp | 200 ++ src/core/notificationsubscriber.h | 74 + src/core/partfetcher.cpp | 170 + src/core/partfetcher.h | 106 + src/core/pastehelper.cpp | 324 ++ src/core/pastehelper_p.h | 56 + src/core/pluginloader.cpp | 174 + src/core/pluginloader_p.h | 57 + src/core/protocolhelper.cpp | 764 +++++ src/core/protocolhelper_p.h | 315 ++ src/core/qtest_akonadi.h | 193 ++ src/core/relation.cpp | 113 + src/core/relation.h | 122 + src/core/relationsync.cpp | 103 + src/core/relationsync.h | 43 + src/core/remotelog.cpp | 27 + src/core/searchquery.cpp | 362 ++ src/core/searchquery.h | 290 ++ src/core/servermanager.cpp | 418 +++ src/core/servermanager.h | 225 ++ src/core/servermanager_p.h | 30 + src/core/session.cpp | 459 +++ src/core/session.h | 127 + src/core/session_p.h | 137 + src/core/sessionthread.cpp | 96 + src/core/sessionthread_p.h | 38 + src/core/sharedvaluepool_p.h | 40 + src/core/specialcollections.cpp | 269 ++ src/core/specialcollections.h | 157 + src/core/specialcollections_p.h | 77 + src/core/supertrait.h | 49 + src/core/tag.cpp | 240 ++ src/core/tag.h | 252 ++ src/core/tag_p.h | 50 + src/core/tagfetchscope.cpp | 90 + src/core/tagfetchscope.h | 123 + src/core/tagsync.cpp | 235 ++ src/core/tagsync.h | 50 + src/core/trashsettings.cpp | 33 + src/core/trashsettings.h | 38 + src/core/typepluginloader.cpp | 437 +++ src/core/typepluginloader_p.h | 82 + src/interfaces/CMakeLists.txt | 21 + .../org.freedesktop.Akonadi.Agent.Control.xml | 19 + .../org.freedesktop.Akonadi.Agent.Search.xml | 21 + .../org.freedesktop.Akonadi.Agent.Status.xml | 42 + .../org.freedesktop.Akonadi.AgentManager.xml | 153 + ...eedesktop.Akonadi.AgentManagerInternal.xml | 15 + .../org.freedesktop.Akonadi.AgentServer.xml | 22 + ...org.freedesktop.Akonadi.ControlManager.xml | 6 + .../org.freedesktop.Akonadi.Janitor.xml | 15 + ...reedesktop.Akonadi.NotificationManager.xml | 20 + ...freedesktop.Akonadi.NotificationSource.xml | 105 + .../org.freedesktop.Akonadi.Preprocessor.xml | 14 + ...reedesktop.Akonadi.PreprocessorManager.xml | 13 + ...freedesktop.Akonadi.Resource.Transport.xml | 14 + .../org.freedesktop.Akonadi.Resource.xml | 50 + ...org.freedesktop.Akonadi.Resource2.Task.xml | 43 + .../org.freedesktop.Akonadi.Resource2.xml | 55 + ...rg.freedesktop.Akonadi.ResourceManager.xml | 15 + .../org.freedesktop.Akonadi.SearchManager.xml | 13 + .../org.freedesktop.Akonadi.Server.xml | 9 + ...rg.freedesktop.Akonadi.StorageDebugger.xml | 65 + .../org.freedesktop.Akonadi.Tracer.xml | 17 + ...freedesktop.Akonadi.TracerNotification.xml | 33 + src/interfaces/org.kde.Akonadi.Accounts.xml | 11 + src/private/CMakeLists.txt | 128 + src/private/capabilities_p.h | 22 + src/private/compressionstream.cpp | 324 ++ src/private/compressionstream_p.h | 48 + src/private/datastream_p.cpp | 133 + src/private/datastream_p_p.h | 327 ++ src/private/dbus.cpp | 138 + src/private/dbus_p.h | 77 + src/private/externalpartstorage.cpp | 311 ++ src/private/externalpartstorage_p.h | 90 + src/private/imapparser.cpp | 686 ++++ src/private/imapparser_p.h | 196 ++ src/private/imapset.cpp | 295 ++ src/private/imapset_p.h | 229 ++ src/private/instance.cpp | 55 + src/private/instance_p.h | 23 + src/private/protocol.cpp | 848 +++++ src/private/protocol.xml | 1112 ++++++ src/private/protocol_exception_p.h | 42 + src/private/protocol_p.h | 717 ++++ src/private/protocolgen/CMakeLists.txt | 16 + src/private/protocolgen/cppgenerator.cpp | 836 +++++ src/private/protocolgen/cppgenerator.h | 50 + src/private/protocolgen/cpphelper.cpp | 67 + src/private/protocolgen/cpphelper.h | 20 + src/private/protocolgen/main.cpp | 41 + src/private/protocolgen/nodetree.cpp | 309 ++ src/private/protocolgen/nodetree.h | 191 ++ src/private/protocolgen/typehelper.cpp | 83 + src/private/protocolgen/typehelper.h | 27 + src/private/protocolgen/xmlparser.cpp | 276 ++ src/private/protocolgen/xmlparser.h | 44 + src/private/scope.cpp | 407 +++ src/private/scope_p.h | 117 + src/private/standarddirs.cpp | 186 ++ src/private/standarddirs_p.h | 100 + src/private/tristate.cpp | 24 + src/private/tristate_p.h | 26 + src/qsqlite/.no_coding_style | 1 + src/qsqlite/CMakeLists.txt | 31 + src/qsqlite/README | 3 + src/qsqlite/src/qsql_sqlite.cpp | 818 +++++ src/qsqlite/src/qsql_sqlite.h | 57 + src/qsqlite/src/smain.cpp | 46 + src/qsqlite/src/sqlite3.json | 3 + src/qsqlite/src/sqlite_blocking.cpp | 91 + src/qsqlite/src/sqlite_blocking.h | 24 + src/rds/CMakeLists.txt | 22 + src/rds/bridgeconnection.cpp | 111 + src/rds/bridgeconnection.h | 57 + src/rds/bridgeserver.cpp | 19 + src/rds/bridgeserver.h | 42 + src/rds/exception.h | 23 + src/rds/main.cpp | 30 + src/selftest/CMakeLists.txt | 15 + src/selftest/main.cpp | 34 + src/server/CMakeLists.txt | 213 ++ src/server/aggregatedfetchscope.cpp | 492 +++ src/server/aggregatedfetchscope.h | 104 + src/server/aklocalserver.cpp | 19 + src/server/aklocalserver.h | 30 + src/server/akonadi.cpp | 455 +++ src/server/akonadi.h | 124 + src/server/akthread.cpp | 78 + src/server/akthread.h | 44 + src/server/cachecleaner.cpp | 139 + src/server/cachecleaner.h | 76 + src/server/collectionscheduler.cpp | 341 ++ src/server/collectionscheduler.h | 96 + src/server/commandcontext.cpp | 87 + src/server/commandcontext.h | 50 + src/server/connection.cpp | 483 +++ src/server/connection.h | 156 + src/server/dbustracer.cpp | 55 + src/server/dbustracer.h | 53 + src/server/debuginterface.cpp | 30 + src/server/debuginterface.h | 38 + src/server/exception.h | 81 + src/server/filetracer.cpp | 60 + src/server/filetracer.h | 43 + src/server/global.h | 23 + src/server/handler.cpp | 249 ++ src/server/handler.h | 142 + src/server/handler/collectioncopyhandler.cpp | 110 + src/server/handler/collectioncopyhandler.h | 48 + .../handler/collectioncreatehandler.cpp | 118 + src/server/handler/collectioncreatehandler.h | 28 + .../handler/collectiondeletehandler.cpp | 68 + src/server/handler/collectiondeletehandler.h | 39 + src/server/handler/collectionfetchhandler.cpp | 555 +++ src/server/handler/collectionfetchhandler.h | 80 + .../handler/collectionmodifyhandler.cpp | 285 ++ src/server/handler/collectionmodifyhandler.h | 32 + src/server/handler/collectionmovehandler.cpp | 71 + src/server/handler/collectionmovehandler.h | 34 + .../handler/collectionstatsfetchhandler.cpp | 45 + .../handler/collectionstatsfetchhandler.h | 30 + src/server/handler/itemcopyhandler.cpp | 114 + src/server/handler/itemcopyhandler.h | 54 + src/server/handler/itemcreatehandler.cpp | 496 +++ src/server/handler/itemcreatehandler.h | 49 + src/server/handler/itemdeletehandler.cpp | 57 + src/server/handler/itemdeletehandler.h | 37 + src/server/handler/itemfetchhandler.cpp | 43 + src/server/handler/itemfetchhandler.h | 31 + src/server/handler/itemfetchhelper.cpp | 747 +++++ src/server/handler/itemfetchhelper.h | 93 + src/server/handler/itemlinkhandler.cpp | 101 + src/server/handler/itemlinkhandler.h | 34 + src/server/handler/itemmodifyhandler.cpp | 384 +++ src/server/handler/itemmodifyhandler.h | 73 + src/server/handler/itemmovehandler.cpp | 161 + src/server/handler/itemmovehandler.h | 44 + src/server/handler/loginhandler.cpp | 31 + src/server/handler/loginhandler.h | 30 + src/server/handler/logouthandler.cpp | 23 + src/server/handler/logouthandler.h | 30 + src/server/handler/relationfetchhandler.cpp | 77 + src/server/handler/relationfetchhandler.h | 31 + src/server/handler/relationmodifyhandler.cpp | 109 + src/server/handler/relationmodifyhandler.h | 31 + src/server/handler/relationremovehandler.cpp | 79 + src/server/handler/relationremovehandler.h | 26 + src/server/handler/resourceselecthandler.cpp | 39 + src/server/handler/resourceselecthandler.h | 36 + src/server/handler/searchcreatehandler.cpp | 81 + src/server/handler/searchcreatehandler.h | 31 + src/server/handler/searchhandler.cpp | 96 + src/server/handler/searchhandler.h | 40 + src/server/handler/searchhelper.cpp | 95 + src/server/handler/searchhelper.h | 24 + src/server/handler/searchresulthandler.cpp | 69 + src/server/handler/searchresulthandler.h | 35 + src/server/handler/tagcreatehandler.cpp | 136 + src/server/handler/tagcreatehandler.h | 26 + src/server/handler/tagdeletehandler.cpp | 45 + src/server/handler/tagdeletehandler.h | 26 + src/server/handler/tagfetchhandler.cpp | 33 + src/server/handler/tagfetchhandler.h | 31 + src/server/handler/tagfetchhelper.cpp | 160 + src/server/handler/tagfetchhelper.h | 43 + src/server/handler/tagmodifyhandler.cpp | 156 + src/server/handler/tagmodifyhandler.h | 26 + src/server/handler/transactionhandler.cpp | 52 + src/server/handler/transactionhandler.h | 31 + src/server/handlerhelper.cpp | 427 +++ src/server/handlerhelper.h | 116 + src/server/intervalcheck.cpp | 81 + src/server/intervalcheck.h | 54 + src/server/main.cpp | 73 + src/server/notificationmanager.cpp | 202 ++ src/server/notificationmanager.h | 84 + src/server/notificationsubscriber.cpp | 660 ++++ src/server/notificationsubscriber.h | 105 + src/server/preprocessorinstance.cpp | 208 ++ src/server/preprocessorinstance.h | 186 ++ src/server/preprocessormanager.cpp | 434 +++ src/server/preprocessormanager.h | 259 ++ src/server/resourcemanager.cpp | 63 + src/server/resourcemanager.h | 40 + src/server/search/abstractsearchengine.h | 46 + src/server/search/abstractsearchplugin.h | 55 + src/server/search/agentsearchengine.cpp | 44 + src/server/search/agentsearchengine.h | 25 + src/server/search/agentsearchinstance.cpp | 59 + src/server/search/agentsearchinstance.h | 44 + src/server/search/searchmanager.cpp | 421 +++ src/server/search/searchmanager.h | 108 + src/server/search/searchrequest.cpp | 145 + src/server/search/searchrequest.h | 73 + src/server/search/searchtaskmanager.cpp | 300 ++ src/server/search/searchtaskmanager.h | 96 + src/server/storage/akonadi-mysql-client.sh | 15 + src/server/storage/akonadi-mysql-server.sh | 16 + src/server/storage/akonadidb.qrc | 5 + src/server/storage/akonadidb.xml | 245 ++ src/server/storage/akonadidb.xsd | 101 + src/server/storage/collectionqueryhelper.cpp | 148 + src/server/storage/collectionqueryhelper.h | 61 + src/server/storage/collectionstatistics.cpp | 209 ++ src/server/storage/collectionstatistics.h | 64 + src/server/storage/collectiontreecache.cpp | 382 +++ src/server/storage/collectiontreecache.h | 103 + src/server/storage/countquerybuilder.h | 76 + src/server/storage/datastore.cpp | 1471 ++++++++ src/server/storage/datastore.h | 371 ++ src/server/storage/dbconfig.cpp | 151 + src/server/storage/dbconfig.h | 131 + src/server/storage/dbconfigmysql.cpp | 636 ++++ src/server/storage/dbconfigmysql.h | 98 + src/server/storage/dbconfigpostgresql.cpp | 647 ++++ src/server/storage/dbconfigpostgresql.h | 98 + src/server/storage/dbconfigsqlite.cpp | 279 ++ src/server/storage/dbconfigsqlite.h | 80 + src/server/storage/dbdeadlockcatcher.h | 46 + src/server/storage/dbexception.cpp | 29 + src/server/storage/dbexception.h | 33 + src/server/storage/dbinitializer.cpp | 397 +++ src/server/storage/dbinitializer.h | 183 + src/server/storage/dbinitializer_p.cpp | 354 ++ src/server/storage/dbinitializer_p.h | 71 + src/server/storage/dbintrospector.cpp | 107 + src/server/storage/dbintrospector.h | 112 + src/server/storage/dbintrospector_impl.cpp | 199 ++ src/server/storage/dbintrospector_impl.h | 42 + src/server/storage/dbtype.cpp | 33 + src/server/storage/dbtype.h | 38 + src/server/storage/dbupdate.xml | 342 ++ src/server/storage/dbupdate.xsd | 44 + src/server/storage/dbupdater.cpp | 570 ++++ src/server/storage/dbupdater.h | 82 + .../storage/doxygen-preprocess-entities.sh | 17 + src/server/storage/entities-dox.xsl | 65 + src/server/storage/entities-header.xsl | 299 ++ src/server/storage/entities-source.xsl | 734 ++++ src/server/storage/entities.xsl | 262 ++ src/server/storage/entity.cpp | 167 + src/server/storage/entity.h | 168 + src/server/storage/itemqueryhelper.cpp | 139 + src/server/storage/itemqueryhelper.h | 54 + src/server/storage/itemretrievaljob.cpp | 63 + src/server/storage/itemretrievaljob.h | 77 + src/server/storage/itemretrievalmanager.cpp | 224 ++ src/server/storage/itemretrievalmanager.h | 97 + src/server/storage/itemretrievalrequest.cpp | 16 + src/server/storage/itemretrievalrequest.h | 77 + src/server/storage/itemretriever.cpp | 440 +++ src/server/storage/itemretriever.h | 101 + src/server/storage/mysql-global-mobile.conf | 103 + src/server/storage/mysql-global.conf | 100 + src/server/storage/notificationcollector.cpp | 605 ++++ src/server/storage/notificationcollector.h | 247 ++ src/server/storage/parthelper.cpp | 188 ++ src/server/storage/parthelper.h | 68 + src/server/storage/partstreamer.cpp | 358 ++ src/server/storage/partstreamer.h | 65 + src/server/storage/parttypehelper.cpp | 69 + src/server/storage/parttypehelper.h | 89 + src/server/storage/query.cpp | 93 + src/server/storage/query.h | 145 + src/server/storage/querybuilder.cpp | 686 ++++ src/server/storage/querybuilder.h | 305 ++ src/server/storage/querycache.cpp | 114 + src/server/storage/querycache.h | 37 + src/server/storage/queryhelper.cpp | 44 + src/server/storage/queryhelper.h | 33 + src/server/storage/schema-header.xsl | 33 + src/server/storage/schema-source.xsl | 149 + src/server/storage/schema.h | 38 + src/server/storage/schema.xsl | 61 + src/server/storage/schematypes.cpp | 82 + src/server/storage/schematypes.h | 120 + src/server/storage/selectquerybuilder.h | 43 + src/server/storage/storagedebugger.cpp | 223 ++ src/server/storage/storagedebugger.h | 103 + src/server/storage/tagqueryhelper.cpp | 73 + src/server/storage/tagqueryhelper.h | 42 + src/server/storage/transaction.cpp | 38 + src/server/storage/transaction.h | 61 + src/server/storagejanitor.cpp | 863 +++++ src/server/storagejanitor.h | 140 + src/server/tracer.cpp | 163 + src/server/tracer.h | 156 + src/server/tracerinterface.h | 108 + src/server/utils.cpp | 231 ++ src/server/utils.h | 109 + src/shared/CMakeLists.txt | 32 + src/shared/akapplication.cpp | 116 + src/shared/akapplication.h | 116 + src/shared/akdebug.cpp | 246 ++ src/shared/akdebug.h | 17 + src/shared/akhelpers.h | 19 + src/shared/akqt.h | 36 + src/shared/akranges.h | 478 +++ src/shared/akremotelog.cpp | 202 ++ src/shared/akremotelog.h | 10 + src/shared/akscopeguard.h | 38 + src/shared/akstd.h | 19 + src/shared/aktest.h | 130 + src/shared/aktraits.h | 117 + src/shared/vectorhelper.h | 47 + src/widgets/CMakeLists.txt | 201 ++ src/widgets/actionstatemanager.cpp | 401 +++ src/widgets/actionstatemanager_p.h | 70 + src/widgets/agentactionmanager.cpp | 319 ++ src/widgets/agentactionmanager.h | 168 + src/widgets/agentconfigurationdialog.cpp | 107 + src/widgets/agentconfigurationdialog.h | 31 + src/widgets/agentconfigurationwidget.cpp | 166 + src/widgets/agentconfigurationwidget.h | 49 + src/widgets/agentconfigurationwidget_p.h | 46 + src/widgets/agentinstancewidget.cpp | 295 ++ src/widgets/agentinstancewidget.h | 125 + src/widgets/agenttypedialog.cpp | 114 + src/widgets/agenttypedialog.h | 82 + src/widgets/agenttypewidget.cpp | 261 ++ src/widgets/agenttypewidget.h | 90 + src/widgets/akonadiwidgetstests_export.h.in | 2 + src/widgets/cachepolicypage.cpp | 155 + src/widgets/cachepolicypage.h | 78 + src/widgets/cachepolicypage.ui | 302 ++ src/widgets/collectioncombobox.cpp | 173 + src/widgets/collectioncombobox.h | 136 + src/widgets/collectiondialog.cpp | 417 +++ src/widgets/collectiondialog.h | 202 ++ .../collectiongeneralpropertiespage.cpp | 78 + .../collectiongeneralpropertiespage.ui | 137 + .../collectiongeneralpropertiespage_p.h | 36 + src/widgets/collectionmaintenancepage.cpp | 155 + src/widgets/collectionmaintenancepage.h | 37 + src/widgets/collectionmaintenancepage.ui | 153 + src/widgets/collectionpropertiesdialog.cpp | 233 ++ src/widgets/collectionpropertiesdialog.h | 135 + src/widgets/collectionpropertiespage.cpp | 53 + src/widgets/collectionpropertiespage.h | 210 ++ src/widgets/collectionrequester.cpp | 259 ++ src/widgets/collectionrequester.h | 134 + src/widgets/collectionstatisticsdelegate.cpp | 332 ++ src/widgets/collectionstatisticsdelegate.h | 127 + src/widgets/collectionview.cpp | 253 ++ src/widgets/collectionview.h | 125 + src/widgets/conflictresolvedialog.cpp | 309 ++ src/widgets/conflictresolvedialog_p.h | 70 + src/widgets/controlgui.cpp | 262 ++ src/widgets/controlgui.h | 126 + src/widgets/controlprogressindicator.ui | 37 + src/widgets/dragdropmanager.cpp | 324 ++ src/widgets/dragdropmanager_p.h | 68 + src/widgets/entitylistview.cpp | 241 ++ src/widgets/entitylistview.h | 193 ++ src/widgets/entitytreeview.cpp | 327 ++ src/widgets/entitytreeview.h | 224 ++ src/widgets/erroroverlay.cpp | 256 ++ src/widgets/erroroverlay.ui | 433 +++ src/widgets/erroroverlay_p.h | 61 + src/widgets/etmviewstatesaver.cpp | 113 + src/widgets/etmviewstatesaver.h | 41 + src/widgets/itemview.cpp | 167 + src/widgets/itemview.h | 133 + src/widgets/manageaccountwidget.cpp | 223 ++ src/widgets/manageaccountwidget.h | 69 + src/widgets/manageaccountwidget.ui | 108 + src/widgets/progressspinnerdelegate.cpp | 111 + src/widgets/progressspinnerdelegate_p.h | 79 + src/widgets/recentcollectionaction.cpp | 152 + src/widgets/recentcollectionaction_p.h | 68 + src/widgets/renamefavoritedialog.cpp | 36 + src/widgets/renamefavoritedialog.ui | 56 + src/widgets/renamefavoritedialog_p.h | 29 + src/widgets/selftestdialog.cpp | 670 ++++ src/widgets/selftestdialog.h | 80 + src/widgets/selftestdialog.ui | 78 + src/widgets/standardactionmanager.cpp | 1990 +++++++++++ src/widgets/standardactionmanager.h | 428 +++ src/widgets/subscriptiondialog.cpp | 160 + src/widgets/subscriptiondialog.h | 59 + src/widgets/subscriptiondialog.ui | 118 + src/widgets/tageditwidget.cpp | 283 ++ src/widgets/tageditwidget.h | 49 + src/widgets/tageditwidget.ui | 64 + src/widgets/tagmanagementdialog.cpp | 76 + src/widgets/tagmanagementdialog.h | 38 + src/widgets/tagmanagementdialog.ui | 72 + src/widgets/tagselectioncombobox.cpp | 299 ++ src/widgets/tagselectioncombobox.h | 53 + src/widgets/tagselectiondialog.cpp | 100 + src/widgets/tagselectiondialog.h | 50 + src/widgets/tagselectiondialog.ui | 72 + src/widgets/tagselectwidget.cpp | 70 + src/widgets/tagselectwidget.h | 48 + src/widgets/tagwidget.cpp | 139 + src/widgets/tagwidget.h | 51 + src/widgets/tagwidget.ui | 42 + src/xml/CMakeLists.txt | 99 + src/xml/akonadi-xml.xsd | 79 + src/xml/akonadi2xml.cpp | 56 + src/xml/autotests/CMakeLists.txt | 15 + src/xml/autotests/collectiontest.cpp | 95 + src/xml/autotests/collectiontest.h | 21 + src/xml/autotests/knutdemo.xml | 72 + src/xml/autotests/xmldocumenttest.cpp | 48 + src/xml/format_p.h | 88 + src/xml/xmldocument.cpp | 321 ++ src/xml/xmldocument.h | 122 + src/xml/xmlreader.cpp | 160 + src/xml/xmlreader.h | 71 + src/xml/xmlwritejob.cpp | 155 + src/xml/xmlwritejob.h | 38 + src/xml/xmlwriter.cpp | 121 + src/xml/xmlwriter.h | 63 + templates/.clang-format | 2 + templates/CMakeLists.txt | 6 + templates/akonadiresource/CMakeLists.txt | 33 + templates/akonadiresource/README | 89 + .../akonadiresource.kdevtemplate | 76 + templates/akonadiresource/akonadiresource.png | Bin 0 -> 69059 bytes .../src/%{APPNAMELC}resource.cpp | 105 + .../src/%{APPNAMELC}resource.desktop | 10 + .../src/%{APPNAMELC}resource.h | 37 + templates/akonadiresource/src/CMakeLists.txt | 39 + templates/akonadiresource/src/settings.kcfg | 14 + templates/akonadiresource/src/settings.kcfgc | 8 + templates/akonadiserializer/CMakeLists.txt | 27 + templates/akonadiserializer/README | 66 + .../akonadiserializer.kdevtemplate | 75 + .../akonadiserializer/akonadiserializer.png | Bin 0 -> 14537 bytes .../akonadiserializer/src/CMakeLists.txt | 16 + .../src/akonadi_serializer_%{APPNAMELC}.cpp | 40 + .../akonadi_serializer_%{APPNAMELC}.desktop | 7 + .../src/akonadi_serializer_%{APPNAMELC}.h | 33 + tests/CMakeLists.txt | 1 + tests/asapcat/imap-4.10-sync.asap | 4 + tests/asapcat/imap-4.11-body-check.asap | 4 + tests/asapcat/kmail-4.10-folder-listing.asap | 5 + tests/asapcat/kmail-4.11-folder-listing.asap | 5 + tests/asapcat/kmail-4.12-folder-listing.asap | 5 + tests/libs/CMakeLists.txt | 37 + tests/libs/agentinstancewidgettest.cpp | 66 + tests/libs/agentinstancewidgettest.h | 28 + tests/libs/agenttypewidgettest.cpp | 90 + tests/libs/agenttypewidgettest.h | 32 + tests/libs/collectiondialog.cpp | 45 + tests/libs/conflictresolvedialogtest_gui.cpp | 30 + tests/libs/etm_test_app/CMakeLists.txt | 16 + tests/libs/etm_test_app/main.cpp | 37 + tests/libs/etm_test_app/mainwindow.cpp | 72 + tests/libs/etm_test_app/mainwindow.h | 31 + tests/libs/itemdumper.cpp | 102 + tests/libs/itemdumper.h | 29 + tests/libs/pluginloadertest.cpp | 36 + tests/libs/selftester.cpp | 30 + tests/libs/subscriber.cpp | 30 + tools/clang-tidy-to-junit.py | 123 + tools/run-clang-tidy.sh | 46 + 1229 files changed, 301259 insertions(+) create mode 100644 .clang-tidy create mode 100644 .clang-tidy-ignore create mode 100644 .git-blame-ignore-revs create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .kateconfig create mode 100644 .krazy create mode 100644 AUTHORS create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 CMakePresets.json.license create mode 100644 CTestConfig.cmake create mode 100644 CTestCustom.cmake create mode 100644 ExtraDesktop.sh create mode 100644 INSTALL create mode 100644 Info.plist.template create mode 100644 KF5AkonadiConfig.cmake.in create mode 100644 KF5AkonadiMacros.cmake create mode 100644 LICENSES/BSD-3-Clause.txt create mode 100644 LICENSES/CC0-1.0.txt create mode 100644 LICENSES/GPL-2.0-only.txt create mode 100644 LICENSES/GPL-2.0-or-later.txt create mode 100644 LICENSES/GPL-3.0-only.txt create mode 100644 LICENSES/LGPL-2.0-only.txt create mode 100644 LICENSES/LGPL-2.0-or-later.txt create mode 100644 LICENSES/LGPL-2.1-only.txt create mode 100644 LICENSES/LGPL-2.1-or-later.txt create mode 100644 LICENSES/LicenseRef-KDE-Accepted-GPL.txt create mode 100644 LICENSES/MIT.txt create mode 100644 LICENSES/Qt-LGPL-exception-1.1.txt create mode 100644 NEWS create mode 100644 README.md create mode 100644 README.sqlite create mode 100644 akonadi-mime.xml create mode 100644 akonadifull-version.h.cmake create mode 100644 apparmor/CMakeLists.txt create mode 100644 apparmor/mariadbd_akonadi create mode 100644 apparmor/mysqld_akonadi create mode 100644 apparmor/postgresql_akonadi create mode 100644 apparmor/usr.bin.akonadiserver create mode 100644 autotests/CMakeLists.txt create mode 100644 autotests/akonadicontrol/CMakeLists.txt create mode 100644 autotests/akonadicontrol/agenttypetest.cpp create mode 100644 autotests/akonadicontrol/data/akonaditestresource.desktop create mode 100644 autotests/libs/CMakeLists.txt create mode 100644 autotests/libs/actionstatemanagertest.cpp create mode 100644 autotests/libs/attributefactorytest.cpp create mode 100644 autotests/libs/attributefactorytest.h create mode 100644 autotests/libs/autoincrementtest.cpp create mode 100644 autotests/libs/autoincrementtest.h create mode 100644 autotests/libs/cachepolicytest.cpp create mode 100644 autotests/libs/cachepolicytest.h create mode 100644 autotests/libs/cachetest.cpp create mode 100644 autotests/libs/changerecordertest.cpp create mode 100644 autotests/libs/collectionattributetest.cpp create mode 100644 autotests/libs/collectionattributetest.h create mode 100644 autotests/libs/collectioncolorattributetest.cpp create mode 100644 autotests/libs/collectioncopytest.cpp create mode 100644 autotests/libs/collectioncreatetest.cpp create mode 100644 autotests/libs/collectioncreator.cpp create mode 100644 autotests/libs/collectionjobtest.cpp create mode 100644 autotests/libs/collectionjobtest.h create mode 100644 autotests/libs/collectionmodifytest.cpp create mode 100644 autotests/libs/collectionmovetest.cpp create mode 100644 autotests/libs/collectionpathresolvertest.cpp create mode 100644 autotests/libs/collectionpathresolvertest.h create mode 100644 autotests/libs/collectionsynctest.cpp create mode 100644 autotests/libs/collectionutilstest.cpp create mode 100644 autotests/libs/conflictresolvedialogtest.cpp create mode 100644 autotests/libs/conflictresolvedialogtest.h create mode 100644 autotests/libs/entitycachetest.cpp create mode 100644 autotests/libs/entitydisplayattributetest.cpp create mode 100644 autotests/libs/entitytreemodeltest.cpp create mode 100644 autotests/libs/etmpopulationtest.cpp create mode 100644 autotests/libs/fakeakonadiservercommand.cpp create mode 100644 autotests/libs/fakeakonadiservercommand.h create mode 100644 autotests/libs/fakeentitycache.cpp create mode 100644 autotests/libs/fakeentitycache.h create mode 100644 autotests/libs/fakemonitor.cpp create mode 100644 autotests/libs/fakemonitor.h create mode 100644 autotests/libs/fakeserverdata.cpp create mode 100644 autotests/libs/fakeserverdata.h create mode 100644 autotests/libs/fakesession.cpp create mode 100644 autotests/libs/fakesession.h create mode 100644 autotests/libs/favoriteproxytest.cpp create mode 100644 autotests/libs/firstrunner.cpp create mode 100644 autotests/libs/gidtest.cpp create mode 100644 autotests/libs/gidtest.h create mode 100644 autotests/libs/inspectablechangerecorder.cpp create mode 100644 autotests/libs/inspectablechangerecorder.h create mode 100644 autotests/libs/inspectablemonitor.cpp create mode 100644 autotests/libs/inspectablemonitor.h create mode 100644 autotests/libs/invalidatecachejobtest.cpp create mode 100644 autotests/libs/itemappendtest.cpp create mode 100644 autotests/libs/itemappendtest.h create mode 100644 autotests/libs/itembenchmark.cpp create mode 100644 autotests/libs/itemcopytest.cpp create mode 100644 autotests/libs/itemdeletetest.cpp create mode 100644 autotests/libs/itemfetchtest.cpp create mode 100644 autotests/libs/itemfetchtest.h create mode 100644 autotests/libs/itemhydratest.cpp create mode 100644 autotests/libs/itemhydratest.h create mode 100644 autotests/libs/itemmovetest.cpp create mode 100644 autotests/libs/itemsearchjobtest.cpp create mode 100644 autotests/libs/itemserializertest.cpp create mode 100644 autotests/libs/itemserializertest.h create mode 100644 autotests/libs/itemstoretest.cpp create mode 100644 autotests/libs/itemstoretest.h create mode 100644 autotests/libs/itemsynctest.cpp create mode 100644 autotests/libs/itemtest.cpp create mode 100644 autotests/libs/itemtest.h create mode 100644 autotests/libs/jobtest.cpp create mode 100644 autotests/libs/lazypopulationtest.cpp create mode 100644 autotests/libs/linktest.cpp create mode 100644 autotests/libs/mimetypecheckertest.cpp create mode 100644 autotests/libs/mimetypecheckertest.h create mode 100644 autotests/libs/modelspy.cpp create mode 100644 autotests/libs/modelspy.h create mode 100644 autotests/libs/monitorfiltertest.cpp create mode 100644 autotests/libs/monitornotificationtest.cpp create mode 100644 autotests/libs/monitortest.cpp create mode 100644 autotests/libs/monitortest.h create mode 100644 autotests/libs/protocolhelpertest.cpp create mode 100644 autotests/libs/proxymodelstest.cpp create mode 100644 autotests/libs/relationtest.cpp create mode 100644 autotests/libs/resourceschedulertest.cpp create mode 100644 autotests/libs/resourceschedulertest.h create mode 100644 autotests/libs/resourcetest.cpp create mode 100644 autotests/libs/searchjobtest.cpp create mode 100644 autotests/libs/searchjobtest.h create mode 100644 autotests/libs/searchquerytest.cpp create mode 100644 autotests/libs/servermanagertest.cpp create mode 100644 autotests/libs/sharedvaluepooltest.cpp create mode 100644 autotests/libs/statisticsproxymodeltest.cpp create mode 100644 autotests/libs/subscriptiontest.cpp create mode 100644 autotests/libs/tagmodeltest.cpp create mode 100644 autotests/libs/tagsynctest.cpp create mode 100644 autotests/libs/tagtest.cpp create mode 100644 autotests/libs/tagtest_simple.cpp create mode 100644 autotests/libs/test_model_helpers.h create mode 100644 autotests/libs/testattribute.h create mode 100644 autotests/libs/testenvironmenttest.cpp create mode 100644 autotests/libs/testresource/CMakeLists.txt create mode 100644 autotests/libs/testresource/Info.plist.template create mode 100644 autotests/libs/testresource/Messages.sh create mode 100644 autotests/libs/testresource/knut-template.xml create mode 100644 autotests/libs/testresource/knutresource.cpp create mode 100644 autotests/libs/testresource/knutresource.desktop create mode 100644 autotests/libs/testresource/knutresource.h create mode 100644 autotests/libs/testresource/knutresource.kcfg create mode 100644 autotests/libs/testresource/settings.kcfgc create mode 100644 autotests/libs/testresource/tests/CMakeLists.txt create mode 100644 autotests/libs/testresource/tests/knut-empty.xml create mode 100644 autotests/libs/testresource/tests/knut-step1.xml create mode 100644 autotests/libs/testresource/tests/knut-step2.xml create mode 100644 autotests/libs/testresource/tests/knutdemo.js create mode 100644 autotests/libs/testresource/tests/knutdemo.xml create mode 100644 autotests/libs/testresource/tests/testmail.mbox create mode 100644 autotests/libs/testrunner/CMakeLists.txt create mode 100644 autotests/libs/testrunner/config.cpp create mode 100644 autotests/libs/testrunner/config.h create mode 100644 autotests/libs/testrunner/config.xml create mode 100644 autotests/libs/testrunner/main.cpp create mode 100644 autotests/libs/testrunner/setup.cpp create mode 100644 autotests/libs/testrunner/setup.h create mode 100644 autotests/libs/testrunner/shellscript.cpp create mode 100644 autotests/libs/testrunner/shellscript.h create mode 100644 autotests/libs/testrunner/testrunner-config.xsd create mode 100644 autotests/libs/testrunner/testrunner.cpp create mode 100644 autotests/libs/testrunner/testrunner.h create mode 100644 autotests/libs/testsearchplugin/CMakeLists.txt create mode 100644 autotests/libs/testsearchplugin/akonadi_test_searchplugin.json create mode 100644 autotests/libs/testsearchplugin/testsearchplugin.cpp create mode 100644 autotests/libs/testsearchplugin/testsearchplugin.h create mode 100644 autotests/libs/transactiontest.cpp create mode 100644 autotests/libs/transactiontest.h create mode 100644 autotests/libs/unittestenv/config.xml create mode 100644 autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc create mode 100644 autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc create mode 100644 autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc create mode 100644 autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc create mode 100644 autotests/libs/unittestenv/xdglocal/testdata-res1.xml create mode 100644 autotests/libs/unittestenv/xdglocal/testdata-res2.xml create mode 100644 autotests/libs/unittestenv/xdglocal/testdata-res3.xml create mode 100644 autotests/private/CMakeLists.txt create mode 100644 autotests/private/akdbustest.cpp create mode 100644 autotests/private/akstandarddirstest.cpp create mode 100644 autotests/private/compressionstreamtest.cpp create mode 100644 autotests/private/externalpartstoragetest.cpp create mode 100644 autotests/private/imapparsertest.cpp create mode 100644 autotests/private/imapparsertest.h create mode 100644 autotests/private/imapsettest.cpp create mode 100644 autotests/private/imapsettest.h create mode 100644 autotests/private/notificationmessagetest.cpp create mode 100644 autotests/private/notificationmessagetest.h create mode 100644 autotests/private/protocoltest.cpp create mode 100644 autotests/private/protocoltest.h create mode 100644 autotests/server/CMakeLists.txt create mode 100644 autotests/server/aggregatedfetchscopetest.cpp create mode 100644 autotests/server/collectioncreatehandlertest.cpp create mode 100644 autotests/server/collectionfetchhandlertest.cpp create mode 100644 autotests/server/collectionmodifyhandlertest.cpp create mode 100644 autotests/server/collectionschedulertest.cpp create mode 100644 autotests/server/collectionstatisticstest.cpp create mode 100644 autotests/server/collectiontreecachetest.cpp create mode 100644 autotests/server/dbconfigtest.cpp create mode 100644 autotests/server/dbdeadlockcatchertest.cpp create mode 100644 autotests/server/dbinitializer.cpp create mode 100644 autotests/server/dbinitializer.h create mode 100644 autotests/server/dbinitializertest.cpp create mode 100644 autotests/server/dbinitializertest.h create mode 100644 autotests/server/dbintrospectortest.cpp create mode 100644 autotests/server/dbpopulator.xsl create mode 100644 autotests/server/dbtest_data/dbdata.xml create mode 100644 autotests/server/dbtest_data/dbinit_mysql create mode 100644 autotests/server/dbtest_data/dbinit_mysql_incremental create mode 100644 autotests/server/dbtest_data/dbinit_psql create mode 100644 autotests/server/dbtest_data/dbinit_psql_incremental create mode 100644 autotests/server/dbtest_data/dbinit_sqlite create mode 100644 autotests/server/dbtest_data/dbinit_sqlite_incremental create mode 100644 autotests/server/dbtest_data/dbtest_data.qrc create mode 100644 autotests/server/dbtest_data/unittest_dbupdate.xml create mode 100644 autotests/server/dbtest_data/unittest_schema.xml create mode 100644 autotests/server/dbtypetest.cpp create mode 100644 autotests/server/dbupdatertest.cpp create mode 100644 autotests/server/dbupdatertest.h create mode 100644 autotests/server/fakeakonadiserver.cpp create mode 100644 autotests/server/fakeakonadiserver.h create mode 100644 autotests/server/fakeclient.cpp create mode 100644 autotests/server/fakeclient.h create mode 100644 autotests/server/fakeconnection.cpp create mode 100644 autotests/server/fakeconnection.h create mode 100644 autotests/server/fakedatastore.cpp create mode 100644 autotests/server/fakedatastore.h create mode 100644 autotests/server/fakeentities.h create mode 100644 autotests/server/fakeintervalcheck.cpp create mode 100644 autotests/server/fakeintervalcheck.h create mode 100644 autotests/server/fakeitemretrievalmanager.cpp create mode 100644 autotests/server/fakeitemretrievalmanager.h create mode 100644 autotests/server/fakesearchmanager.cpp create mode 100644 autotests/server/fakesearchmanager.h create mode 100644 autotests/server/fetchhandlertest.cpp create mode 100644 autotests/server/handlertest.cpp create mode 100644 autotests/server/inspectablenotificationcollector.cpp create mode 100644 autotests/server/inspectablenotificationcollector.h create mode 100644 autotests/server/itemcreatehandlertest.cpp create mode 100644 autotests/server/itemlinkhandlertest.cpp create mode 100644 autotests/server/itemmovehandlertest.cpp create mode 100644 autotests/server/itemretrievertest.cpp create mode 100644 autotests/server/mockobjects.h create mode 100644 autotests/server/notificationmanagertest.cpp create mode 100644 autotests/server/notificationsubscribertest.cpp create mode 100644 autotests/server/parthelpertest.cpp create mode 100644 autotests/server/partstreamertest.cpp create mode 100644 autotests/server/parttypehelpertest.cpp create mode 100644 autotests/server/querybuildertest.cpp create mode 100644 autotests/server/querybuildertest.h create mode 100644 autotests/server/relationhandlertest.cpp create mode 100644 autotests/server/searchtest.cpp create mode 100644 autotests/server/taghandlertest.cpp create mode 100644 autotests/shared/CMakeLists.txt create mode 100644 autotests/shared/akrangestest.cpp create mode 100644 autotests/shared/akscopeguardtest.cpp create mode 100644 autotests/widgets/CMakeLists.txt create mode 100644 autotests/widgets/subscriptiondialogtest.cpp create mode 100644 autotests/widgets/tageditwidgettest.cpp create mode 100644 autotests/widgets/tagselectioncomboboxtest.cpp create mode 100644 autotests/widgets/tagwidgettest.cpp create mode 100644 autotests/widgets/unittestenv/config.xml create mode 100644 autotests/widgets/unittestenv/xdgconfig/akonadi-firstrunrc create mode 100644 autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_0rc create mode 100644 autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_1rc create mode 100644 autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_2rc create mode 100644 autotests/widgets/unittestenv/xdglocal/testdata-res1.xml create mode 100644 autotests/widgets/unittestenv/xdglocal/testdata-res2.xml create mode 100644 autotests/widgets/unittestenv/xdglocal/testdata-res3.xml create mode 100644 cmake/modules/AkonadiMacros.cmake create mode 100644 cmake/modules/FindSqlite.cmake create mode 100644 config-akonadi.h.cmake create mode 100644 docs/client_libraries.md create mode 100644 docs/history.md create mode 100644 docs/images/bufferedcaching1.png create mode 100644 docs/images/bufferedcaching2.png create mode 100644 docs/images/bufferedcaching3.png create mode 100644 docs/images/bufferedcaching4.png create mode 100644 docs/images/bufferedcaching6.png create mode 100644 docs/images/descendantentitiesproxymodel-colfilter.png create mode 100644 docs/images/descendantentitiesproxymodel-withansecnames.png create mode 100644 docs/images/descendantentitiesproxymodel.png create mode 100644 docs/images/entitytreemodel-collections.png create mode 100644 docs/images/entitytreemodel-showroot.png create mode 100644 docs/images/entitytreemodel-showrootwithname.png create mode 100644 docs/images/entitytreemodel.png create mode 100644 docs/images/mailmodelapp.png create mode 100644 docs/images/selectionproxymodel-ordered.png create mode 100644 docs/images/selectionproxymodelmultipleselection-withdescendant.png create mode 100644 docs/images/selectionproxymodelmultipleselection.png create mode 100644 docs/images/selectionproxymodelsimpleselection.png create mode 100644 docs/images/treeandlistapp.png create mode 100644 docs/images/treeandlistappwithdesclist.png create mode 100644 docs/internals.md create mode 100644 docs/kontact.svg create mode 100644 docs/server.md create mode 100644 docs/tags.md create mode 100644 icons/128-apps-akonadi.png create mode 100644 icons/16-apps-akonadi.png create mode 100644 icons/22-apps-akonadi.png create mode 100644 icons/256-apps-akonadi.png create mode 100644 icons/32-apps-akonadi.png create mode 100644 icons/48-apps-akonadi.png create mode 100644 icons/64-apps-akonadi.png create mode 100644 icons/CMakeLists.txt create mode 100644 icons/sc-apps-akonadi.svgz create mode 100644 logo.png create mode 100644 metainfo.yaml create mode 100644 metainfo.yaml.license create mode 100644 po/ar/akonadi_knut_resource.po create mode 100644 po/ar/libakonadi5.po create mode 100644 po/az/akonadi_knut_resource.po create mode 100644 po/az/libakonadi5.po create mode 100644 po/be/libakonadi5.po create mode 100644 po/bs/akonadi_knut_resource.po create mode 100644 po/bs/libakonadi5.po create mode 100644 po/ca/akonadi_knut_resource.po create mode 100644 po/ca/libakonadi5.po create mode 100644 po/ca@valencia/akonadi_knut_resource.po create mode 100644 po/ca@valencia/libakonadi5.po create mode 100644 po/cs/akonadi_knut_resource.po create mode 100644 po/cs/libakonadi5.po create mode 100644 po/da/akonadi_knut_resource.po create mode 100644 po/da/libakonadi5.po create mode 100644 po/de/akonadi_knut_resource.po create mode 100644 po/de/libakonadi5.po create mode 100644 po/el/akonadi_knut_resource.po create mode 100644 po/el/libakonadi5.po create mode 100644 po/en_GB/akonadi_knut_resource.po create mode 100644 po/en_GB/libakonadi5.po create mode 100644 po/eo/akonadi_knut_resource.po create mode 100644 po/eo/libakonadi5.po create mode 100644 po/es/akonadi_knut_resource.po create mode 100644 po/es/libakonadi5.po create mode 100644 po/et/akonadi_knut_resource.po create mode 100644 po/et/libakonadi5.po create mode 100644 po/eu/akonadi_knut_resource.po create mode 100644 po/eu/libakonadi5.po create mode 100644 po/fi/akonadi_knut_resource.po create mode 100644 po/fi/libakonadi5.po create mode 100644 po/fr/akonadi_knut_resource.po create mode 100644 po/fr/libakonadi5.po create mode 100644 po/ga/akonadi_knut_resource.po create mode 100644 po/ga/libakonadi5.po create mode 100644 po/gl/akonadi_knut_resource.po create mode 100644 po/gl/libakonadi5.po create mode 100644 po/hu/akonadi_knut_resource.po create mode 100644 po/hu/libakonadi5.po create mode 100644 po/ia/akonadi_knut_resource.po create mode 100644 po/ia/libakonadi5.po create mode 100644 po/it/akonadi_knut_resource.po create mode 100644 po/it/libakonadi5.po create mode 100644 po/ja/akonadi_knut_resource.po create mode 100644 po/ja/libakonadi5.po create mode 100644 po/kk/akonadi_knut_resource.po create mode 100644 po/kk/libakonadi5.po create mode 100644 po/km/akonadi_knut_resource.po create mode 100644 po/km/libakonadi5.po create mode 100644 po/ko/akonadi_knut_resource.po create mode 100644 po/ko/libakonadi5.po create mode 100644 po/lt/akonadi_knut_resource.po create mode 100644 po/lt/libakonadi5.po create mode 100644 po/lv/akonadi_knut_resource.po create mode 100644 po/lv/libakonadi5.po create mode 100644 po/mr/akonadi_knut_resource.po create mode 100644 po/mr/libakonadi5.po create mode 100644 po/nb/akonadi_knut_resource.po create mode 100644 po/nb/libakonadi5.po create mode 100644 po/nds/akonadi_knut_resource.po create mode 100644 po/nds/libakonadi5.po create mode 100644 po/nl/akonadi_knut_resource.po create mode 100644 po/nl/libakonadi5.po create mode 100644 po/nn/akonadi_knut_resource.po create mode 100644 po/nn/libakonadi5.po create mode 100644 po/pa/akonadi_knut_resource.po create mode 100644 po/pa/libakonadi5.po create mode 100644 po/pl/akonadi_knut_resource.po create mode 100644 po/pl/libakonadi5.po create mode 100644 po/pt/akonadi_knut_resource.po create mode 100644 po/pt/libakonadi5.po create mode 100644 po/pt_BR/akonadi_knut_resource.po create mode 100644 po/pt_BR/libakonadi5.po create mode 100644 po/ro/akonadi_knut_resource.po create mode 100644 po/ro/libakonadi5.po create mode 100644 po/ru/akonadi_knut_resource.po create mode 100644 po/ru/libakonadi5.po create mode 100644 po/se/libakonadi5.po create mode 100644 po/sk/akonadi_knut_resource.po create mode 100644 po/sk/libakonadi5.po create mode 100644 po/sl/akonadi_knut_resource.po create mode 100644 po/sl/libakonadi5.po create mode 100644 po/sq/akonadi_knut_resource.po create mode 100644 po/sr/akonadi_knut_resource.po create mode 100644 po/sr/libakonadi5.po create mode 100644 po/sv/akonadi_knut_resource.po create mode 100644 po/sv/libakonadi5.po create mode 100644 po/tr/akonadi_knut_resource.po create mode 100644 po/tr/libakonadi5.po create mode 100644 po/ug/akonadi_knut_resource.po create mode 100644 po/ug/libakonadi5.po create mode 100644 po/uk/akonadi_knut_resource.po create mode 100644 po/uk/libakonadi5.po create mode 100644 po/zh_CN/akonadi_knut_resource.po create mode 100644 po/zh_CN/libakonadi5.po create mode 100644 po/zh_TW/akonadi_knut_resource.po create mode 100644 po/zh_TW/libakonadi5.po create mode 100644 sanitizers.supp create mode 100644 src/.krazy create mode 100644 src/CMakeLists.txt create mode 100755 src/Messages.sh create mode 100644 src/agentbase/CMakeLists.txt create mode 100644 src/agentbase/accountsintegration.cpp create mode 100644 src/agentbase/accountsintegration.h create mode 100644 src/agentbase/agentbase.cpp create mode 100644 src/agentbase/agentbase.h create mode 100644 src/agentbase/agentbase_p.h create mode 100644 src/agentbase/agentfactory.cpp create mode 100644 src/agentbase/agentfactory.h create mode 100644 src/agentbase/agentsearchinterface.cpp create mode 100644 src/agentbase/agentsearchinterface.h create mode 100644 src/agentbase/agentsearchinterface_p.h create mode 100644 src/agentbase/preprocessorbase.cpp create mode 100644 src/agentbase/preprocessorbase.h create mode 100644 src/agentbase/preprocessorbase_p.cpp create mode 100644 src/agentbase/preprocessorbase_p.h create mode 100644 src/agentbase/recursivemover.cpp create mode 100644 src/agentbase/recursivemover_p.h create mode 100644 src/agentbase/resourcebase.cpp create mode 100644 src/agentbase/resourcebase.h create mode 100644 src/agentbase/resourcebase.kcfg create mode 100644 src/agentbase/resourcebasesettings.kcfgc create mode 100644 src/agentbase/resourcescheduler.cpp create mode 100644 src/agentbase/resourcescheduler_p.h create mode 100644 src/agentbase/resourcesettings.cpp create mode 100644 src/agentbase/resourcesettings.h create mode 100644 src/agentbase/transportresourcebase.cpp create mode 100644 src/agentbase/transportresourcebase.h create mode 100644 src/agentbase/transportresourcebase_p.h create mode 100644 src/agentserver/CMakeLists.txt create mode 100644 src/agentserver/TODO create mode 100644 src/agentserver/agentlauncher.cpp create mode 100644 src/agentserver/agentpluginloader.cpp create mode 100644 src/agentserver/agentpluginloader.h create mode 100644 src/agentserver/agentserver.cpp create mode 100644 src/agentserver/agentserver.h create mode 100644 src/agentserver/agentthread.cpp create mode 100644 src/agentserver/agentthread.h create mode 100644 src/agentserver/main.cpp create mode 100644 src/akonadicontrol/CMakeLists.txt create mode 100644 src/akonadicontrol/accountsintegration.cpp create mode 100644 src/akonadicontrol/accountsintegration.h create mode 100644 src/akonadicontrol/agentbrokeninstance.cpp create mode 100644 src/akonadicontrol/agentbrokeninstance.h create mode 100644 src/akonadicontrol/agentinstance.cpp create mode 100644 src/akonadicontrol/agentinstance.h create mode 100644 src/akonadicontrol/agentmanager.cpp create mode 100644 src/akonadicontrol/agentmanager.h create mode 100644 src/akonadicontrol/agentprocessinstance.cpp create mode 100644 src/akonadicontrol/agentprocessinstance.h create mode 100644 src/akonadicontrol/agentthreadinstance.cpp create mode 100644 src/akonadicontrol/agentthreadinstance.h create mode 100644 src/akonadicontrol/agenttype.cpp create mode 100644 src/akonadicontrol/agenttype.h create mode 100644 src/akonadicontrol/controlmanager.cpp create mode 100644 src/akonadicontrol/controlmanager.h create mode 100644 src/akonadicontrol/main.cpp create mode 100644 src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake create mode 100644 src/akonadicontrol/processcontrol.cpp create mode 100644 src/akonadicontrol/processcontrol.h create mode 100644 src/akonadictl/CMakeLists.txt create mode 100644 src/akonadictl/akonadistarter.cpp create mode 100644 src/akonadictl/akonadistarter.h create mode 100644 src/akonadictl/main.cpp create mode 100644 src/asapcat/CMakeLists.txt create mode 100644 src/asapcat/main.cpp create mode 100644 src/asapcat/session.cpp create mode 100644 src/asapcat/session.h create mode 100644 src/core/CMakeLists.txt create mode 100644 src/core/abstractdifferencesreporter.h create mode 100644 src/core/agentconfigurationbase.cpp create mode 100644 src/core/agentconfigurationbase.h create mode 100644 src/core/agentconfigurationfactorybase.cpp create mode 100644 src/core/agentconfigurationfactorybase.h create mode 100644 src/core/agentconfigurationmanager.cpp create mode 100644 src/core/agentconfigurationmanager_p.h create mode 100644 src/core/agentinstance.cpp create mode 100644 src/core/agentinstance.h create mode 100644 src/core/agentinstance_p.h create mode 100644 src/core/agentmanager.cpp create mode 100644 src/core/agentmanager.h create mode 100644 src/core/agentmanager_p.h create mode 100644 src/core/agenttype.cpp create mode 100644 src/core/agenttype.h create mode 100644 src/core/agenttype_p.h create mode 100644 src/core/akonaditests_export.h.in create mode 100644 src/core/asyncselectionhandler.cpp create mode 100644 src/core/asyncselectionhandler_p.h create mode 100644 src/core/attributefactory.cpp create mode 100644 src/core/attributefactory.h create mode 100644 src/core/attributes/attribute.cpp create mode 100644 src/core/attributes/attribute.h create mode 100644 src/core/attributes/collectioncolorattribute.cpp create mode 100644 src/core/attributes/collectioncolorattribute.h create mode 100644 src/core/attributes/collectionidentificationattribute.cpp create mode 100644 src/core/attributes/collectionidentificationattribute.h create mode 100644 src/core/attributes/collectionquotaattribute.cpp create mode 100644 src/core/attributes/collectionquotaattribute.h create mode 100644 src/core/attributes/collectionrightsattribute.cpp create mode 100644 src/core/attributes/collectionrightsattribute_p.h create mode 100644 src/core/attributes/entityannotationsattribute.cpp create mode 100644 src/core/attributes/entityannotationsattribute.h create mode 100644 src/core/attributes/entitydeletedattribute.cpp create mode 100644 src/core/attributes/entitydeletedattribute.h create mode 100644 src/core/attributes/entitydisplayattribute.cpp create mode 100644 src/core/attributes/entitydisplayattribute.h create mode 100644 src/core/attributes/entityhiddenattribute.cpp create mode 100644 src/core/attributes/entityhiddenattribute.h create mode 100644 src/core/attributes/favoritecollectionattribute.cpp create mode 100644 src/core/attributes/favoritecollectionattribute.h create mode 100644 src/core/attributes/indexpolicyattribute.cpp create mode 100644 src/core/attributes/indexpolicyattribute.h create mode 100644 src/core/attributes/persistentsearchattribute.cpp create mode 100644 src/core/attributes/persistentsearchattribute.h create mode 100644 src/core/attributes/specialcollectionattribute.cpp create mode 100644 src/core/attributes/specialcollectionattribute.h create mode 100644 src/core/attributes/tagattribute.cpp create mode 100644 src/core/attributes/tagattribute.h create mode 100644 src/core/attributestorage.cpp create mode 100644 src/core/attributestorage_p.h create mode 100644 src/core/braveheart.cpp create mode 100644 src/core/cachepolicy.cpp create mode 100644 src/core/cachepolicy.h create mode 100644 src/core/changemediator_p.cpp create mode 100644 src/core/changemediator_p.h create mode 100644 src/core/changenotification.cpp create mode 100644 src/core/changenotification.h create mode 100644 src/core/changenotificationdependenciesfactory.cpp create mode 100644 src/core/changenotificationdependenciesfactory_p.h create mode 100644 src/core/changerecorder.cpp create mode 100644 src/core/changerecorder.h create mode 100644 src/core/changerecorder_p.cpp create mode 100644 src/core/changerecorder_p.h create mode 100644 src/core/changerecorderjournal.cpp create mode 100644 src/core/changerecorderjournal_p.h create mode 100644 src/core/collection.cpp create mode 100644 src/core/collection.h create mode 100644 src/core/collection_p.h create mode 100644 src/core/collectionfetchscope.cpp create mode 100644 src/core/collectionfetchscope.h create mode 100644 src/core/collectionpathresolver.cpp create mode 100644 src/core/collectionpathresolver.h create mode 100644 src/core/collectionstatistics.cpp create mode 100644 src/core/collectionstatistics.h create mode 100644 src/core/collectionsync.cpp create mode 100644 src/core/collectionsync_p.h create mode 100644 src/core/collectionutils.h create mode 100644 src/core/commandbuffer_p.h create mode 100644 src/core/config.cpp create mode 100644 src/core/config_p.h create mode 100644 src/core/conflicthandler.cpp create mode 100644 src/core/conflicthandler_p.h create mode 100644 src/core/connection.cpp create mode 100644 src/core/connection_p.h create mode 100644 src/core/control.cpp create mode 100644 src/core/control.h create mode 100644 src/core/differencesalgorithminterface.h create mode 100644 src/core/entitycache.cpp create mode 100644 src/core/entitycache_p.h create mode 100644 src/core/exception.cpp create mode 100644 src/core/exceptionbase.h create mode 100644 src/core/firstrun.cpp create mode 100644 src/core/firstrun_p.h create mode 100644 src/core/gidextractor.cpp create mode 100644 src/core/gidextractor_p.h create mode 100644 src/core/gidextractorinterface.h create mode 100644 src/core/item.cpp create mode 100644 src/core/item.h create mode 100644 src/core/item_p.h create mode 100644 src/core/itemchangelog.cpp create mode 100644 src/core/itemchangelog_p.h create mode 100644 src/core/itemfetchscope.cpp create mode 100644 src/core/itemfetchscope.h create mode 100644 src/core/itemfetchscope_p.h create mode 100644 src/core/itemmonitor.cpp create mode 100644 src/core/itemmonitor.h create mode 100644 src/core/itemmonitor_p.h create mode 100644 src/core/itempayloadinternals_p.h create mode 100644 src/core/itemserializer.cpp create mode 100644 src/core/itemserializer_p.h create mode 100644 src/core/itemserializerplugin.cpp create mode 100644 src/core/itemserializerplugin.h create mode 100644 src/core/itemsync.cpp create mode 100644 src/core/itemsync.h create mode 100644 src/core/jobs/agentinstancecreatejob.cpp create mode 100644 src/core/jobs/agentinstancecreatejob.h create mode 100644 src/core/jobs/collectionattributessynchronizationjob.cpp create mode 100644 src/core/jobs/collectionattributessynchronizationjob.h create mode 100644 src/core/jobs/collectioncopyjob.cpp create mode 100644 src/core/jobs/collectioncopyjob.h create mode 100644 src/core/jobs/collectioncreatejob.cpp create mode 100644 src/core/jobs/collectioncreatejob.h create mode 100644 src/core/jobs/collectiondeletejob.cpp create mode 100644 src/core/jobs/collectiondeletejob.h create mode 100644 src/core/jobs/collectionfetchjob.cpp create mode 100644 src/core/jobs/collectionfetchjob.h create mode 100644 src/core/jobs/collectionmodifyjob.cpp create mode 100644 src/core/jobs/collectionmodifyjob.h create mode 100644 src/core/jobs/collectionmovejob.cpp create mode 100644 src/core/jobs/collectionmovejob.h create mode 100644 src/core/jobs/collectionstatisticsjob.cpp create mode 100644 src/core/jobs/collectionstatisticsjob.h create mode 100644 src/core/jobs/invalidatecachejob.cpp create mode 100644 src/core/jobs/invalidatecachejob_p.h create mode 100644 src/core/jobs/itemcopyjob.cpp create mode 100644 src/core/jobs/itemcopyjob.h create mode 100644 src/core/jobs/itemcreatejob.cpp create mode 100644 src/core/jobs/itemcreatejob.h create mode 100644 src/core/jobs/itemdeletejob.cpp create mode 100644 src/core/jobs/itemdeletejob.h create mode 100644 src/core/jobs/itemfetchjob.cpp create mode 100644 src/core/jobs/itemfetchjob.h create mode 100644 src/core/jobs/itemmodifyjob.cpp create mode 100644 src/core/jobs/itemmodifyjob.h create mode 100644 src/core/jobs/itemmodifyjob_p.h create mode 100644 src/core/jobs/itemmovejob.cpp create mode 100644 src/core/jobs/itemmovejob.h create mode 100644 src/core/jobs/itemsearchjob.cpp create mode 100644 src/core/jobs/itemsearchjob.h create mode 100644 src/core/jobs/job.cpp create mode 100644 src/core/jobs/job.h create mode 100644 src/core/jobs/job_p.h create mode 100644 src/core/jobs/kjobprivatebase.cpp create mode 100644 src/core/jobs/kjobprivatebase_p.h create mode 100644 src/core/jobs/linkjob.cpp create mode 100644 src/core/jobs/linkjob.h create mode 100644 src/core/jobs/linkjobimpl_p.h create mode 100644 src/core/jobs/recursiveitemfetchjob.cpp create mode 100644 src/core/jobs/recursiveitemfetchjob.h create mode 100644 src/core/jobs/relationcreatejob.cpp create mode 100644 src/core/jobs/relationcreatejob.h create mode 100644 src/core/jobs/relationdeletejob.cpp create mode 100644 src/core/jobs/relationdeletejob.h create mode 100644 src/core/jobs/relationfetchjob.cpp create mode 100644 src/core/jobs/relationfetchjob.h create mode 100644 src/core/jobs/resourceselectjob.cpp create mode 100644 src/core/jobs/resourceselectjob_p.h create mode 100644 src/core/jobs/resourcesynchronizationjob.cpp create mode 100644 src/core/jobs/resourcesynchronizationjob.h create mode 100644 src/core/jobs/searchcreatejob.cpp create mode 100644 src/core/jobs/searchcreatejob.h create mode 100644 src/core/jobs/searchresultjob.cpp create mode 100644 src/core/jobs/searchresultjob_p.h create mode 100644 src/core/jobs/specialcollectionsdiscoveryjob.cpp create mode 100644 src/core/jobs/specialcollectionsdiscoveryjob.h create mode 100644 src/core/jobs/specialcollectionshelperjobs.cpp create mode 100644 src/core/jobs/specialcollectionshelperjobs_p.h create mode 100644 src/core/jobs/specialcollectionsrequestjob.cpp create mode 100644 src/core/jobs/specialcollectionsrequestjob.h create mode 100644 src/core/jobs/subscriptionjob.cpp create mode 100644 src/core/jobs/subscriptionjob_p.h create mode 100644 src/core/jobs/tagcreatejob.cpp create mode 100644 src/core/jobs/tagcreatejob.h create mode 100644 src/core/jobs/tagdeletejob.cpp create mode 100644 src/core/jobs/tagdeletejob.h create mode 100644 src/core/jobs/tagfetchjob.cpp create mode 100644 src/core/jobs/tagfetchjob.h create mode 100644 src/core/jobs/tagmodifyjob.cpp create mode 100644 src/core/jobs/tagmodifyjob.h create mode 100644 src/core/jobs/transactionjobs.cpp create mode 100644 src/core/jobs/transactionjobs.h create mode 100644 src/core/jobs/transactionsequence.cpp create mode 100644 src/core/jobs/transactionsequence.h create mode 100644 src/core/jobs/trashjob.cpp create mode 100644 src/core/jobs/trashjob.h create mode 100644 src/core/jobs/trashrestorejob.cpp create mode 100644 src/core/jobs/trashrestorejob.h create mode 100644 src/core/jobs/unlinkjob.cpp create mode 100644 src/core/jobs/unlinkjob.h create mode 100644 src/core/kcfg2dbus.xsl create mode 100644 src/core/mimetypechecker.cpp create mode 100644 src/core/mimetypechecker.h create mode 100644 src/core/mimetypechecker_p.h create mode 100644 src/core/models/agentfilterproxymodel.cpp create mode 100644 src/core/models/agentfilterproxymodel.h create mode 100644 src/core/models/agentinstancemodel.cpp create mode 100644 src/core/models/agentinstancemodel.h create mode 100644 src/core/models/agenttypemodel.cpp create mode 100644 src/core/models/agenttypemodel.h create mode 100644 src/core/models/collectionfilterproxymodel.cpp create mode 100644 src/core/models/collectionfilterproxymodel.h create mode 100644 src/core/models/entitymimetypefiltermodel.cpp create mode 100644 src/core/models/entitymimetypefiltermodel.h create mode 100644 src/core/models/entityorderproxymodel.cpp create mode 100644 src/core/models/entityorderproxymodel.h create mode 100644 src/core/models/entityrightsfiltermodel.cpp create mode 100644 src/core/models/entityrightsfiltermodel.h create mode 100644 src/core/models/entitytreemodel.cpp create mode 100644 src/core/models/entitytreemodel.h create mode 100644 src/core/models/entitytreemodel_p.cpp create mode 100644 src/core/models/entitytreemodel_p.h create mode 100644 src/core/models/favoritecollectionsmodel.cpp create mode 100644 src/core/models/favoritecollectionsmodel.h create mode 100644 src/core/models/recursivecollectionfilterproxymodel.cpp create mode 100644 src/core/models/recursivecollectionfilterproxymodel.h create mode 100644 src/core/models/selectionproxymodel.cpp create mode 100644 src/core/models/selectionproxymodel.h create mode 100644 src/core/models/statisticsproxymodel.cpp create mode 100644 src/core/models/statisticsproxymodel.h create mode 100644 src/core/models/subscriptionmodel.cpp create mode 100644 src/core/models/subscriptionmodel_p.h create mode 100644 src/core/models/tagmodel.cpp create mode 100644 src/core/models/tagmodel.h create mode 100644 src/core/models/tagmodel_p.cpp create mode 100644 src/core/models/tagmodel_p.h create mode 100644 src/core/models/trashfilterproxymodel.cpp create mode 100644 src/core/models/trashfilterproxymodel.h create mode 100644 src/core/monitor.cpp create mode 100644 src/core/monitor.h create mode 100644 src/core/monitor_p.cpp create mode 100644 src/core/monitor_p.h create mode 100644 src/core/notificationsource_p.cpp create mode 100644 src/core/notificationsource_p.h create mode 100644 src/core/notificationsubscriber.cpp create mode 100644 src/core/notificationsubscriber.h create mode 100644 src/core/partfetcher.cpp create mode 100644 src/core/partfetcher.h create mode 100644 src/core/pastehelper.cpp create mode 100644 src/core/pastehelper_p.h create mode 100644 src/core/pluginloader.cpp create mode 100644 src/core/pluginloader_p.h create mode 100644 src/core/protocolhelper.cpp create mode 100644 src/core/protocolhelper_p.h create mode 100644 src/core/qtest_akonadi.h create mode 100644 src/core/relation.cpp create mode 100644 src/core/relation.h create mode 100644 src/core/relationsync.cpp create mode 100644 src/core/relationsync.h create mode 100644 src/core/remotelog.cpp create mode 100644 src/core/searchquery.cpp create mode 100644 src/core/searchquery.h create mode 100644 src/core/servermanager.cpp create mode 100644 src/core/servermanager.h create mode 100644 src/core/servermanager_p.h create mode 100644 src/core/session.cpp create mode 100644 src/core/session.h create mode 100644 src/core/session_p.h create mode 100644 src/core/sessionthread.cpp create mode 100644 src/core/sessionthread_p.h create mode 100644 src/core/sharedvaluepool_p.h create mode 100644 src/core/specialcollections.cpp create mode 100644 src/core/specialcollections.h create mode 100644 src/core/specialcollections_p.h create mode 100644 src/core/supertrait.h create mode 100644 src/core/tag.cpp create mode 100644 src/core/tag.h create mode 100644 src/core/tag_p.h create mode 100644 src/core/tagfetchscope.cpp create mode 100644 src/core/tagfetchscope.h create mode 100644 src/core/tagsync.cpp create mode 100644 src/core/tagsync.h create mode 100644 src/core/trashsettings.cpp create mode 100644 src/core/trashsettings.h create mode 100644 src/core/typepluginloader.cpp create mode 100644 src/core/typepluginloader_p.h create mode 100644 src/interfaces/CMakeLists.txt create mode 100644 src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.AgentManager.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.AgentServer.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.ControlManager.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Janitor.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Resource.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Resource2.Task.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Resource2.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.SearchManager.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Server.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.Tracer.xml create mode 100644 src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml create mode 100644 src/interfaces/org.kde.Akonadi.Accounts.xml create mode 100644 src/private/CMakeLists.txt create mode 100644 src/private/capabilities_p.h create mode 100644 src/private/compressionstream.cpp create mode 100644 src/private/compressionstream_p.h create mode 100644 src/private/datastream_p.cpp create mode 100644 src/private/datastream_p_p.h create mode 100644 src/private/dbus.cpp create mode 100644 src/private/dbus_p.h create mode 100644 src/private/externalpartstorage.cpp create mode 100644 src/private/externalpartstorage_p.h create mode 100644 src/private/imapparser.cpp create mode 100644 src/private/imapparser_p.h create mode 100644 src/private/imapset.cpp create mode 100644 src/private/imapset_p.h create mode 100644 src/private/instance.cpp create mode 100644 src/private/instance_p.h create mode 100644 src/private/protocol.cpp create mode 100644 src/private/protocol.xml create mode 100644 src/private/protocol_exception_p.h create mode 100644 src/private/protocol_p.h create mode 100644 src/private/protocolgen/CMakeLists.txt create mode 100644 src/private/protocolgen/cppgenerator.cpp create mode 100644 src/private/protocolgen/cppgenerator.h create mode 100644 src/private/protocolgen/cpphelper.cpp create mode 100644 src/private/protocolgen/cpphelper.h create mode 100644 src/private/protocolgen/main.cpp create mode 100644 src/private/protocolgen/nodetree.cpp create mode 100644 src/private/protocolgen/nodetree.h create mode 100644 src/private/protocolgen/typehelper.cpp create mode 100644 src/private/protocolgen/typehelper.h create mode 100644 src/private/protocolgen/xmlparser.cpp create mode 100644 src/private/protocolgen/xmlparser.h create mode 100644 src/private/scope.cpp create mode 100644 src/private/scope_p.h create mode 100644 src/private/standarddirs.cpp create mode 100644 src/private/standarddirs_p.h create mode 100644 src/private/tristate.cpp create mode 100644 src/private/tristate_p.h create mode 100644 src/qsqlite/.no_coding_style create mode 100644 src/qsqlite/CMakeLists.txt create mode 100644 src/qsqlite/README create mode 100644 src/qsqlite/src/qsql_sqlite.cpp create mode 100644 src/qsqlite/src/qsql_sqlite.h create mode 100644 src/qsqlite/src/smain.cpp create mode 100644 src/qsqlite/src/sqlite3.json create mode 100644 src/qsqlite/src/sqlite_blocking.cpp create mode 100644 src/qsqlite/src/sqlite_blocking.h create mode 100644 src/rds/CMakeLists.txt create mode 100644 src/rds/bridgeconnection.cpp create mode 100644 src/rds/bridgeconnection.h create mode 100644 src/rds/bridgeserver.cpp create mode 100644 src/rds/bridgeserver.h create mode 100644 src/rds/exception.h create mode 100644 src/rds/main.cpp create mode 100644 src/selftest/CMakeLists.txt create mode 100644 src/selftest/main.cpp create mode 100644 src/server/CMakeLists.txt create mode 100644 src/server/aggregatedfetchscope.cpp create mode 100644 src/server/aggregatedfetchscope.h create mode 100644 src/server/aklocalserver.cpp create mode 100644 src/server/aklocalserver.h create mode 100644 src/server/akonadi.cpp create mode 100644 src/server/akonadi.h create mode 100644 src/server/akthread.cpp create mode 100644 src/server/akthread.h create mode 100644 src/server/cachecleaner.cpp create mode 100644 src/server/cachecleaner.h create mode 100644 src/server/collectionscheduler.cpp create mode 100644 src/server/collectionscheduler.h create mode 100644 src/server/commandcontext.cpp create mode 100644 src/server/commandcontext.h create mode 100644 src/server/connection.cpp create mode 100644 src/server/connection.h create mode 100644 src/server/dbustracer.cpp create mode 100644 src/server/dbustracer.h create mode 100644 src/server/debuginterface.cpp create mode 100644 src/server/debuginterface.h create mode 100644 src/server/exception.h create mode 100644 src/server/filetracer.cpp create mode 100644 src/server/filetracer.h create mode 100644 src/server/global.h create mode 100644 src/server/handler.cpp create mode 100644 src/server/handler.h create mode 100644 src/server/handler/collectioncopyhandler.cpp create mode 100644 src/server/handler/collectioncopyhandler.h create mode 100644 src/server/handler/collectioncreatehandler.cpp create mode 100644 src/server/handler/collectioncreatehandler.h create mode 100644 src/server/handler/collectiondeletehandler.cpp create mode 100644 src/server/handler/collectiondeletehandler.h create mode 100644 src/server/handler/collectionfetchhandler.cpp create mode 100644 src/server/handler/collectionfetchhandler.h create mode 100644 src/server/handler/collectionmodifyhandler.cpp create mode 100644 src/server/handler/collectionmodifyhandler.h create mode 100644 src/server/handler/collectionmovehandler.cpp create mode 100644 src/server/handler/collectionmovehandler.h create mode 100644 src/server/handler/collectionstatsfetchhandler.cpp create mode 100644 src/server/handler/collectionstatsfetchhandler.h create mode 100644 src/server/handler/itemcopyhandler.cpp create mode 100644 src/server/handler/itemcopyhandler.h create mode 100644 src/server/handler/itemcreatehandler.cpp create mode 100644 src/server/handler/itemcreatehandler.h create mode 100644 src/server/handler/itemdeletehandler.cpp create mode 100644 src/server/handler/itemdeletehandler.h create mode 100644 src/server/handler/itemfetchhandler.cpp create mode 100644 src/server/handler/itemfetchhandler.h create mode 100644 src/server/handler/itemfetchhelper.cpp create mode 100644 src/server/handler/itemfetchhelper.h create mode 100644 src/server/handler/itemlinkhandler.cpp create mode 100644 src/server/handler/itemlinkhandler.h create mode 100644 src/server/handler/itemmodifyhandler.cpp create mode 100644 src/server/handler/itemmodifyhandler.h create mode 100644 src/server/handler/itemmovehandler.cpp create mode 100644 src/server/handler/itemmovehandler.h create mode 100644 src/server/handler/loginhandler.cpp create mode 100644 src/server/handler/loginhandler.h create mode 100644 src/server/handler/logouthandler.cpp create mode 100644 src/server/handler/logouthandler.h create mode 100644 src/server/handler/relationfetchhandler.cpp create mode 100644 src/server/handler/relationfetchhandler.h create mode 100644 src/server/handler/relationmodifyhandler.cpp create mode 100644 src/server/handler/relationmodifyhandler.h create mode 100644 src/server/handler/relationremovehandler.cpp create mode 100644 src/server/handler/relationremovehandler.h create mode 100644 src/server/handler/resourceselecthandler.cpp create mode 100644 src/server/handler/resourceselecthandler.h create mode 100644 src/server/handler/searchcreatehandler.cpp create mode 100644 src/server/handler/searchcreatehandler.h create mode 100644 src/server/handler/searchhandler.cpp create mode 100644 src/server/handler/searchhandler.h create mode 100644 src/server/handler/searchhelper.cpp create mode 100644 src/server/handler/searchhelper.h create mode 100644 src/server/handler/searchresulthandler.cpp create mode 100644 src/server/handler/searchresulthandler.h create mode 100644 src/server/handler/tagcreatehandler.cpp create mode 100644 src/server/handler/tagcreatehandler.h create mode 100644 src/server/handler/tagdeletehandler.cpp create mode 100644 src/server/handler/tagdeletehandler.h create mode 100644 src/server/handler/tagfetchhandler.cpp create mode 100644 src/server/handler/tagfetchhandler.h create mode 100644 src/server/handler/tagfetchhelper.cpp create mode 100644 src/server/handler/tagfetchhelper.h create mode 100644 src/server/handler/tagmodifyhandler.cpp create mode 100644 src/server/handler/tagmodifyhandler.h create mode 100644 src/server/handler/transactionhandler.cpp create mode 100644 src/server/handler/transactionhandler.h create mode 100644 src/server/handlerhelper.cpp create mode 100644 src/server/handlerhelper.h create mode 100644 src/server/intervalcheck.cpp create mode 100644 src/server/intervalcheck.h create mode 100644 src/server/main.cpp create mode 100644 src/server/notificationmanager.cpp create mode 100644 src/server/notificationmanager.h create mode 100644 src/server/notificationsubscriber.cpp create mode 100644 src/server/notificationsubscriber.h create mode 100644 src/server/preprocessorinstance.cpp create mode 100644 src/server/preprocessorinstance.h create mode 100644 src/server/preprocessormanager.cpp create mode 100644 src/server/preprocessormanager.h create mode 100644 src/server/resourcemanager.cpp create mode 100644 src/server/resourcemanager.h create mode 100644 src/server/search/abstractsearchengine.h create mode 100644 src/server/search/abstractsearchplugin.h create mode 100644 src/server/search/agentsearchengine.cpp create mode 100644 src/server/search/agentsearchengine.h create mode 100644 src/server/search/agentsearchinstance.cpp create mode 100644 src/server/search/agentsearchinstance.h create mode 100644 src/server/search/searchmanager.cpp create mode 100644 src/server/search/searchmanager.h create mode 100644 src/server/search/searchrequest.cpp create mode 100644 src/server/search/searchrequest.h create mode 100644 src/server/search/searchtaskmanager.cpp create mode 100644 src/server/search/searchtaskmanager.h create mode 100755 src/server/storage/akonadi-mysql-client.sh create mode 100755 src/server/storage/akonadi-mysql-server.sh create mode 100644 src/server/storage/akonadidb.qrc create mode 100644 src/server/storage/akonadidb.xml create mode 100644 src/server/storage/akonadidb.xsd create mode 100644 src/server/storage/collectionqueryhelper.cpp create mode 100644 src/server/storage/collectionqueryhelper.h create mode 100644 src/server/storage/collectionstatistics.cpp create mode 100644 src/server/storage/collectionstatistics.h create mode 100644 src/server/storage/collectiontreecache.cpp create mode 100644 src/server/storage/collectiontreecache.h create mode 100644 src/server/storage/countquerybuilder.h create mode 100644 src/server/storage/datastore.cpp create mode 100644 src/server/storage/datastore.h create mode 100644 src/server/storage/dbconfig.cpp create mode 100644 src/server/storage/dbconfig.h create mode 100644 src/server/storage/dbconfigmysql.cpp create mode 100644 src/server/storage/dbconfigmysql.h create mode 100644 src/server/storage/dbconfigpostgresql.cpp create mode 100644 src/server/storage/dbconfigpostgresql.h create mode 100644 src/server/storage/dbconfigsqlite.cpp create mode 100644 src/server/storage/dbconfigsqlite.h create mode 100644 src/server/storage/dbdeadlockcatcher.h create mode 100644 src/server/storage/dbexception.cpp create mode 100644 src/server/storage/dbexception.h create mode 100644 src/server/storage/dbinitializer.cpp create mode 100644 src/server/storage/dbinitializer.h create mode 100644 src/server/storage/dbinitializer_p.cpp create mode 100644 src/server/storage/dbinitializer_p.h create mode 100644 src/server/storage/dbintrospector.cpp create mode 100644 src/server/storage/dbintrospector.h create mode 100644 src/server/storage/dbintrospector_impl.cpp create mode 100644 src/server/storage/dbintrospector_impl.h create mode 100644 src/server/storage/dbtype.cpp create mode 100644 src/server/storage/dbtype.h create mode 100644 src/server/storage/dbupdate.xml create mode 100644 src/server/storage/dbupdate.xsd create mode 100644 src/server/storage/dbupdater.cpp create mode 100644 src/server/storage/dbupdater.h create mode 100755 src/server/storage/doxygen-preprocess-entities.sh create mode 100644 src/server/storage/entities-dox.xsl create mode 100644 src/server/storage/entities-header.xsl create mode 100644 src/server/storage/entities-source.xsl create mode 100644 src/server/storage/entities.xsl create mode 100644 src/server/storage/entity.cpp create mode 100644 src/server/storage/entity.h create mode 100644 src/server/storage/itemqueryhelper.cpp create mode 100644 src/server/storage/itemqueryhelper.h create mode 100644 src/server/storage/itemretrievaljob.cpp create mode 100644 src/server/storage/itemretrievaljob.h create mode 100644 src/server/storage/itemretrievalmanager.cpp create mode 100644 src/server/storage/itemretrievalmanager.h create mode 100644 src/server/storage/itemretrievalrequest.cpp create mode 100644 src/server/storage/itemretrievalrequest.h create mode 100644 src/server/storage/itemretriever.cpp create mode 100644 src/server/storage/itemretriever.h create mode 100644 src/server/storage/mysql-global-mobile.conf create mode 100644 src/server/storage/mysql-global.conf create mode 100644 src/server/storage/notificationcollector.cpp create mode 100644 src/server/storage/notificationcollector.h create mode 100644 src/server/storage/parthelper.cpp create mode 100644 src/server/storage/parthelper.h create mode 100644 src/server/storage/partstreamer.cpp create mode 100644 src/server/storage/partstreamer.h create mode 100644 src/server/storage/parttypehelper.cpp create mode 100644 src/server/storage/parttypehelper.h create mode 100644 src/server/storage/query.cpp create mode 100644 src/server/storage/query.h create mode 100644 src/server/storage/querybuilder.cpp create mode 100644 src/server/storage/querybuilder.h create mode 100644 src/server/storage/querycache.cpp create mode 100644 src/server/storage/querycache.h create mode 100644 src/server/storage/queryhelper.cpp create mode 100644 src/server/storage/queryhelper.h create mode 100644 src/server/storage/schema-header.xsl create mode 100644 src/server/storage/schema-source.xsl create mode 100644 src/server/storage/schema.h create mode 100644 src/server/storage/schema.xsl create mode 100644 src/server/storage/schematypes.cpp create mode 100644 src/server/storage/schematypes.h create mode 100644 src/server/storage/selectquerybuilder.h create mode 100644 src/server/storage/storagedebugger.cpp create mode 100644 src/server/storage/storagedebugger.h create mode 100644 src/server/storage/tagqueryhelper.cpp create mode 100644 src/server/storage/tagqueryhelper.h create mode 100644 src/server/storage/transaction.cpp create mode 100644 src/server/storage/transaction.h create mode 100644 src/server/storagejanitor.cpp create mode 100644 src/server/storagejanitor.h create mode 100644 src/server/tracer.cpp create mode 100644 src/server/tracer.h create mode 100644 src/server/tracerinterface.h create mode 100644 src/server/utils.cpp create mode 100644 src/server/utils.h create mode 100644 src/shared/CMakeLists.txt create mode 100644 src/shared/akapplication.cpp create mode 100644 src/shared/akapplication.h create mode 100644 src/shared/akdebug.cpp create mode 100644 src/shared/akdebug.h create mode 100644 src/shared/akhelpers.h create mode 100644 src/shared/akqt.h create mode 100644 src/shared/akranges.h create mode 100644 src/shared/akremotelog.cpp create mode 100644 src/shared/akremotelog.h create mode 100644 src/shared/akscopeguard.h create mode 100644 src/shared/akstd.h create mode 100644 src/shared/aktest.h create mode 100644 src/shared/aktraits.h create mode 100644 src/shared/vectorhelper.h create mode 100644 src/widgets/CMakeLists.txt create mode 100644 src/widgets/actionstatemanager.cpp create mode 100644 src/widgets/actionstatemanager_p.h create mode 100644 src/widgets/agentactionmanager.cpp create mode 100644 src/widgets/agentactionmanager.h create mode 100644 src/widgets/agentconfigurationdialog.cpp create mode 100644 src/widgets/agentconfigurationdialog.h create mode 100644 src/widgets/agentconfigurationwidget.cpp create mode 100644 src/widgets/agentconfigurationwidget.h create mode 100644 src/widgets/agentconfigurationwidget_p.h create mode 100644 src/widgets/agentinstancewidget.cpp create mode 100644 src/widgets/agentinstancewidget.h create mode 100644 src/widgets/agenttypedialog.cpp create mode 100644 src/widgets/agenttypedialog.h create mode 100644 src/widgets/agenttypewidget.cpp create mode 100644 src/widgets/agenttypewidget.h create mode 100644 src/widgets/akonadiwidgetstests_export.h.in create mode 100644 src/widgets/cachepolicypage.cpp create mode 100644 src/widgets/cachepolicypage.h create mode 100644 src/widgets/cachepolicypage.ui create mode 100644 src/widgets/collectioncombobox.cpp create mode 100644 src/widgets/collectioncombobox.h create mode 100644 src/widgets/collectiondialog.cpp create mode 100644 src/widgets/collectiondialog.h create mode 100644 src/widgets/collectiongeneralpropertiespage.cpp create mode 100644 src/widgets/collectiongeneralpropertiespage.ui create mode 100644 src/widgets/collectiongeneralpropertiespage_p.h create mode 100644 src/widgets/collectionmaintenancepage.cpp create mode 100644 src/widgets/collectionmaintenancepage.h create mode 100644 src/widgets/collectionmaintenancepage.ui create mode 100644 src/widgets/collectionpropertiesdialog.cpp create mode 100644 src/widgets/collectionpropertiesdialog.h create mode 100644 src/widgets/collectionpropertiespage.cpp create mode 100644 src/widgets/collectionpropertiespage.h create mode 100644 src/widgets/collectionrequester.cpp create mode 100644 src/widgets/collectionrequester.h create mode 100644 src/widgets/collectionstatisticsdelegate.cpp create mode 100644 src/widgets/collectionstatisticsdelegate.h create mode 100644 src/widgets/collectionview.cpp create mode 100644 src/widgets/collectionview.h create mode 100644 src/widgets/conflictresolvedialog.cpp create mode 100644 src/widgets/conflictresolvedialog_p.h create mode 100644 src/widgets/controlgui.cpp create mode 100644 src/widgets/controlgui.h create mode 100644 src/widgets/controlprogressindicator.ui create mode 100644 src/widgets/dragdropmanager.cpp create mode 100644 src/widgets/dragdropmanager_p.h create mode 100644 src/widgets/entitylistview.cpp create mode 100644 src/widgets/entitylistview.h create mode 100644 src/widgets/entitytreeview.cpp create mode 100644 src/widgets/entitytreeview.h create mode 100644 src/widgets/erroroverlay.cpp create mode 100644 src/widgets/erroroverlay.ui create mode 100644 src/widgets/erroroverlay_p.h create mode 100644 src/widgets/etmviewstatesaver.cpp create mode 100644 src/widgets/etmviewstatesaver.h create mode 100644 src/widgets/itemview.cpp create mode 100644 src/widgets/itemview.h create mode 100644 src/widgets/manageaccountwidget.cpp create mode 100644 src/widgets/manageaccountwidget.h create mode 100644 src/widgets/manageaccountwidget.ui create mode 100644 src/widgets/progressspinnerdelegate.cpp create mode 100644 src/widgets/progressspinnerdelegate_p.h create mode 100644 src/widgets/recentcollectionaction.cpp create mode 100644 src/widgets/recentcollectionaction_p.h create mode 100644 src/widgets/renamefavoritedialog.cpp create mode 100644 src/widgets/renamefavoritedialog.ui create mode 100644 src/widgets/renamefavoritedialog_p.h create mode 100644 src/widgets/selftestdialog.cpp create mode 100644 src/widgets/selftestdialog.h create mode 100644 src/widgets/selftestdialog.ui create mode 100644 src/widgets/standardactionmanager.cpp create mode 100644 src/widgets/standardactionmanager.h create mode 100644 src/widgets/subscriptiondialog.cpp create mode 100644 src/widgets/subscriptiondialog.h create mode 100644 src/widgets/subscriptiondialog.ui create mode 100644 src/widgets/tageditwidget.cpp create mode 100644 src/widgets/tageditwidget.h create mode 100644 src/widgets/tageditwidget.ui create mode 100644 src/widgets/tagmanagementdialog.cpp create mode 100644 src/widgets/tagmanagementdialog.h create mode 100644 src/widgets/tagmanagementdialog.ui create mode 100644 src/widgets/tagselectioncombobox.cpp create mode 100644 src/widgets/tagselectioncombobox.h create mode 100644 src/widgets/tagselectiondialog.cpp create mode 100644 src/widgets/tagselectiondialog.h create mode 100644 src/widgets/tagselectiondialog.ui create mode 100644 src/widgets/tagselectwidget.cpp create mode 100644 src/widgets/tagselectwidget.h create mode 100644 src/widgets/tagwidget.cpp create mode 100644 src/widgets/tagwidget.h create mode 100644 src/widgets/tagwidget.ui create mode 100644 src/xml/CMakeLists.txt create mode 100644 src/xml/akonadi-xml.xsd create mode 100644 src/xml/akonadi2xml.cpp create mode 100644 src/xml/autotests/CMakeLists.txt create mode 100644 src/xml/autotests/collectiontest.cpp create mode 100644 src/xml/autotests/collectiontest.h create mode 100644 src/xml/autotests/knutdemo.xml create mode 100644 src/xml/autotests/xmldocumenttest.cpp create mode 100644 src/xml/format_p.h create mode 100644 src/xml/xmldocument.cpp create mode 100644 src/xml/xmldocument.h create mode 100644 src/xml/xmlreader.cpp create mode 100644 src/xml/xmlreader.h create mode 100644 src/xml/xmlwritejob.cpp create mode 100644 src/xml/xmlwritejob.h create mode 100644 src/xml/xmlwriter.cpp create mode 100644 src/xml/xmlwriter.h create mode 100644 templates/.clang-format create mode 100644 templates/CMakeLists.txt create mode 100644 templates/akonadiresource/CMakeLists.txt create mode 100644 templates/akonadiresource/README create mode 100644 templates/akonadiresource/akonadiresource.kdevtemplate create mode 100644 templates/akonadiresource/akonadiresource.png create mode 100644 templates/akonadiresource/src/%{APPNAMELC}resource.cpp create mode 100644 templates/akonadiresource/src/%{APPNAMELC}resource.desktop create mode 100644 templates/akonadiresource/src/%{APPNAMELC}resource.h create mode 100644 templates/akonadiresource/src/CMakeLists.txt create mode 100644 templates/akonadiresource/src/settings.kcfg create mode 100644 templates/akonadiresource/src/settings.kcfgc create mode 100644 templates/akonadiserializer/CMakeLists.txt create mode 100644 templates/akonadiserializer/README create mode 100644 templates/akonadiserializer/akonadiserializer.kdevtemplate create mode 100644 templates/akonadiserializer/akonadiserializer.png create mode 100644 templates/akonadiserializer/src/CMakeLists.txt create mode 100644 templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.cpp create mode 100644 templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.desktop create mode 100644 templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.h create mode 100644 tests/CMakeLists.txt create mode 100644 tests/asapcat/imap-4.10-sync.asap create mode 100644 tests/asapcat/imap-4.11-body-check.asap create mode 100644 tests/asapcat/kmail-4.10-folder-listing.asap create mode 100644 tests/asapcat/kmail-4.11-folder-listing.asap create mode 100644 tests/asapcat/kmail-4.12-folder-listing.asap create mode 100644 tests/libs/CMakeLists.txt create mode 100644 tests/libs/agentinstancewidgettest.cpp create mode 100644 tests/libs/agentinstancewidgettest.h create mode 100644 tests/libs/agenttypewidgettest.cpp create mode 100644 tests/libs/agenttypewidgettest.h create mode 100644 tests/libs/collectiondialog.cpp create mode 100644 tests/libs/conflictresolvedialogtest_gui.cpp create mode 100644 tests/libs/etm_test_app/CMakeLists.txt create mode 100644 tests/libs/etm_test_app/main.cpp create mode 100644 tests/libs/etm_test_app/mainwindow.cpp create mode 100644 tests/libs/etm_test_app/mainwindow.h create mode 100644 tests/libs/itemdumper.cpp create mode 100644 tests/libs/itemdumper.h create mode 100644 tests/libs/pluginloadertest.cpp create mode 100644 tests/libs/selftester.cpp create mode 100644 tests/libs/subscriber.cpp create mode 100755 tools/clang-tidy-to-junit.py create mode 100755 tools/run-clang-tidy.sh diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..09e58bd --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,61 @@ +# clang-analyzer-cplusplus.NewDeleteLeaks triggers false-positives in QObject::connect() +# readability-redundant-access-specifiers triggered by Q_SLOTS +# readability-inconsistent-declaration-parameter-name trigered by generated Qt code +# performance-no-automatic-move is triggered by constness of qstring_literal_tmp in QStringLiteral macro +# +# TODO make those pass: +# readability-function-size - for now some tests and the generated code contains extremely long +# functions, which should be split into smaller functions + +Checks: -*, + bugprone-*, + clang-analyzer-*, + -clang-analyzer-osx, + -clang-analyzer-cplusplus.NewDeleteLeaks, + google-*, + -google-build-using-namespace, + -google-readability-todo, + -google-runtime-references, + -google-readability-function-size, + -google-default-arguments, + misc-*, + -misc-definitions-in-headers, + -*-non-private-member-variables-in-classes, + performance-*, + -performance-no-automatic-move, + readability-*, + -readability-avoid-const-params-in-decls, + -readability-convert-member-functions-to-static, + -readability-else-after-return, + -readability-redundant-access-specifiers, + -readability-implicit-bool-conversion, + -readability-static-accessed-through-instance, + -readability-inconsistent-declaration-parameter-name, + -readability-magic-numbers, + -readability-make-member-function-const, + -readability-function-size +AnalyzeTemporaryDtors: true +CheckOptions: + - key: bugprone-assert-side-effects.AssertMacros + value: 'Q_ASSERT;QVERIFY;QCOMPARE;AKVERIFY' + - key: CheckFunctionCalls + value: true + - key: StringCompareLikeFuctions + value: 'QString::compare;QString::localeAwareCompare' + - key: WarnOnSizeOfIntegerExpression + value: 1 + - key: bugprone-dangling-handle.HandleClasses + value: 'std::string_view;QStringView' + - key: IgnoreClassesWithAllMemberVariablesBeingPublic + value: true + - key: VectorLikeClasses + value: 'std::vector;QVector' +WarningsAsErrors: bugprone-*, + clang-*, + google-*, + misc-*, + performance-*, + readability-*, + -readability-magic-numbers, + -readability-make-member-function-const + diff --git a/.clang-tidy-ignore b/.clang-tidy-ignore new file mode 100644 index 0000000..662d4a4 --- /dev/null +++ b/.clang-tidy-ignore @@ -0,0 +1,13 @@ +# Ignore generated files that we can't do anything about (no major issues there, though) +resourcebasesettings.cpp +designerplugin.cpp + +# Ignore generated Q_LOGGING_CATEGORY files, the checks take forever for some reason +# and are generated anyway. +[a-z_]+_debug.cpp +mocs_[a-z_]+.cpp + +# Ignore autotests and tests, it halves the run time +autotests/ +tests/ + diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..54c0daa --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# clang-format +0f667cad762e7e6bcdbf5cc81af9cf6b1ff510f1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c81125 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Ignore the following files +*~ +*.[oa] +*.diff +*.kate-swp +*.kdev4 +.kdev_include_paths +*.kdevelop.pcs +*.moc +*.moc.cpp +*.orig +*.user +.*.swp +.swp.* +Doxyfile +Makefile +/build*/ +.cmake/ +CMakeLists.txt.user* +*.unc-backup* +compile_commands.json +.clang-format +.clangd +.idea +/cmake-build* +.cache diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..6bb7a68 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,35 @@ +include: + - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-before.yml + - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-applications-linux.yml + +build_clazy_clang_tidy: + stage: build + image: "kdeorg/ci-suse-qt515" + extends: .linux + only: + - merge_requests + before_script: + - zypper install -y clazy jq + - git clone --depth=1 https://invent.kde.org/sysadmin/ci-tooling.git $CI_TOOLING + - git clone --depth=1 https://invent.kde.org/sysadmin/repo-metadata.git $CI_TOOLING/repo-metadata + - git clone --depth=1 https://invent.kde.org/sysadmin/kde-build-metadata.git $CI_TOOLING/kde-build-metadata + - git clone --depth=1 https://invent.kde.org/sdk/kde-dev-scripts.git $CI_TOOLING/kde-dev-scripts + + script: + - export CXX=clazy + - export CXXFLAGS="-Werror -Wno-deprecated-declarations -Wno-deprecated-copy" + - export CLAZY_CHECKS="level0,level1,level2,no-ctor-missing-parent-argument,isempty-vs-count,qhash-with-char-pointer-key,raw-environment-function,qproperty-type-mismatch" + - export CLAZY_HEADER_FILTER='^(?!ui_)\w+.h\$' + - export CLAZY_IGNORE_DIRS="autotests" + - python3 -u $CI_TOOLING/helpers/prepare-dependencies.py --product $PRODUCT --project $PROJECT --branchGroup $BRANCH_GROUP --environment production --platform $PLATFORM --installTo $INSTALL_PREFIX + - python3 -u $CI_TOOLING/helpers/configure-build.py --product $PRODUCT --project $PROJECT --branchGroup $BRANCH_GROUP --platform $PLATFORM --installTo $INSTALL_PREFIX + - python3 -u $CI_TOOLING/helpers/compile-build.py --product $PRODUCT --project $PROJECT --branchGroup $BRANCH_GROUP --platform $PLATFORM --usingInstall $INSTALL_PREFIX + - time ./tools/run-clang-tidy.sh $(pwd) $(pwd)/build + variables: + PLATFORM: SUSEQt5.15 + BRANCH_GROUP: kf5-qt5 + artifacts: + paths: + - build/clang-tidy-report.xml + reports: + junit: build/clang-tidy-report.xml diff --git a/.kateconfig b/.kateconfig new file mode 100644 index 0000000..0d4badb --- /dev/null +++ b/.kateconfig @@ -0,0 +1 @@ +// kate: space-indent on; indent-width 4; remove-trailing-space on; remove-trailing-space-save on; diff --git a/.krazy b/.krazy new file mode 100644 index 0000000..614c639 --- /dev/null +++ b/.krazy @@ -0,0 +1,3 @@ +EXCLUDE i18ncheckarg,syscalls,qclasses,qmethods,crashy,strings,cpp +SKIP /tests/ + diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..9243fa3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,88 @@ +Maintainer: +- Dan Vrátil + +Main Authors: +- Volker Krause +- Till Adam +- Tobias Koenig +- Kevin Krammer + +Contributors: +- Albert Astals Cid +- Àlex Fiestas +- Alex Merry +- Alexander Neundorf +- Allen Winter +- Andras Mantia +- Andreas Cord-Landwehr +- Andreas Gungl +- Andreas Hartmetz +- Andreas Holzammer +- Andreas Pakulat +- Andre Heinecke +- Aurélien Gâteau +- Bertjan Broeksema +- Bjoern Ricks +- Carlo Segato +- Christian Ehrlicher +- Christian Mollekopf +- Cédric Villemain +- Christian Schaarschmidt +- Christophe Giboudeaux +- Constantin Berzan +- Dario Freddi +- David Faure +- David Jarvie +- Dirk Mueller +- Frank Osterfeld +- Grégory Oestreicher +- Gregory Schlomoff +- Guy Maurel +- Harald Fernengel +- Helio Chissini de Castro +- Igor Trindade Oliveira +- Ingo Kloecker +- Jaime Torres +- Jakub Stachowski +- Jesper Thomschütz +- Jesse Lee Zamora +- Kevin Ottens +- Kitware, Inc., Insight Consortium. +- Laurent Montel +- Leo Franchi +- Loic Marteau +- Manolo Valdes +- Marc Mutz +- Marco Martin +- Matthias Kretz +- Michael Drueing +- Michael Jansen +- Mike Arthur +- Milian Wolff +- Mirko Boehm +- Nicolás Alvarez +- Nicolas Lécureuil +- Olivier Trichet +- Patrick Spendrin +- Pavel Heimlich +- Pino Toscano +- Raphael Kubo da Costa +- Rex Dieter +- Robert Zwerus +- Rolf Eike Beer +- Romain Pokrzywka +- Sebastian Sauer +- Sebastian Trueg +- Sergio Martins +- Shaheed Haque +- Stephen Kelly +- Szymon Stefanek +- Thomas Friedrichsmeier +- Thomas McGuire +- Timo Hummel +- Tom Albers +- Torgny Nyblom +- Vadim Zhukov +- Will Stephenson +- Wolfgang Rohdewald +- Yury G. Kudryashov diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..552d1f3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,395 @@ +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) + +set(PIM_VERSION "5.18.0") +project(Akonadi VERSION ${PIM_VERSION}) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# ECM setup +set(KF5_MIN_VERSION "5.83.0") + +find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) + +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) + +include(GenerateExportHeader) +include(ECMGenerateHeaders) +include(ECMGeneratePriFile) + +include(ECMSetupVersion) +include(FeatureSummary) +include(KDEGitCommitHooks) +include(CheckAtomic) +include(CheckIncludeFiles) +include(ECMQtDeclareLoggingCategory) +include(CheckSymbolExists) + +include(KDEPackageAppTemplates) +include(ECMMarkNonGuiExecutable) +include(ECMAddTests) +include(ECMSetupQtPluginMacroNames) + +include(AkonadiMacros) + +set(QT_REQUIRED_VERSION "5.15.0") +set(RELEASE_SERVICE_VERSION "21.08.0") +set(AKONADI_FULL_VERSION "${PIM_VERSION} (${RELEASE_SERVICE_VERSION})") + +configure_file(akonadifull-version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/akonadifull-version.h @ONLY) + + +ecm_setup_version(PROJECT + VARIABLE_PREFIX AKONADI + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/akonadi_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfigVersion.cmake" + SOVERSION 5) + +# Find packages +find_package(Qt5Core ${QT_REQUIRED_VERSION} REQUIRED COMPONENTS Private) +find_package(Qt5Sql ${QT_REQUIRED_VERSION} REQUIRED COMPONENTS Private) +find_package(Qt5DBus ${QT_REQUIRED_VERSION} REQUIRED) +find_package(Qt5Network ${QT_REQUIRED_VERSION} REQUIRED) +find_package(Qt5Test ${QT_REQUIRED_VERSION} REQUIRED) +find_package(Qt5Widgets ${QT_REQUIRED_VERSION} REQUIRED) +find_package(Qt5Xml ${QT_REQUIRED_VERSION} REQUIRED) + +find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5ConfigWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5CoreAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5I18n ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5IconThemes ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5ItemModels ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5ItemViews ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5WidgetsAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5WindowSystem ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(KF5Crash ${KF5_MIN_VERSION} CONFIG REQUIRED) +find_package(Qt5Designer NO_MODULE) +set_package_properties(Qt5Designer PROPERTIES + PURPOSE "Required to build the Qt Designer plugins" + TYPE OPTIONAL +) + +option(BUILD_DESIGNERPLUGIN "Build plugin for Qt Designer" ON) +add_feature_info(DESIGNERPLUGIN ${BUILD_DESIGNERPLUGIN} "Build plugin for Qt Designer") + +set(Boost_MINIMUM_VERSION "1.34.0") + +# The Boost CMake config files have been incompatible with the CMake files for +# a long time. We'll only use what CMake's FindBoost.cmake module provides +# until the minimum Boost version is raised to 1.70. +set(Boost_NO_BOOST_CMAKE ON) + +find_package(Boost ${Boost_MINIMUM_VERSION} MODULE REQUIRED) +set_package_properties(Boost PROPERTIES + DESCRIPTION "Boost C++ Libraries" + URL "https://www.boost.org" + TYPE REQUIRED +) + +set(AccountsQt5_MINIMUM_VERSION "1.15") +find_package(AccountsQt5 ${AccountsQt5_MINIMUM_VERSION}) +set_package_properties(AccountsQt5 PROPERTIES + DESCRIPTION "Qt bindings for the Accounts framework" + URL "https://gitlab.com/accounts-sso/libaccounts-qt" + TYPE OPTIONAL +) +set(KAccounts_MINIMUM_VERSION "19.08.0") +find_package(KAccounts ${KAccounts_MINIMUM_VERSION}) +set_package_properties(KAccounts PROPERTIES + DESCRIPTION "KDE library for Accounts framework integration" + URL "https://cgit.kde.org/kaccounts-integration.git" + TYPE OPTIONAL +) +if (${AccountsQt5_FOUND} AND ${KAccounts_FOUND}) + set(WITH_ACCOUNTS TRUE) +endif() + +set(LibLZMA_MINIMUM_VERSION "5.0.0") +find_package(LibLZMA ${LibLZMA_MINIMUM_VERSION}) +set_package_properties(LibLZMA PROPERTIES + DESCRIPTION "LZMA compression library" + URL "https://tukaani.org/xz/" + TYPE REQUIRED +) + + +if(BUILD_TESTING) + set(AKONADI_TESTS_EXPORT AKONADICORE_EXPORT) + set(AKONADIWIDGET_TESTS_EXPORT AKONADIWIDGETS_EXPORT) + add_definitions(-DBUILD_TESTING) +endif() + +ecm_setup_qtplugin_macro_names( + JSON_ARG2 + "AKONADI_AGENTCONFIG_FACTORY" + CONFIG_CODE_VARIABLE + PACKAGE_SETUP_AUTOMOC_VARIABLES +) + +# Make sure the KF5Akonadi_DATA_DIR is absolute before passing it to KF5AkonadiConfig.cmake.in +# otherwise build fails either on OSX CI, or for normal users +if (IS_ABSOLUTE "${KDE_INSTALL_DATADIR_KF5}") + set(KF5Akonadi_DATA_DIR "${KDE_INSTALL_DATADIR_KF5}/akonadi") +else() + set(KF5Akonadi_DATA_DIR "${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_DATADIR_KF5}/akonadi") +endif() + +check_symbol_exists(malloc_trim "malloc.h" HAVE_MALLOC_TRIM) + +############### Build Options ############### +option(AKONADI_BUILD_QSQLITE "Build the Sqlite backend." TRUE) +option(BUILD_TOOLS "Build and install tools for development and testing purposes." TRUE) +option(INSTALL_APPARMOR "Install AppArmor profiles" TRUE) + +if(BUILD_TESTING) + set(BUILD_TOOLS TRUE) +endif() + +option(USE_UNITY_CMAKE_SUPPORT "Use UNITY cmake support (speedup compile time)" OFF) + +set(COMPILE_WITH_UNITY_CMAKE_SUPPORT OFF) +if (USE_UNITY_CMAKE_SUPPORT) + set(COMPILE_WITH_UNITY_CMAKE_SUPPORT ON) + add_definitions(-DCOMPILE_WITH_UNITY_CMAKE_SUPPORT) +endif() +set(SMI_MIN_VERSION "1.3") +find_package(SharedMimeInfo ${SMI_MIN_VERSION} REQUIRED) + +find_program(XSLTPROC_EXECUTABLE xsltproc) +if(NOT XSLTPROC_EXECUTABLE) + message(FATAL_ERROR "\nThe command line XSLT processor program 'xsltproc' could not be found.\nPlease install xsltproc.\n") +endif() + +find_program(MYSQLD_EXECUTABLE NAMES mysqld + PATHS /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec /opt/mysql/libexec /usr/mysql/bin /opt/mysql/sbin + DOC "The mysqld executable path. ONLY needed at runtime" +) + +find_path(MYSQLD_SCRIPTS_PATH NAMES mysql_install_db + DOC "Path to the mysql or mariadb installation scripts (mysql_install_db, mysql_upgrade...)" +) + +if(MYSQLD_EXECUTABLE) + message(STATUS "MySQL Server found: ${MYSQLD_EXECUTABLE}") +else() + message(STATUS "MySQL Server wasn't found. it is required to use the MySQL backend.") +endif() + +if(MYSQLD_SCRIPTS_PATH) + message(STATUS "MySQL scripts location: ${MYSQLD_SCRIPTS_PATH}") +else() + message(STATUS "MySQL scripts location was not found. Use -DMYSQLD_SCRIPTS_PATH to point to the install location.") +endif() + +find_path(POSTGRES_PATH NAMES pg_ctl + HINTS /usr/lib${LIB_SUFFIX}/postgresql/8.4/bin + /usr/lib${LIB_SUFFIX}/postgresql/9.0/bin + /usr/lib${LIB_SUFFIX}/postgresql/9.1/bin + DOC "The pg_ctl executable path. ONLY needed at runtime by the PostgreSQL backend" +) + +if(POSTGRES_PATH) + message(STATUS "PostgreSQL Server found.") +else() + message(STATUS "PostgreSQL wasn't found. it is required to use the Postgres backend.") +endif() + + +if("${DATABASE_BACKEND}" STREQUAL "SQLITE") + set(SQLITE_TYPE "REQUIRED") +else() + set(SQLITE_TYPE "OPTIONAL") +endif() + +if(AKONADI_BUILD_QSQLITE) + set(SQLITE_MIN_VERSION 3.6.23) + find_package(Sqlite ${SQLITE_MIN_VERSION}) + set_package_properties(Sqlite PROPERTIES + URL "https://www.sqlite.org" + DESCRIPTION "The Sqlite database library" + TYPE ${SQLITE_TYPE} + PURPOSE "Required by the Sqlite backend" + ) +endif() + +find_program(XMLLINT_EXECUTABLE xmllint) +if(NOT XMLLINT_EXECUTABLE) + message(STATUS "xmllint not found, skipping akonadidb.xml schema validation") +endif() + +check_include_files(unistd.h HAVE_UNISTD_H) + +if (IS_ABSOLUTE "${DBUS_INTERFACES_INSTALL_DIR}") + set(AKONADI_DBUS_INTERFACES_INSTALL_DIR "${DBUS_INTERFACES_INSTALL_DIR}") +else() + set(AKONADI_DBUS_INTERFACES_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${DBUS_INTERFACES_INSTALL_DIR}") +endif() + +if (IS_ABSOLUTE "${KDE_INSTALL_INCLUDEDIR_KF5}") + set(AKONADI_INCLUDE_DIR "${KDE_INSTALL_INCLUDEDIR_KF5}") +else() + set(AKONADI_INCLUDE_DIR "${CMAKE_INSTALL_PREFIX}/${KDE_INSTALL_INCLUDEDIR_KF5}") +endif() + +############### Build Options ############### + +include(CTest) # Calls enable_testing(). +include(CTestConfig.cmake) + +if(NOT DEFINED DATABASE_BACKEND) + set(DATABASE_BACKEND "MYSQL" CACHE STRING "The default database backend to use for Akonadi. Can be either MYSQL, POSTGRES or SQLITE") +endif() + +############### CTest options ############### +# Set a timeout value of 1 minute per test +set(DART_TESTING_TIMEOUT 60) + +# CTestCustom.cmake has to be in the CTEST_BINARY_DIR. +# in the KDE build system, this is the same as CMAKE_BINARY_DIR. +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CTestCustom.cmake ${CMAKE_CURRENT_BINARY_DIR}/CTestCustom.cmake COPYONLY) + +############### Macros ############### + +macro(SET_DEFAULT_DB_BACKEND) + set(_backend ${ARGV0}) + if("${_backend}" STREQUAL "SQLITE") + set(AKONADI_DATABASE_BACKEND QSQLITE3) + set(AKONADI_BUILD_QSQLITE TRUE) + else() + if("${_backend}" STREQUAL "POSTGRES") + set(AKONADI_DATABASE_BACKEND QPSQL) + else() + set(AKONADI_DATABASE_BACKEND QMYSQL) + endif() + endif() + + message(STATUS "Using default db backend ${AKONADI_DATABASE_BACKEND}") +endmacro() + +#### DB BACKEND DEFAULT #### +set_default_db_backend(${DATABASE_BACKEND}) + +############### Compilers flags ############### + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_C_COMPILER MATCHES "icc" OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-long-long -std=iso9899:1990 -Wundef -Wcast-align -Werror-implicit-function-declaration -Wchar-subscripts -Wall -Wextra -Wpointer-arith -Wwrite-strings -Wformat-security -Wmissing-format-attribute -fno-common") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wnon-virtual-dtor -Wundef -Wcast-align -Wchar-subscripts -Wall -Wextra -Wpointer-arith -Wformat-security -fno-common -pedantic") + CHECK_CXX_COMPILER_FLAG(-Wno-deprecated-copy NO_DEPRECATED_COPY) + if (NO_DEPRECATED_COPY) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-copy") + endif() + + set(CMAKE_CXX_FLAGS_DEBUGFULL "-g3 -fno-inline" CACHE STRING "Flags used by the C++ compiler during debugfull builds." FORCE) + set(CMAKE_C_FLAGS_DEBUGFULL "-g3 -fno-inline" CACHE STRING "Flags used by the C compiler during debugfull builds." FORCE) + mark_as_advanced(CMAKE_CXX_FLAGS_DEBUGFULL CMAKE_C_FLAGS_DEBUGFULL) +elseif (MSVC) + # This sets the __cplusplus macro to a real value based on the version of C++ specified by + # the /std switch. Without it MSVC keeps reporting C++98, so feature detection doesn't work. + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:__cplusplus") +endif() + + +if(MSVC) + set(_ENABLE_EXCEPTIONS -EHsc) +else() + set(_ENABLE_EXCEPTIONS -fexceptions) +endif() + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_ENABLE_EXCEPTIONS}") + +############### Configure files ############# + +configure_file(config-akonadi.h.cmake ${Akonadi_BINARY_DIR}/config-akonadi.h) + +############### build targets ############### + +add_definitions(-DTRANSLATION_DOMAIN=\"libakonadi5\") + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + src +) + +add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x055400) +#add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f00) +add_definitions(-DQT_NO_EMIT) +remove_definitions(-DQT_NO_FOREACH) +remove_definitions(-DQT_NO_KEYWORDS) +add_definitions(-DQT_NO_SIGNALS_SLOTS_KEYWORDS) + +add_subdirectory(src) +add_subdirectory(icons) + +add_subdirectory(templates) + +if(BUILD_TOOLS) + # add testrunner (application for managing a self-contained + # test environment) + add_subdirectory(autotests/libs/testrunner) + add_subdirectory(autotests/libs/testresource) + add_subdirectory(autotests/libs/testsearchplugin) +endif() + +if(BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) +endif() + +if(INSTALL_APPARMOR) + add_subdirectory(apparmor) +endif() + +############### install stuff ############### + +install(FILES akonadi-mime.xml DESTINATION ${KDE_INSTALL_MIMEDIR}) +update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR}) + +############### CMake Config Files ############### + +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Akonadi") + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KF5AkonadiConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} + PATH_VARS AKONADI_DBUS_INTERFACES_INSTALL_DIR + AKONADI_INCLUDE_DIR + KF5Akonadi_DATA_DIR +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiConfigVersion.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/KF5AkonadiMacros.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel +) + +install(EXPORT + KF5AkonadiTargets + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + FILE KF5AkonadiTargets.cmake + NAMESPACE KF5::) + +ecm_qt_install_logging_categories( + EXPORT AKONADI + FILE akonadi.categories + DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} + ) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadi_version.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel +) + +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) +ki18n_install(po) +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..049e31b --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,128 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "dev", + "displayName": "Build as debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "asan", + "displayName": "Build with Asan support.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-asan", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ECM_ENABLE_SANITIZERS" : "'address;undefined'", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "dev-clang", + "displayName": "dev-clang", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-clang", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + }, + "environment": { + "CXX": "clang++", + "CCACHE_DISABLE": "ON" + } + }, + { + "name": "unity", + "displayName": "Build with CMake unity support.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-unity", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "USE_UNITY_CMAKE_SUPPORT": "ON", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "displayName": "Build as release mode.", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "profile", + "displayName": "profile", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-profile", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "clazy", + "displayName": "clazy", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-clazy", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + }, + "environment": { + "CXX": "clazy", + "CCACHE_DISABLE": "ON" + } + } + + ], + "buildPresets": [ + { + "name": "dev", + "configurePreset": "dev" + }, + { + "name": "release", + "configurePreset": "release" + }, + { + "name": "dev-clang", + "configurePreset": "dev-clang" + }, + { + "name": "asan", + "configurePreset": "asan" + }, + { + "name": "unity", + "configurePreset": "unity" + }, + { + "name": "clazy", + "configurePreset": "clazy", + "environment": { + "CLAZY_CHECKS" : "level0,level1,detaching-member,ifndef-define-typo,isempty-vs-count,qrequiredresult-candidates,reserve-candidates,signal-with-return-value,unneeded-cast,function-args-by-ref,function-args-by-value,returning-void-expression,no-ctor-missing-parent-argument,isempty-vs-count,qhash-with-char-pointer-key,raw-environment-function,qproperty-type-mismatch,old-style-connect,qstring-allocations,container-inside-loop,heap-allocated-small-trivial-type,inefficient-qlist,qstring-varargs,level2,detaching-member,heap-allocated-small-trivial-type,isempty-vs-count,qstring-varargs,qvariant-template-instantiation,raw-environment-function,reserve-candidates,signal-with-return-value,thread-with-slots,no-ctor-missing-parent-argument,no-missing-typeinfo", + "CCACHE_DISABLE" : "ON" + } + } + ], + "testPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "output": {"outputOnFailure": true}, + "execution": {"noTestsAction": "error", "stopOnFailure": false} + }, + { + "name": "asan", + "configurePreset": "asan", + "output": {"outputOnFailure": true}, + "execution": {"noTestsAction": "error", "stopOnFailure": true} + } + ] +} diff --git a/CMakePresets.json.license b/CMakePresets.json.license new file mode 100644 index 0000000..d9ab68a --- /dev/null +++ b/CMakePresets.json.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2021 Laurent Montel +# SPDX-License-Identifier: BSD-3-Clause diff --git a/CTestConfig.cmake b/CTestConfig.cmake new file mode 100644 index 0000000..23e8d6e --- /dev/null +++ b/CTestConfig.cmake @@ -0,0 +1,13 @@ +## This file should be placed in the root directory of your project. +## Then modify the CMakeLists.txt file in the root directory of your +## project to incorporate the testing dashboard. +## # The following are required to uses Dart and the Cdash dashboard +## ENABLE_TESTING() +## INCLUDE(CTest) +set(CTEST_PROJECT_NAME "akonadi") +set(CTEST_NIGHTLY_START_TIME "00:00:00 UTC") + +set(CTEST_DROP_METHOD "http") +set(CTEST_DROP_SITE "my.cdash.org") +set(CTEST_DROP_LOCATION "/submit.php?project=akonadi") +set(CTEST_DROP_SITE_CDASH TRUE) diff --git a/CTestCustom.cmake b/CTestCustom.cmake new file mode 100644 index 0000000..0e35273 --- /dev/null +++ b/CTestCustom.cmake @@ -0,0 +1,22 @@ +# This file contains all the specific settings that will be used +# when running 'make Experimental' + +# Change the maximum warnings that will be displayed +# on the report page (default 50) +set(CTEST_CUSTOM_MAXIMUM_NUMBER_OF_WARNINGS 1000) + +# Errors that will be ignored +set(CTEST_CUSTOM_ERROR_EXCEPTION + ${CTEST_CUSTOM_ERROR_EXCEPTION} + "ICECC" + "Segmentation fault" + "GConf Error" + "Client failed to connect to the D-BUS daemon" + "Failed to connect to socket" + "qlist.h.*increases required alignment of target type" + "qmap.h.*increases required alignment of target type" + "qhash.h.*increases required alignment of target type" + ) + +# No coverage for these files (auto-generated, unit tests, etc) +set(CTEST_CUSTOM_COVERAGE_EXCLUDE ".moc$" "moc_" "ui_" "/tests" "/autotests" "qrc_" "adaptor.h$" "adaptor.cpp$" "/src/server/[^/]+interface\\.") diff --git a/ExtraDesktop.sh b/ExtraDesktop.sh new file mode 100644 index 0000000..65b63a5 --- /dev/null +++ b/ExtraDesktop.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +#This file outputs in a separate line each file with a .desktop syntax +#that needs to be translated but has a non .desktop extension +find -name \*.kdevtemplate -print diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..f4d31c7 --- /dev/null +++ b/INSTALL @@ -0,0 +1,60 @@ +Akonadi's build system uses cmake. + +So to compile Akonadi first create a build dir + + mkdir build + cd build + +then run cmake: + + cmake .. + +(a typical cmake option that is often used is: -DCMAKE_INSTALL_PREFIX=) + +cmake then presents a configuration summary. At this point you may +want to install missing dependancies (if you do, remove the CMakeCache.txt) +and run cmake again. + +Finally build Akonadi: + + make + +And install it (in most cases root privileges are required): + + make install + +That's all :) + +=== Build Options === + +The following options are available when running CMake: + +* AKONADI_BUILD_TESTS (Default: TRUE): Build the Akonadi unit tests +* AKONADI_BUILD_QSQLITE (Default: TRUE): Build the SQLite backend +* KDE_INSTALL_USE_QT_SYS_PATHS (Default: FALSE): Useful for distributions. + Once enabled, the qsqlite3 backend will be installed in the sqlbackends subdirectory of the default Qt plugins directory + specified at Qt build time. +* DATABASE_BACKEND (Default: MYSQL, available: MYSQL, POSTGRES, SQLITE): Define which database driver to use by default. + MYSQL is preferred, SQLITE should be avoided. + +=== Build Requirements === + +Required: + +* CMake (https://cmake.org/) >= 3.5 +* Qt5 >= 5.11.0 (http://qt.nokia.com/downloads) +* Shared-mime-info >= 0.20 (https://freedesktop.org/wiki/Software/shared-mime-info/) +* Xsltproc (http://xmlsoft.org/XSLT/downloads.html) + +Optional: + +* Mysqld (https://www.mysql.com/) - Optional at build time. You can pass -DMYSQLD_EXECUTABLE=/path/to/mysqld when running CMake instead +* SQlite >= 3.6.23 (https://www.sqlite.org/index.html) - Needed if you want to build the Sqlite backend +* Postgresql (https://www.postgresql.org/) - Optional at build time. You can pass -DPOSTGRES_PATH=/path/to/pg_ctl when running CMake instead + +=== Runtime Requirements === + +* SQlite if you plan to use the SQLite backend (NOT RECOMMENDED for desktop) +* MySQL server >= 5.1.3 (or compatible replacements such as MariaDB) if you plan to use the Mysql backend +* a Postgresql server if you plan to use the Postgres backend + diff --git a/Info.plist.template b/Info.plist.template new file mode 100644 index 0000000..c39ddb9 --- /dev/null +++ b/Info.plist.template @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + LSUIElement + 1 + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + diff --git a/KF5AkonadiConfig.cmake.in b/KF5AkonadiConfig.cmake.in new file mode 100644 index 0000000..bcf7320 --- /dev/null +++ b/KF5AkonadiConfig.cmake.in @@ -0,0 +1,41 @@ +@PACKAGE_INIT@ + +set_and_check(AKONADI_DBUS_INTERFACES_DIR "@PACKAGE_AKONADI_DBUS_INTERFACES_INSTALL_DIR@") +set_and_check(AKONADI_INCLUDE_DIR "@PACKAGE_AKONADI_INCLUDE_DIR@") + +# The directory where akonadi-xml.xsd and kcfg2dbus.xsl are installed +set(KF5Akonadi_DATA_DIR "@PACKAGE_KF5Akonadi_DATA_DIR@") + +# set the directories +if(NOT AKONADI_INSTALL_DIR) + set(AKONADI_INSTALL_DIR "@CMAKE_INSTALL_PREFIX@") +endif(NOT AKONADI_INSTALL_DIR) +include(CMakeFindDependencyMacro) +find_dependency(KF5Config "@KF5_MIN_VERSION@") +find_dependency(KF5ConfigWidgets "@KF5_MIN_VERSION@") +find_dependency(KF5CoreAddons "@KF5_MIN_VERSION@") +find_dependency(KF5ItemModels "@KF5_MIN_VERSION@") +find_dependency(KF5XmlGui "@KF5_MIN_VERSION@") + +find_dependency(Qt5Core "@QT_REQUIRED_VERSION@") +find_dependency(Qt5DBus "@QT_REQUIRED_VERSION@") +find_dependency(Qt5Gui "@QT_REQUIRED_VERSION@") +find_dependency(Qt5Network "@QT_REQUIRED_VERSION@") +find_dependency(Qt5Widgets "@QT_REQUIRED_VERSION@") +find_dependency(Qt5Xml "@QT_REQUIRED_VERSION@") + +if(BUILD_TESTING) + # Link targets required by KF5AkonadiMacros.cmake + find_dependency(KF5KIO "@KF5_MIN_VERSION@") + find_dependency(Qt5Test "@QT_REQUIRED_VERSION@") +endif() + +set(Boost_NO_BOOST_CMAKE ON) +find_dependency(Boost "@Boost_MINIMUM_VERSION@") +# Reset Boost_NO_BOOST_CMAKE now that we found what we need +unset(Boost_NO_BOOST_CMAKE) + +include(${CMAKE_CURRENT_LIST_DIR}/KF5AkonadiTargets.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/KF5AkonadiMacros.cmake) + +@PACKAGE_SETUP_AUTOMOC_VARIABLES@ diff --git a/KF5AkonadiMacros.cmake b/KF5AkonadiMacros.cmake new file mode 100644 index 0000000..9ce62cc --- /dev/null +++ b/KF5AkonadiMacros.cmake @@ -0,0 +1,142 @@ +# +# Convenience macros to add akonadi testrunner unit-tests +# +# Set AKONADI_RUN_MYSQL_ISOLATED_TESTS to false to prevent run the tests against MySQL +# Set AKONADI_RUN_PGSQL_ISOLATED_TESTS to false to prevent run the tests against PostgreSQL +# Set AKONADI_RUN_SQLITE_ISOLATED_TESTS to false to prevent run the tests against SQLite +# Set AKONADI_TESTS_XML to true if you want qtestlib to generate (per backend) XML files with the test results +# +# You still need to provide the test environment, see akonadi/autotests/libs/unittestenv +# copy the unittestenv directory to your unit test directory and update the files +# as necessary + +function(add_akonadi_isolated_test) + + function(add_akonadi_isolated_test_impl) + set(options) + set(oneValueArgs SOURCE) + set(multiValueArgs BACKENDS ADDITIONAL_SOURCES LINK_LIBRARIES) + cmake_parse_arguments(CONFIG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(_test ${CONFIG_SOURCE}) + get_filename_component(_name ${CONFIG_SOURCE} NAME_WE) + add_executable(${_name} ${_test} ${CONFIG_ADDITIONAL_SOURCES}) + ecm_mark_as_test(${_name}) + target_link_libraries(${_name} + Qt::Test Qt::Gui Qt::Widgets Qt::Network KF5::KIOCore + KF5::AkonadiCore KF5::AkonadiPrivate Qt::DBus + ${CONFIG_LINK_LIBRARIES} + ) + + if (NOT DEFINED _testrunner) + if (${PROJECT_NAME} STREQUAL Akonadi AND TARGET akonaditest) + # If this macro is used in Akonadi itself, just use the target name; + # CMake will replace it with the path to the executable in the build + # directory. This will ensure it works even on a clean build, + # where the executable doesn't exist yet at cmake time. + set(_testrunner akonaditest) + else() + find_program(_testrunner NAMES akonaditest akonaditest.exe) + if (NOT _testrunner) + message(WARNING "Could not locate akonaditest executable, isolated Akonadi tests will fail!") + endif() + endif() + endif() + + # based on kde4_add_unit_test + set(_executable $) + if (APPLE) + set(_executable ${_executable}.app/Contents/MacOS/${_name}) + endif() + + function(_defineTest name backend) + set(backends ${ARGN}) + if (NOT DEFINED AKONADI_RUN_${backend}_ISOLATED_TESTS OR AKONADI_RUN_${backend}_ISOLATED_TESTS) + LIST(LENGTH "${backends}" backendsLen) + string(TOLOWER ${backend} lcbackend) + LIST(FIND "${backends}" ${lcbackend} enableBackend) + if (${backendsLen} EQUAL 0 OR ${enableBackend} GREATER -1) + set(configFile ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config.xml) + if (AKONADI_TESTS_XML) + set(extraOptions -xml -o "${TEST_RESULT_OUTPUT_PATH}/${lcbackend}-${name}.xml") + endif() + set(_test_name akonadi-${lcbackend}-${name}) + add_test(NAME ${_test_name} + COMMAND ${_testrunner} -c "${configFile}" -b ${lcbackend} + ${_executable} ${extraOptions} + ) + # Taken from ECMAddTests.cmake + if (CMAKE_LIBRARY_OUTPUT_DIRECTORY) + if(CMAKE_HOST_SYSTEM MATCHES "Windows") + set(PATHSEP ";") + else() # e.g. Linux + set(PATHSEP ":") + endif() + set(_plugin_path $ENV{QT_PLUGIN_PATH}) + set(_test_env + QT_PLUGIN_PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}${PATHSEP}$ENV{QT_PLUGIN_PATH} + LD_LIBRARY_PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}${PATHSEP}$ENV{LD_LIBRARY_PATH} + ) + set_tests_properties(${_test_name} PROPERTIES ENVIRONMENT "${_test_env}") + endif() + endif() + endif() + endfunction() + + find_program(MYSQLD_EXECUTABLE mysqld /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec /opt/mysql/libexec /usr/mysql/bin) + if (MYSQLD_EXECUTABLE AND NOT WIN32) + _defineTest(${_name} "MYSQL" ${CONFIG_BACKENDS}) + endif() + + find_program(POSTGRES_EXECUTABLE postgres) + if (POSTGRES_EXECUTABLE AND NOT WIN32) + _defineTest(${_name} "PGSQL" ${CONFIG_BACKENDS}) + endif() + + _defineTest(${_name} "SQLITE" ${CONFIG_BACKENDS}) + endfunction() + + LIST(LENGTH "${ARGN}" argc) + if (${argc} EQUAL 0) + add_akonadi_isolated_test_impl(SOURCE ${ARGN}) + else() + add_akonadi_isolated_test_impl(${ARGN}) + endif() +endfunction() + +function(add_akonadi_isolated_test_advanced source additional_sources link_libraries) + add_akonadi_isolated_test(SOURCE ${source} + ADDITIONAL_SOURCES "${additional_sources}" + LINK_LIBRARIES "${link_libraries}" + ) +endfunction() + +function(kcfg_generate_dbus_interface _kcfg _name) + if (NOT XSLTPROC_EXECUTABLE) + message(FATAL_ERROR "xsltproc executable not found but needed by KCFG_GENERATE_DBUS_INTERFACE()") + endif() + + # When using this macro inside Akonadi, we need to refer to the file in the + # repo + if (Akonadi_SOURCE_DIR) + set(xsl_path ${Akonadi_SOURCE_DIR}/src/core/kcfg2dbus.xsl) + else() + set(xsl_path ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl) + endif() + file(RELATIVE_PATH xsl_relpath ${CMAKE_CURRENT_BINARY_DIR} ${xsl_path}) + if (IS_ABSOLUTE ${_kcfg}) + file(RELATIVE_PATH kcfg_relpath ${CMAKE_CURRENT_BINARY_DIR} ${_kcfg}) + else() + file(RELATIVE_PATH kcfg_relpath ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/${_kcfg}) + endif() + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${_name}.xml + --stringparam interfaceName ${_name} + ${xsl_relpath} + ${kcfg_relpath} + DEPENDS + ${xsl_path} + ${_kcfg} + ) +endfunction() diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..0741db7 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,26 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..a343ccd --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,119 @@ +Creative Commons Legal Code + +CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES +NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE +AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION +ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE +OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS +LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION +OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive +Copyright and Related Rights (defined below) upon the creator and subsequent +owner(s) (each and all, an "owner") of an original work of authorship and/or +a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later claims +of infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further production +of creative, cultural and scientific works, or to gain reputation or greater +distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with +a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or +her Copyright and Related Rights in the Work and the meaning and intended +legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected +by copyright and related or neighboring rights ("Copyright and Related Rights"). +Copyright and Related Rights include, but are not limited to, the following: + +i. the right to reproduce, adapt, distribute, perform, display, communicate, +and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + +iii. publicity and privacy rights pertaining to a person's image or likeness +depicted in a Work; + +iv. rights protecting against unfair competition in regards to a Work, subject +to the limitations in paragraph 4(a), below; + +v. rights protecting the extraction, dissemination, use and reuse of data +in a Work; + +vi. database rights (such as those arising under Directive 96/9/EC of the +European Parliament and of the Council of 11 March 1996 on the legal protection +of databases, and under any national implementation thereof, including any +amended or successor version of such directive); and + +vii. other similar, equivalent or corresponding rights throughout the world +based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time extensions), +(iii) in any current or future medium and for any number of copies, and (iv) +for any purpose whatsoever, including without limitation commercial, advertising +or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the +benefit of each member of the public at large and to the detriment of Affirmer's +heirs and successors, fully intending that such Waiver shall not be subject +to revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account Affirmer's +express Statement of Purpose. In addition, to the extent the Waiver is so +judged Affirmer hereby grants to each affected person a royalty-free, non +transferable, non sublicensable, non exclusive, irrevocable and unconditional +license to exercise Affirmer's Copyright and Related Rights in the Work (i) +in all territories worldwide, (ii) for the maximum duration provided by applicable +law or treaty (including future time extensions), (iii) in any current or +future medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional purposes +(the "License"). The License shall be deemed effective as of the date CC0 +was applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder of +the License, and in such case Affirmer hereby affirms that he or she will +not (i) exercise any of his or her remaining Copyright and Related Rights +in the Work or (ii) assert any associated claims and causes of action with +respect to the Work, in either case contrary to Affirmer's express Statement +of Purpose. + + 4. Limitations and Disclaimers. + +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, +licensed or otherwise affected by this document. + +b. Affirmer offers the Work as-is and makes no representations or warranties +of any kind concerning the Work, express, implied, statutory or otherwise, +including without limitation warranties of title, merchantability, fitness +for a particular purpose, non infringement, or the absence of latent or other +defects, accuracy, or the present or absence of errors, whether or not discoverable, +all to the greatest extent permissible under applicable law. + +c. Affirmer disclaims responsibility for clearing rights of other persons +that may apply to the Work or any use thereof, including without limitation +any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims +responsibility for obtaining any necessary consents, permissions or other +rights required for any use of the Work. + +d. Affirmer understands and acknowledges that Creative Commons is not a party +to this document and has no duty or obligation with respect to this CC0 or +use of the Work. diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000..0f3d641 --- /dev/null +++ b/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,319 @@ +GNU GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C)< yyyy> + +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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/LICENSES/GPL-2.0-or-later.txt b/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000..1d80ac3 --- /dev/null +++ b/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,319 @@ +GNU GENERAL PUBLIC LICENSE + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software +is covered by the GNU Lesser General Public License instead.) You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must give the recipients all the rights that you have. You +must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If +the software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or translated +into another language. (Hereinafter, translation is included without limitation +in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program +is not restricted, and the output from the Program is covered only if its +contents constitute a work based on the Program (independent of having been +made by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source code +as you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence +of any warranty; and give any other recipients of the Program a copy of this +License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print +such an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Program, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Program. + +In addition, mere aggregation of another work not based on the Program with +the Program (or with a work based on the Program) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source code, +which must be distributed under the terms of Sections 1 and 2 above on a medium +customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for noncommercial +distribution and only if you received the program in object code or executable +form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with +the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except +as expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses terminated +so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Program +(or any work based on the Program), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor +to copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of +the rights granted herein. You are not responsible for enforcing compliance +by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Program at all. For example, if a +patent license would not permit royalty-free redistribution of the Program +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system, which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of +the General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose +any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing and reuse +of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when +it starts in an interactive mode: + +Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show +c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be mouse-clicks +or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' +(which makes passes at compilers) written by James Hacker. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/LICENSES/GPL-3.0-only.txt b/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000..e142a52 --- /dev/null +++ b/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,625 @@ +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for them if you wish), that +you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs, and that you know you +can do these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And +you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. +Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a "modified version" of the earlier work +or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To "convey" a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A "Major Component", in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive +it, in any medium, provided that you conspicuously and appropriately publish +on each copy an appropriate copyright notice; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to "keep intact all notices". + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an "aggregate" if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + + 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + + 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. "Knowingly relying" +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) +that contradict the conditions of this License, they do not excuse you from +the conditions of this License. If you cannot convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + + 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU General Public License from time to time. Such new versions will be similar +in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License "or any +later version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM +PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + + 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM +AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO +USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. END OF +TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + +This is free software, and you are welcome to redistribute it under certain +conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/LICENSES/LGPL-2.0-only.txt b/LICENSES/LGPL-2.0-only.txt new file mode 100644 index 0000000..5c96471 --- /dev/null +++ b/LICENSES/LGPL-2.0-only.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library 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 Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.0-or-later.txt b/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000..5c96471 --- /dev/null +++ b/LICENSES/LGPL-2.0-or-later.txt @@ -0,0 +1,446 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + +Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. + +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is numbered 2 because +it goes with version 2 of the ordinary GPL.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Library General Public License, applies to some specially +designated Free Software Foundation software, and to any other libraries whose +authors decide to use it. You can use it for your libraries, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library, or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +a program with the library, you must provide complete object files to the +recipients so that they can relink them with the library, after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +Our method of protecting your rights has two steps: (1) copyright the library, +and (2) offer you this license which gives you legal permission to copy, distribute +and/or modify the library. + +Also, for each distributor's protection, we want to make certain that everyone +understands that there is no warranty for this free library. If the library +is modified by someone else and passed on, we want its recipients to know +that what they have is not the original version, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that companies distributing free software will individually +obtain patent licenses, thus in effect transforming the program into proprietary +software. To prevent this, we have made it clear that any patent must be licensed +for everyone's free use or not licensed at all. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License, which was designed for utility programs. This license, +the GNU Library General Public License, applies to certain designated libraries. +This license is quite different from the ordinary one; be sure to read it +in full, and don't assume that anything in it is the same as in the ordinary +license. + +The reason we have a separate public license for some libraries is that they +blur the distinction we usually make between modifying or adding to a program +and simply using it. Linking a program with a library, without changing the +library, is in some sense simply using the library, and is analogous to running +a utility program or application program. However, in a textual and legal +sense, the linked executable is a combined work, a derivative of the original +library, and the ordinary General Public License treats it as such. + +Because of this blurred distinction, using the ordinary General Public License +for libraries did not effectively promote software sharing, because most developers +did not use the libraries. We concluded that weaker conditions might promote +sharing better. + +However, unrestricted linking of non-free programs would deprive the users +of those programs of all benefit from the free status of the libraries themselves. +This Library General Public License is intended to permit developers of non-free +programs to use free libraries, while preserving your freedom as a user of +such programs to change the free libraries that are incorporated in them. +(We have not seen how to achieve this as regards changes in header files, +but we have achieved it as regards changes in the actual functions of the +Library.) The hope is that this will lead to faster development of free libraries. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, while the latter only works together with the library. + +Note that it is possible for a library to be covered by the ordinary General +Public License rather than by this special one. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library which contains a +notice placed by the copyright holder or other authorized party saying it +may be distributed under the terms of this Library General Public License +(also called "this License"). Each licensee is addressed as "you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also compile or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +c) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +d) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the source code distributed need +not include anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the operating +system on which the executable runs, unless that component itself accompanies +the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Library General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +one line to give the library's name and an idea of what it does. + +Copyright (C) year name of author + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Library General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) +any later version. + +This library 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 Library General Public License for more +details. + +You should have received a copy of the GNU Library General Public License +along with this library; if not, write to the Free Software Foundation, Inc., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +signature of Ty Coon, 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.1-only.txt b/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000..130dffb --- /dev/null +++ b/LICENSES/LGPL-2.1-only.txt @@ -0,0 +1,467 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the +successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + +< one line to give the library's name and an idea of what it does. > + +Copyright (C) < year > < name of author > + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library 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 Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information +on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +< signature of Ty Coon > , 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LGPL-2.1-or-later.txt b/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 0000000..04bb156 --- /dev/null +++ b/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,468 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 2.1, February 1999 + +Copyright (C) 1991, 1999 Free Software Foundation, Inc. + +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts as the +successor of the GNU Library Public License, version 2, hence the version +number 2.1.] + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public Licenses are intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. + +This license, the Lesser General Public License, applies to some specially +designated software packages--typically libraries--of the Free Software Foundation +and other authors who decide to use it. You can use it too, but we suggest +you first think carefully about whether this license or the ordinary General +Public License is the better strategy to use in any particular case, based +on the explanations below. + +When we speak of free software, we are referring to freedom of use, not price. +Our General Public Licenses are designed to make sure that you have the freedom +to distribute copies of free software (and charge for this service if you +wish); that you receive source code or can get it if you want it; that you +can change the software and use pieces of it in new free programs; and that +you are informed that you can do these things. + +To protect your rights, we need to make restrictions that forbid distributors +to deny you these rights or to ask you to surrender these rights. These restrictions +translate to certain responsibilities for you if you distribute copies of +the library or if you modify it. + +For example, if you distribute copies of the library, whether gratis or for +a fee, you must give the recipients all the rights that we gave you. You must +make sure that they, too, receive or can get the source code. If you link +other code with the library, you must provide complete object files to the +recipients, so that they can relink them with the library after making changes +to the library and recompiling it. And you must show them these terms so they +know their rights. + +We protect your rights with a two-step method: (1) we copyright the library, +and (2) we offer you this license, which gives you legal permission to copy, +distribute and/or modify the library. + +To protect each distributor, we want to make it very clear that there is no +warranty for the free library. Also, if the library is modified by someone +else and passed on, the recipients should know that what they have is not +the original version, so that the original author's reputation will not be +affected by problems that might be introduced by others. + +Finally, software patents pose a constant threat to the existence of any free +program. We wish to make sure that a company cannot effectively restrict the +users of a free program by obtaining a restrictive license from a patent holder. +Therefore, we insist that any patent license obtained for a version of the +library must be consistent with the full freedom of use specified in this +license. + +Most GNU software, including some libraries, is covered by the ordinary GNU +General Public License. This license, the GNU Lesser General Public License, +applies to certain designated libraries, and is quite different from the ordinary +General Public License. We use this license for certain libraries in order +to permit linking those libraries into non-free programs. + +When a program is linked with a library, whether statically or using a shared +library, the combination of the two is legally speaking a combined work, a +derivative of the original library. The ordinary General Public License therefore +permits such linking only if the entire combination fits its criteria of freedom. +The Lesser General Public License permits more lax criteria for linking other +code with the library. + +We call this license the "Lesser" General Public License because it does Less +to protect the user's freedom than the ordinary General Public License. It +also provides other free software developers Less of an advantage over competing +non-free programs. These disadvantages are the reason we use the ordinary +General Public License for many libraries. However, the Lesser license provides +advantages in certain special circumstances. + +For example, on rare occasions, there may be a special need to encourage the +widest possible use of a certain library, so that it becomes a de-facto standard. +To achieve this, non-free programs must be allowed to use the library. A more +frequent case is that a free library does the same job as widely used non-free +libraries. In this case, there is little to gain by limiting the free library +to free software only, so we use the Lesser General Public License. + +In other cases, permission to use a particular library in non-free programs +enables a greater number of people to use a large body of free software. For +example, permission to use the GNU C Library in non-free programs enables +many more people to use the whole GNU operating system, as well as its variant, +the GNU/Linux operating system. + +Although the Lesser General Public License is Less protective of the users' +freedom, it does ensure that the user of a program that is linked with the +Library has the freedom and the wherewithal to run that program using a modified +version of the Library. + +The precise terms and conditions for copying, distribution and modification +follow. Pay close attention to the difference between a "work based on the +library" and a "work that uses the library". The former contains code derived +from the library, whereas the latter must be combined with the library in +order to run. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License Agreement applies to any software library or other program +which contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Lesser General +Public License (also called "this License"). Each licensee is addressed as +"you". + +A "library" means a collection of software functions and/or data prepared +so as to be conveniently linked with application programs (which use some +of those functions and data) to form executables. + +The "Library", below, refers to any such software library or work which has +been distributed under these terms. A "work based on the Library" means either +the Library or any derivative work under copyright law: that is to say, a +work containing the Library or a portion of it, either verbatim or with modifications +and/or translated straightforwardly into another language. (Hereinafter, translation +is included without limitation in the term "modification".) + +"Source code" for a work means the preferred form of the work for making modifications +to it. For a library, complete source code means all the source code for all +modules it contains, plus any associated interface definition files, plus +the scripts used to control compilation and installation of the library. + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running a program +using the Library is not restricted, and output from such a program is covered +only if its contents constitute a work based on the Library (independent of +the use of the Library in a tool for writing it). Whether that is true depends +on what the Library does and what the program that uses the Library does. + +1. You may copy and distribute verbatim copies of the Library's complete source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and disclaimer +of warranty; keep intact all the notices that refer to this License and to +the absence of any warranty; and distribute a copy of this License along with +the Library. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Library or any portion of it, +thus forming a work based on the Library, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all +of these conditions: + + a) The modified work must itself be a software library. + +b) You must cause the files modified to carry prominent notices stating that +you changed the files and the date of any change. + +c) You must cause the whole of the work to be licensed at no charge to all +third parties under the terms of this License. + +d) If a facility in the modified Library refers to a function or a table of +data to be supplied by an application program that uses the facility, other +than as an argument passed when the facility is invoked, then you must make +a good faith effort to ensure that, in the event an application does not supply +such function or table, the facility still operates, and performs whatever +part of its purpose remains meaningful. + +(For example, a function in a library to compute square roots has a purpose +that is entirely well-defined independent of the application. Therefore, Subsection +2d requires that any application-supplied function or table used by this function +must be optional: if the application does not supply it, the square root function +must still compute square roots.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Library, and can be reasonably +considered independent and separate works in themselves, then this License, +and its terms, do not apply to those sections when you distribute them as +separate works. But when you distribute the same sections as part of a whole +which is a work based on the Library, the distribution of the whole must be +on the terms of this License, whose permissions for other licensees extend +to the entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise +the right to control the distribution of derivative or collective works based +on the Library. + +In addition, mere aggregation of another work not based on the Library with +the Library (or with a work based on the Library) on a volume of a storage +or distribution medium does not bring the other work under the scope of this +License. + +3. You may opt to apply the terms of the ordinary GNU General Public License +instead of this License to a given copy of the Library. To do this, you must +alter all the notices that refer to this License, so that they refer to the +ordinary GNU General Public License, version 2, instead of to this License. +(If a newer version than version 2 of the ordinary GNU General Public License +has appeared, then you can specify that version instead if you wish.) Do not +make any other change in these notices. + +Once this change is made in a given copy, it is irreversible for that copy, +so the ordinary GNU General Public License applies to all subsequent copies +and derivative works made from that copy. + +This option is useful when you wish to copy part of the code of the Library +into a program that is not a library. + +4. You may copy and distribute the Library (or a portion or derivative of +it, under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you accompany it with the complete corresponding +machine-readable source code, which must be distributed under the terms of +Sections 1 and 2 above on a medium customarily used for software interchange. + +If distribution of object code is made by offering access to copy from a designated +place, then offering equivalent access to copy the source code from the same +place satisfies the requirement to distribute the source code, even though +third parties are not compelled to copy the source along with the object code. + +5. A program that contains no derivative of any portion of the Library, but +is designed to work with the Library by being compiled or linked with it, +is called a "work that uses the Library". Such a work, in isolation, is not +a derivative work of the Library, and therefore falls outside the scope of +this License. + +However, linking a "work that uses the Library" with the Library creates an +executable that is a derivative of the Library (because it contains portions +of the Library), rather than a "work that uses the library". The executable +is therefore covered by this License. Section 6 states terms for distribution +of such executables. + +When a "work that uses the Library" uses material from a header file that +is part of the Library, the object code for the work may be a derivative work +of the Library even though the source code is not. Whether this is true is +especially significant if the work can be linked without the Library, or if +the work is itself a library. The threshold for this to be true is not precisely +defined by law. + +If such an object file uses only numerical parameters, data structure layouts +and accessors, and small macros and small inline functions (ten lines or less +in length), then the use of the object file is unrestricted, regardless of +whether it is legally a derivative work. (Executables containing this object +code plus portions of the Library will still fall under Section 6.) + +Otherwise, if the work is a derivative of the Library, you may distribute +the object code for the work under the terms of Section 6. Any executables +containing that work also fall under Section 6, whether or not they are linked +directly with the Library itself. + +6. As an exception to the Sections above, you may also combine or link a "work +that uses the Library" with the Library to produce a work containing portions +of the Library, and distribute that work under terms of your choice, provided +that the terms permit modification of the work for the customer's own use +and reverse engineering for debugging such modifications. + +You must give prominent notice with each copy of the work that the Library +is used in it and that the Library and its use are covered by this License. +You must supply a copy of this License. If the work during execution displays +copyright notices, you must include the copyright notice for the Library among +them, as well as a reference directing the user to the copy of this License. +Also, you must do one of these things: + +a) Accompany the work with the complete corresponding machine-readable source +code for the Library including whatever changes were used in the work (which +must be distributed under Sections 1 and 2 above); and, if the work is an +executable linked with the Library, with the complete machine-readable "work +that uses the Library", as object code and/or source code, so that the user +can modify the Library and then relink to produce a modified executable containing +the modified Library. (It is understood that the user who changes the contents +of definitions files in the Library will not necessarily be able to recompile +the application to use the modified definitions.) + +b) Use a suitable shared library mechanism for linking with the Library. A +suitable mechanism is one that (1) uses at run time a copy of the library +already present on the user's computer system, rather than copying library +functions into the executable, and (2) will operate properly with a modified +version of the library, if the user installs one, as long as the modified +version is interface-compatible with the version that the work was made with. + +c) Accompany the work with a written offer, valid for at least three years, +to give the same user the materials specified in Subsection 6a, above, for +a charge no more than the cost of performing this distribution. + +d) If distribution of the work is made by offering access to copy from a designated +place, offer equivalent access to copy the above specified materials from +the same place. + +e) Verify that the user has already received a copy of these materials or +that you have already sent this user a copy. + +For an executable, the required form of the "work that uses the Library" must +include any data and utility programs needed for reproducing the executable +from it. However, as a special exception, the materials to be distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +It may happen that this requirement contradicts the license restrictions of +other proprietary libraries that do not normally accompany the operating system. +Such a contradiction means you cannot use both them and the Library together +in an executable that you distribute. + +7. You may place library facilities that are a work based on the Library side-by-side +in a single library together with other library facilities not covered by +this License, and distribute such a combined library, provided that the separate +distribution of the work based on the Library and of the other library facilities +is otherwise permitted, and provided that you do these two things: + +a) Accompany the combined library with a copy of the same work based on the +Library, uncombined with any other library facilities. This must be distributed +under the terms of the Sections above. + +b) Give prominent notice with the combined library of the fact that part of +it is a work based on the Library, and explaining where to find the accompanying +uncombined form of the same work. + +8. You may not copy, modify, sublicense, link with, or distribute the Library +except as expressly provided under this License. Any attempt otherwise to +copy, modify, sublicense, link with, or distribute the Library is void, and +will automatically terminate your rights under this License. However, parties +who have received copies, or rights, from you under this License will not +have their licenses terminated so long as such parties remain in full compliance. + +9. You are not required to accept this License, since you have not signed +it. However, nothing else grants you permission to modify or distribute the +Library or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the Library +(or any work based on the Library), you indicate your acceptance of this License +to do so, and all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + +10. Each time you redistribute the Library (or any work based on the Library), +the recipient automatically receives a license from the original licensor +to copy, distribute, link with or modify the Library subject to these terms +and conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties with this License. + +11. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of +this License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as +a consequence you may not distribute the Library at all. For example, if a +patent license would not permit royalty-free redistribution of the Library +by all those who receive copies directly or indirectly through you, then the +only way you could satisfy both it and this License would be to refrain entirely +from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents +or other property right claims or to contest validity of any such claims; +this section has the sole purpose of protecting the integrity of the free +software distribution system which is implemented by public license practices. +Many people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +12. If the distribution and/or use of the Library is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Library under this License may add an explicit geographical +distribution limitation excluding those countries, so that distribution is +permitted only in or among countries not thus excluded. In such case, this +License incorporates the limitation as if written in the body of this License. + +13. The Free Software Foundation may publish revised and/or new versions of +the Lesser General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to address +new problems or concerns. + +Each version is given a distinguishing version number. If the Library specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Library does not specify a license version number, you may choose any version +ever published by the Free Software Foundation. + +14. If you wish to incorporate parts of the Library into other free programs +whose distribution conditions are incompatible with these, write to the author +to ask for permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make exceptions +for this. Our decision will be guided by the two goals of preserving the free +status of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + +15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE +OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE +THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE +OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES +OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH +HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Libraries + +If you develop a new library, and you want it to be of the greatest possible +use to the public, we recommend making it free software that everyone can +redistribute and change. You can do so by permitting redistribution under +these terms (or, alternatively, under the terms of the ordinary General Public +License). + +To apply these terms, attach the following notices to the library. It is safest +to attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the "copyright" +line and a pointer to where the full notice is found. + + + +Copyright (C) + +This library is free software; you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) +any later version. + +This library 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 Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License along +with this library; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the library, if necessary. Here +is a sample; alter the names: + +Yoyodyne, Inc., hereby disclaims all copyright interest in + +the library `Frob' (a library for tweaking knobs) written + +by James Random Hacker. + +< signature of Ty Coon > , 1 April 1990 + +Ty Coon, President of Vice + +That's all there is to it! diff --git a/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000..60a2dff --- /dev/null +++ b/LICENSES/LicenseRef-KDE-Accepted-GPL.txt @@ -0,0 +1,12 @@ +This library 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 3 of +the license or (at your option) at any later version that is +accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 14 of version 3 of the license. + +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. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES/Qt-LGPL-exception-1.1.txt b/LICENSES/Qt-LGPL-exception-1.1.txt new file mode 100644 index 0000000..d0f532e --- /dev/null +++ b/LICENSES/Qt-LGPL-exception-1.1.txt @@ -0,0 +1,21 @@ +The Qt Company Qt LGPL Exception version 1.1 + +As an additional permission to the GNU Lesser General Public License version 2.1, the object code form of a "work that uses the Library" may incorporate material from a header file that is part of the Library. You may distribute such object code under terms of your choice, provided that: + + (i) the header files of the Library have not been modified; and + + (ii) the incorporated material is limited to numerical parameters, data structure layouts, accessors, macros, inline functions and templates; and + + (iii) you comply with the terms of Section 6 of the GNU Lesser General Public License version 2.1. + +Moreover, you may apply this exception to a modified version of the Library, provided that such modification does not involve copying material from the Library into the modified Library's header files unless such material is limited to + + (i) numerical parameters; + + (ii) data structure layouts; + + (iii) accessors; and + + (iv) small macros, templates and inline functions of five lines or less in length. + +Furthermore, you are not required to apply this additional permission to a modified version of the Library. diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..c06a320 --- /dev/null +++ b/NEWS @@ -0,0 +1,529 @@ +1.13.0 10-August-2014 +---------------------------------------------- +- Fixed virtual collections statistics +- Fixed tag RID fetch +- Fixed HRID-based fetches +- Fixed race condition in StorageDebugger +- Use FindBacktrace.cmake from CMake 3.0 instead of our own detection + +1.12.90 07-July-2014 +---------------------------------------------- +- MERGE command for faster synchronization +- Optimizations in various commands handlers +- SELECT command is obsolete now +- Performance and concurrency improvements in QSQLITE3 driver +- Introduced Collection sync preferences as an improvement over the IMAP-based subscription model +- Disable filesystem copy-on-write for DB files when running on Btrfs +- Introduced direct streaming of external parts +- Fixed SearchManager DBus interface not being registered to DBus +- Fixed handling of tags in AK-APPEND and MERGE commands +- Various fixes in virtual collections handling + +1.12.1 07-April-2014 +---------------------------------------------- +- Fixed deadlock in SearchManager +- Fixed notification emission when appending items +- Fixed ItemRetriever ignoring changeSince argument +- Fixed X-AKAPPEND command response +- Fixed RID-based FETCH +- Fixed data loss in case of long-lasting copy or move operations + +1.12.0 25-March-2014 +---------------------------------------------- +- Improved 'akonadictl status' command output +- Fixed indexing of items in collections with short cache expiration +- Fixed building Akonadi in subdirectory +- Fixed deadlock in SearchManager +- Fixed runtime warnings + +1.11.90 19-March-2014 +---------------------------------------------- +- Fixed collection scheduling +- Fixed indexing of expired items from local resources +- Fixed database schema update with PostgreSQL +- Fixes in searching and search updates + +1.11.80 28-February-2014 +---------------------------------------------- +- Server-search support +- Search plugins support +- Tags support +- Fixes and improvements in search +- Fixes in protocol parser +- Fixed inter-resource moves +- Fixed .desktop files parsing +- Optimized collections tasks scheduling +- Optimized flags handling +- Optimized appending new items via AK-APPEND +- Handle database transactions deadlocks and timeouts +- Improved PostgreSQL support +- Soprano is now an optional dependency +- Removed MySQL Embedded support + +1.11.0 28-November-2013 +---------------------------------------------- +- fix joined UPDATE queries failing with SQLite + +1.10.80 05-November-2013 +---------------------------------------------- +- Servser-side notification filtering +- GID support +- Export custom agent properties to clients +- Faster Akonadi shutdown +- Improved and faster database schema check on start +- Enabled C++11 support +- Optimize some SQL queries +- Store only relative paths to external payload files in database + +1.10.3 04-October-2013 +---------------------------------------------- +- Fix support for latest PostgreSQL +- Check MySQL version at runtime, require at least 5.1 +- Fix crash when destroying DataStore with backends other than MySQL +- Fix problem with too long socket paths +- Send dummy queries to MySQL to keep the connection alive +- Fix crash when no flags are changed + +1.10.2 23-July-2013 +---------------------------------------------- +- Fix PostgreSQL support (once more) + +1.10.1 22-July-2013 +---------------------------------------------- +- Fix PostgreSQL support +- Optimize appending flags to items +- Introduce CHANGEDSINCE parameter to FETCH command + +1.10.0 09-July-2013 +---------------------------------------------- +- Memory optimizations +- Fix a runtime error on Windows + +1.9.80 10-June-2013 +---------------------------------------------- +- Update item access time less often. +- Don't try to start akonadiserver if mysqld is not installed +- Allow to fetch available items even if there are errors in some of the items. +- Properly restrict the external part removal to the deleted collection. +- Support checking the cache for payloads in the FETCH command. +- Add infrastructure to track client capabilities. +- Allow to disable the cache verification on retrieval. +- fsck: move orphaned pim items to lost+found, delete orphaned pim item flags. +- Introduce NotificationMessageV2 that supports batch operations on set of entities. +- Fix build with Boost >= 1.53. +- Fix a runtime issue with MySQL >= 5.6 (MySQL >= 5.1.3 is now the minimum version). + +1.9.2 05-May-2013 +--------------------------------------------- +- Add option to FETCH to ignore external retrieval failures. +- Properly restrict external payload removal. +- Add buildsystem option to choose between Qt4 and Qt5. + +1.9.1 02-March-2013 +--------------------------------------------- +- Disable query cache for Sqlite. +- Handle missing mysqld better. +- Ignore my.cnf settings when using the internal MySQL server. + +1.9.0 23-December-2012 +--------------------------------------------- +- Respect collection cache policy refresh interval for collection tree sync. +- Fix initialization of PostgreSQL database. +- Correctly count items flags in virtual collections. +- Notify parent virtual collections about item changes. +- Require CMake >= 2.8.8. +- Remove dependency to Automoc4. +- Support Qt 5. + +1.8.80 12-November-2012 +--------------------------------------------- +- Recover from lost external payload files. +- Improve the virtual collections handling. +- Notify clients about database schema updates. +- Reduce item access time updates. +- Make use of referential integrity if supported by the database backend. +- Add prepared query cache. +- Many code and queries optimizations. + +1.8.1 14-October-2012 +--------------------------------------------- +- Fix payload loss on some move/copy scenarios. +- Improve error reporting for failed item retrievals. + +1.8.0 25-July-2012 +--------------------------------------------- +- Fix deadlock in ad-hoc Nepomuk searches. + +1.7.95 11-July-2012 +--------------------------------------------- +- Fix Nepomuk queries getting stuck if Nepomuk service crashes. +- Fix unecessary remote retrieval of already cached item parts. +- Reset RID/RREV during cross-resource collection moves. +- Increase timeout for remote item retrieval. + +1.7.90 08-June-2012 +--------------------------------------------- +- Fix handling of large SPARQL queries. +- Support cleanup of orphaned resources in the consistency checker. +- Support compilation with Clang. + +1.7.2 31-March-2012 +--------------------------------------------- +- Fix and optimize searching via Nepomuk. + +1.7.1 03-March-2012 +--------------------------------------------- +- Don't truncate SPARQL queries in virtual collections. +- Optimize change notifications for deleted collection attributes. +- Fix possible data loss during item copy/move operations. + +1.7.0 23-January-2012 +--------------------------------------------- +- Fix search result retrieval from Nepomuk. + +1.6.90 20-December-2011 +--------------------------------------------- +- Support for PostgreSQL >= 9. +- Improve RFC 3501 compatibility in LOGIN and non-silent SELECT commands. +- Add support for running multiple instance concurrently in the same user session. +- Update agent interface to include collectionTreeSynchronized signal. +- Add consistency checker system. +- Add support for database vacuuming. +- Various optimizations to reduce the number of SQL queries. + +1.6.2 03-October-2011 +--------------------------------------------- +- Do not update item revision if only the RID or RREV changed. +- Fix usage of wrong ids for part filenames. +- Only set item dirty flag if the payload changed. +- Only drop content mimetype for unsubscribed collections in LIST/LSUB. + +1.6.1 15-September-2011 +--------------------------------------------- +- Fix crash on agent launcher exit. +- Fix valgrind-ing agents running in the agent launcher. +- Fix restarting of agents in broken state. +- Fix pipe naming on multi-user Windows systems. +- Raise MySQL timeout. + +1.6.0 10-July-2011 +--------------------------------------------- +- Enable external payload storage unconditionally. +- Treat single UID/RID fetches as error if the result set is empty. + +1.5.80 21-May-2011 +--------------------------------------------- +- WinCE database performance improvements. +- Include destination resource in move notifications. +- Fix crash in protocol parser. +- Fix possible race on accessing table caches. +- Use QStringBuilder if available. +- Improved notification message API. + +1.5.3 07-May-2011 +--------------------------------------------- +- Fix crash when copying collections into themselves. + +1.5.2 05-April-2011 +--------------------------------------------- +- Fix XdgBaseDirs reporting duplicated paths. +- Use correct database name when using internal MySQL. + +1.5.1 28-February-2011 +--------------------------------------------- +- Unbreak searching with Nepomuk 4.6. + +1.5.0 22-January-2011 +--------------------------------------------- +- Fix Boost related build issues on Windows. +- Hide akonadi_agent_launcher from Mac OS X dock. + +1.4.95 07-January-2011 +--------------------------------------------- +- Optimize notification compression. +- Consider ignore flag when calculating collection statistics. +- Fix item payload size calculation. +- Improved FETCH response order heuristic. +- Fix Strigi-based persistent search folders. +- Fix error propagation in FETCH command handler. + +1.4.90 20-December-2010 +--------------------------------------------- +- Set agent status for crashed instances. +- Allow to restart crashed agent instances. +- Automatically recover from loss of the resource table. +- Allow to specify the query language in persistent search commands. +- Fix leak of notification sources. + +1.4.85 18-December-2010 +--------------------------------------------- +- Fix agent server startup race. +- Allow to globally enable/disable the agent server. +- Fix autostart of agents running in the agent server. +- Fix agent configuration when running in the agent server. +- Fix agent server shutdown crash. +- Put sockets into /tmp to support AFS/NFS home directories. +- Fix access rights on persistent search folders. +- Add support for sub-collection tree syncs in resource interface. + +1.4.80 21-November-2010 +--------------------------------------------- +- Experimental support for MeeGo. +- Return changed revision numbers in STORE response. +- Fix Nepomuk searches mixing up items and email attachments. +- Experimental Strigi search backend. +- Compensate for Nepomuk D-Bus API breakage. +- Fix parsing of serialization format version. +- Optimize collection statistics queries. +- Optimize protocol output generation. +- Optimize protocol parsing. +- Build-time configurable default database backend. +- Fix ancestor chain quoting. +- Fix finding of components on Windows in install location. +- New subscription interface for change notifications. +- Support for in-process agents and agent server. +- Support for Sqlite. +- Experimental support for ODBC-based database backends. +- Support Windows CE. + +1.4.1 22-October-2010 +--------------------------------------------- +- Improve range query performance. +- Fix MySQL database upgrade happening too early. +- Fix MySQL database upgrade setting wrong priviledges. +- Fix non-index access slowing down server startup. +- ASAP parser performance optimizations +- Respect SocketDirectory setting also for database sockets. +- Allow $USER placeholder in SocketDirectory setting. +- Fix ASAP parser failing on non-zero serialization format versions. + +1.4.0 31-July-2010 +--------------------------------------------- +- Add change notification for collection subscription state changes. +- Enable filesystem payload store by default. +- Fix unicode folder name encoding regression. + +1.3.90 04-July-2010 +--------------------------------------------- +- Reset RIDs on inter-resource moves. +- Optimize disk space usage with internal MySQL. +- Improve error reporting of the Akonadi remote debugging server. +- Fix moving collections into the collection root. +- Report PostgreSQL database errors in english independent of locale settings. +- Fix unicode collection name encoding. +- Optimize cache pruning with filesystem payload store. +- Fix automatic migration between database and filesystem payload store. + +1.3.85 09-June-2010 +--------------------------------------------- +- Avoid unneeded full resource sync when using sync-on-demand cache policies. +- Fix crash when using D-Bus session bus in a secondary thread. +- Reduce emission of unneccessary change notifications. +- Fix empty filename use in fs backend. + +1.3.80 27-May-2010 +--------------------------------------------- +- Fix unicode collection name encoding. +- Support HRID-based FETCH commands. +- Fix Nepomuk-based persistent searches when Nepomuk was not running during Akonadi startup. +- Fix compilation on Windows CE. +- Optimize item retrieval queries. +- Support modification of existing persistent searches. +- Support different query languages for persistent searches. +- Fix PostgreSQL shutdown. +- Add initial support for Sqlite. +- Fix premature command abortion. +- Fix parsing of cascaded lists. +- Support for mysql_update_db. +- Support for mysql_install_db. +- Improved protocol tracing for akonadiconsole. +- Support MySQL backend on Maemo. +- Allow RID changes only to the owning resource. +- Add Akonadi remote debugging server. +- Add support for marking chaced payloads as invalid. +- Add support for remove revision property. +- Fix MySQL connection loss after 8 hours of inactivity. +- Fix D-Bus race on server startup. +- Fix internal MySQL on Windows. +- Fix config and data file location on Windows. +- Fix PostgreSQL startup when using internal server. +- Refactor database configuration abstraction. + +1.3.1 09-February-2010 +--------------------------------------------- +- Fix D-Bus connection leak in Nepomuk search backend. +- Disable slow query logging by default for internal MySQL. + +1.3.0 20-January-2010 +--------------------------------------------- +- Work around D-Bus bug that could cause SEARCH to hang. + +1.2.90 06-January-2010 +--------------------------------------------- +- Fix change notifications for search results. +- Fix database creation with PostgreSQL. +- Fix copying of item flags. +- Fix internal MySQL shutdown. +- Support PostgreSQL in internal mode. +- Fix table name case mismatch. + +1.2.80 01-December-2009 +--------------------------------------------- +- Support for collection content type filtering as part of LIST. +- Adapt to Nepomuk query service changes. +- Experimental support for PostgreSQL. +- Support for preprocessor agents. +- Support for distributed searching. +- Support for agents creating virtual collections. +- Protocol parser fixes for non-Linux/non-KDE clients. +- Support for single-shot searches using the Nepomuk query service. +- Support HRID-based LIST operations. +- Support RID-based MOVE, COLMOVE, LINK and UNLINK opertions. +- Respect cache-only retrieval also regarding on-demand syncing. +- Add configuration accepted/rejected signals to the agent interface. +- Fix change notification compression when using modified parts sets. +- Use one retrieval pipeline per resource. +- Reduce unecessary change notification on flag changes. +- Fix RID quoting. +- Fix resource creating race for autostarted agents. +- Create new database also when using external db servers. +- Return the created result collection when creating a persistent search. + +1.2.1 28-August-2009 +--------------------------------------------- +- Fix item creation with RID's containing a ']'. +- Fix ASAP parser not reading the entire command. + +1.2.0 28-June-2009 +--------------------------------------------- +- Fix attribute joining in collection list results. +- Buildsystem fixes for Mac OS. +- Do not show a console window for akonadi_control on Windows. + +1.1.95 23-June-2009 +--------------------------------------------- +- Fix item size handling. +- Add support for retrieving collection statistics as part + of the AKLIST/AKLSUB commands. +- Add support for collection size statistics. +- Build fixes for Windows. +- Support RID-based operations for CREATE, MODIFY and DELETE. +- Avoid emitting unecessary change notifications when + modifying items or collections. +- Add COLMOVE command. +- Reduce number of database writes when modifying a collection. +- Fix parsing of attributes containing CR or LF characters. + +1.1.90 03-June-2009 +--------------------------------------------- +- Return the storage location for items in FETCH responses +- Fix remode identifier encoding problems +- Fix infinite loop when parsing RID lists +- Fix parsing errors on stray newlines +- Support RID-based operations for STORE and MOVE +- Fix race on resource creation +- Provide modified item parts in change notifications +- Build system fixes + +1.1.85 05-May-2009 +--------------------------------------------- +- Improved CMake scripts so it is possible to detect + the Akonadi version in projects that depend on it. +- Simplified the check for existance of tables. +- Add a dedicated item deletion command, to get rid of + the old STORE/EXPUNGE which was extremely inefficient. +- Some fixes to support sqlite in the future. +- Soprano is required now. +- Qt 4.5.0 is required now. +- Support for collection retrieval by remote identifier. +- Support for item retrieval based on the remote identifier. +- Less useless debug output. +- Fixed leak on socket error. +- Various smaller bug fixes, see ChangeLog for a list. +- Support for writing large payloads to a file. +- New Item retrieval code. +- Added a streaming IMAP parser, and ported code the use it. +- Add support for manually restarting an agent instance. + +1.1.2 30-Apr-2009 +--------------------------------------------- +- Avoid DBUS lockups, reported at: https://bugs.kde.org/182198 +- Update user mysql.conf only if global/local one's are newer + +1.1.1 21-Jan-2009 +--------------------------------------------- +- Fix code that was not executed in a release build. +- Require CMake 2.6.0 which fixes boost detection. +- Don't try to restart an agent that has been deleted. + +1.1.0 03-Jan-2009 +--------------------------------------------- +- Restart agents when their executable changed. +- Buildsystem fixes to find and link boost on all platforms. +- Improvements to the startup to prevent partial startup. +- Include revision number in the version string when building from SVN. +- Shut down when we lost the connection to the D-Bus session bus. +- add some basic handling of command line args. +- Add a D-Bus call to flush the notification queue. +- Automatically fix world-writeable mySQL config files. +- Fix for FreeBSD mysql path. + +1.0.81 16-Dec-2008 +--------------------------------------------- +- Restore protocol backward compatibility with Akonadi 1.0.x servers. +- Build system fixes. +- Fix compiler warnings. +- Fall back to the default server path if the configured one points + to a non-existing file. + +1.0.80 19-Nov-2008 +--------------------------------------------- +- Query agent status information asynchronously and answer all queries from + cached values, reduces the risk of an agents blocking the Akonadi server. +- Increase mysql limits to more realistical values. +- Don't mark all new items as recent. +- Changes so it can store the size of an item. +- Better error detection. +- Prevent translated month names in the protocol. +- Some build fixes. +- Handle multiline output correctly. +- Terminate the control process when the server process failed to start. +- Add the ability to debug or valgrind a resource right from the + beginning, similar to the way this can be done with KIO slaves. +- Fix fetching of linked items in arbitrary collections. +- Add notification support for item references in virtual collections. +- Add LINK/UNLINK commands to edit references to items in virtual collections. +- Add a way to notify agents that their configuration has been changed remotely. +- Make sure that all modification times are stored in UTC time zone. +- Unquoted date time with a lenght of 26 characters was not parsed properly. +- Add serverside timestamp support for items. + +1.0.0 22-July-2008 +--------------------------------------------- +- First official stable release +- Bugfix: Unquoted date time with a lenght of + 26 characters was not parsed properly. +- Add serverside timestamp support for items. +- Build system fixes (windows & automoc) + +0.82.0 18-June-2008 +--------------------------------------------- +- Several build and installation fixes for windows and mac. +- Some improvements in the build system. +- Add item part namespaces. +- Implemented all the fetch modes advertised in ItemFetchScope. +- Notify already running clieants about all found types during startup. + +0.81.0 10-May-2008 +--------------------------------------------- +- Fix bug where full part was not fetched when a partial part was available already. +- Collection parsing optimalisation. +- Optimization for quoted string parsing. +- Use org.freedesktop namespace, instead of org.kde for the dbus interfaces. +- Add support for version numbers for database and protocol. +- Fixed foreach misusage. +- Depend on external automoc package instead of a copy. + +0.80.0 24-Apr-2008 +--------------------------------------------- +- Initial release diff --git a/README.md b/README.md new file mode 100644 index 0000000..1deb4b7 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Akonadi # + +Akonadi aims to be an extensible cross-desktop storage service for PIM data +and meta data providing concurrent read, write, and query access. +It provides unique desktop-wide object identification and retrieval. + +Akonadi framework provides two parts: the server, and client libraries to +access the data managed by the server. + +## Client Libraries ## + +If you are an application developer and want to access and interact with data +stored in Akonadi, you should read the [Akonadi client libraries documentation](@ref client_libraries). + +## Akonadi Server ## + +If you are interested in working on the Akonadi framework itself, you can read +more about the internals of the Akonadi Server and the protocol in the +[Akonadi server documentation](@ref server). + +## History ## + +You can also read a bit more about [history of Akonadi](@ref history) + +## Implementation details ## + +* [Tags](@ref tags) diff --git a/README.sqlite b/README.sqlite new file mode 100644 index 0000000..5ce5774 --- /dev/null +++ b/README.sqlite @@ -0,0 +1,54 @@ +== PREFACE == + +The reason we have our own QtSql Sqlite driver here is because the one shipped +with Qt misses a bunch of multi-threading fixes crucial for Akonadi. Of course, +these changes should be pushed upstream eventually. + +== INSTALL == +When Sqlite is found, the custom driver will be build and installed in: + +${CMAKE_INSTALL_PREFIX}/lib/plugins/sqldrivers or the sqldrivers subdirectory +of the default Qt plugins path specified at Qt build time if you enable the +KDE_INSTALL_USE_QT_SYS_PATHS option when running CMake. + +The next thing you have to do is add that path (if it isn't already) to your +QT_PLUGIN_PATH environment variable. + +Now you should be able to configure the QSQLITE3 driver in akonadiserverrc. + +== PROBLEMS == + +One of the problematic code paths seems to be: + +server/src/handler/fetch.cpp:161-201 + +In this part the code is iterating over the results of an still active SELECT +query and during this iteration it also tries to do INSERT/UPDATE queries. This +means that there is probably a SHARED lock for the reading and a PENDING lock +for the writing queries. For sqlite to be able to write to the db it needs an +EXCLUSIVE lock. A PENDING lock only gets inclusive when all SHARED locks are +gone. + +A possible solution might be to store the results of the SELECT query into +memory, close the query and than start the inserts/updates. + + +== SQLITE INFO == + +From www.sqlite.org: (see qsqlite/src/qsql_sqlite.cpp:525-529) +Run-time selection of threading mode + +If single-thread mode has not been selected at compile-time or start-time, then +individual database connections can be created as either multi-thread or +serialized. It is not possible to downgrade an individual database connection +to single-thread mode. Nor is it possible to escalate an individual database +connection if the compile-time or start-time mode is single-thread. + +The threading mode for an individual database connection is determined by flags +given as the third argument to sqlite3_open_v2(). The SQLITE_OPEN_NOMUTEX flag +causes the database connection to be in the multi-thread mode and the +SQLITE_OPEN_FULLMUTEX flag causes the connection to be in serialized mode. If +neither flag is specified or if sqlite3_open() or sqlite3_open16() are used +instead of sqlite3_open_v2(), then the default mode determined by the +compile-time and start-time settings is used. + diff --git a/akonadi-mime.xml b/akonadi-mime.xml new file mode 100644 index 0000000..599a75b --- /dev/null +++ b/akonadi-mime.xml @@ -0,0 +1,32 @@ + + + + + + + iCal Calendar Event Component + + + + iCal Calendar FreeBusy Component + + + + iCal Calendar Journal Component + + + + iCal Calendar TODO Component + + + Virtual Akonadi Collection + + diff --git a/akonadifull-version.h.cmake b/akonadifull-version.h.cmake new file mode 100644 index 0000000..8a6d18e --- /dev/null +++ b/akonadifull-version.h.cmake @@ -0,0 +1,7 @@ +#ifndef AKONADIFULL_VERSION_H +#define AKONADIFULL_VERSION_H + + +#define AKONADI_FULL_VERSION "@AKONADI_FULL_VERSION@" + +#endif diff --git a/apparmor/CMakeLists.txt b/apparmor/CMakeLists.txt new file mode 100644 index 0000000..ae89768 --- /dev/null +++ b/apparmor/CMakeLists.txt @@ -0,0 +1,2 @@ + +install(FILES usr.bin.akonadiserver mariadbd_akonadi mysqld_akonadi postgresql_akonadi DESTINATION ${KDE_INSTALL_SYSCONFDIR}/apparmor.d) diff --git a/apparmor/mariadbd_akonadi b/apparmor/mariadbd_akonadi new file mode 100644 index 0000000..8c93fbf --- /dev/null +++ b/apparmor/mariadbd_akonadi @@ -0,0 +1,39 @@ +#include + +@{xdg_data_home}=@{HOME}/.local/share + +profile mariadbd_akonadi { + #include + #include + #include + #include + #include + #include + + capability setgid, + capability setuid, + + signal receive set=kill peer=/usr/bin/akonadiserver, + signal receive set=term peer=/usr/bin/akonadiserver, + + /etc/mysql/ r, + /etc/mysql/** r, + /etc/my.cnf{,.d/**} r, + @{sys}/devices/system/cpu/ r, + /{usr/,}bin/{b,d}ash mrix, + /{usr/,}bin/cat mrix, + /{usr/,}bin/chmod mrix, + /{usr/,}bin/dirname mrix, + /{usr/,}bin/hostname mrix, + /{usr/,}bin/mkdir mrix, + /{usr/,}bin/sed mrix, + /usr/bin/my_print_defaults mrix, + /usr/bin/mariadb-install-db mrix, + /usr/bin/mariadb-admin mrix, + /usr/bin/mariadb-check mrix, + /usr/{,s}bin/mariadbd mrix, + /usr/share/mysql/** r, + owner @{xdg_data_home}/akonadi/** rwk, + owner @{PROC}/@{pid}/loginuid r, + owner /{,var/}run/user/@{uid}/akonadi** rwk, +} diff --git a/apparmor/mysqld_akonadi b/apparmor/mysqld_akonadi new file mode 100644 index 0000000..224b5b0 --- /dev/null +++ b/apparmor/mysqld_akonadi @@ -0,0 +1,39 @@ +#include + +@{xdg_data_home}=@{HOME}/.local/share + +profile mysqld_akonadi { + #include + #include + #include + #include + #include + #include + + capability setgid, + capability setuid, + + signal receive set=kill peer=/usr/bin/akonadiserver, + signal receive set=term peer=/usr/bin/akonadiserver, + + /etc/mysql/ r, + /etc/mysql/** r, + /etc/my.cnf{,.d/**} r, + @{sys}/devices/system/cpu/ r, + /{usr/,}bin/{b,d}ash mrix, + /{usr/,}bin/cat mrix, + /{usr/,}bin/chmod mrix, + /{usr/,}bin/dirname mrix, + /{usr/,}bin/hostname mrix, + /{usr/,}bin/mkdir mrix, + /{usr/,}bin/sed mrix, + /usr/bin/my_print_defaults mrix, + /usr/bin/mysql_install_db mrix, + /usr/bin/mysqladmin mrix, + /usr/bin/mysqlcheck mrix, + /usr/{,s}bin/mysqld mrix, + /usr/share/mysql/** r, + owner @{xdg_data_home}/akonadi/** rwk, + owner @{PROC}/@{pid}/loginuid r, + owner /{,var/}run/user/@{uid}/akonadi** rwk, +} diff --git a/apparmor/postgresql_akonadi b/apparmor/postgresql_akonadi new file mode 100644 index 0000000..4263ee6 --- /dev/null +++ b/apparmor/postgresql_akonadi @@ -0,0 +1,41 @@ +#include + +@{xdg_data_home}=@{HOME}/.local/share + +profile postgresql_akonadi { + #include + #include + #include + #include + #include + + capability setgid, + capability setuid, + + signal receive set=kill peer=/usr/bin/akonadiserver, + signal receive set=term peer=/usr/bin/akonadiserver, + + /etc/passwd r, + /{usr/,}bin/{b,d}ash mrix, + /{usr/,}bin/locale mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/initdb mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_ctl mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/postgres mrix, + /usr/share/postgresql/** r, + owner /dev/shm/PostgreSQL.* rw, + owner @{xdg_data_home}/akonadi/** rwlk, + owner @{xdg_data_home}/akonadi/db_data/** l, + owner /{,var/}run/user/@{uid}/akonadi** rwk, + + # pg_upgrade + /{usr/,usr/lib/postgresql/*/}bin/pg_upgrade mrix, + /opt/pgsql*/** mr, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_controldata mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_resetwal mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_dumpall mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_dump mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/vacuumdb mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/psql mrix, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_restore mrix, + /{usr/,}bin/cp mrix, +} diff --git a/apparmor/usr.bin.akonadiserver b/apparmor/usr.bin.akonadiserver new file mode 100644 index 0000000..7a6ba08 --- /dev/null +++ b/apparmor/usr.bin.akonadiserver @@ -0,0 +1,80 @@ +#include + +@{xdg_data_home}=@{HOME}/.local/share + +@{xdg_config_home}=@{HOME}/.config + +/usr/bin/akonadiserver { + #include + #include + #include + #include + #include + #include + #include + + signal send set=kill peer=mysqld_akonadi, + signal send set=term peer=mysqld_akonadi, + ptrace read peer=mysqld_akonadi, + + signal send set=kill peer=mariadbd_akonadi, + signal send set=term peer=mariadbd_akonadi, + ptrace read peer=mariadbd_akonadi, + + signal send set=kill peer=postgresql_akonadi, + signal send set=term peer=postgresql_akonadi, + + dbus (send) + bus=session + interface=org.freedesktop.DBus, + dbus bind + bus=session + name=org.freedesktop.Akonadi, + dbus (receive, send) + bus=session + interface=org.freedesktop.Akonadi**, + + /etc/xdg/** r, + /usr/bin/akonadiserver mr, + /usr/lib/x86_64-linux-gnu/libexec/drkonqi PUx, + /usr/bin/mariadb-admin PUx -> mariadbd_akonadi, + /usr/bin/mariadb-check PUx -> mariadbd_akonadi, + /usr/bin/mariadb-install-db PUx -> mariaddbd_akonadi, + /usr/{,s}bin/mariadbd PUx -> mariaddbd_akonadi, + /usr/bin/mysql_install_db PUx -> mysqld_akonadi, + /usr/bin/mysqladmin PUx -> mysqld_akonadi, + /usr/bin/mysqlcheck PUx -> mysqld_akonadi, + /usr/{,s}bin/mysqld PUx -> mysqld_akonadi, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/initdb PUx -> postgresql_akonadi, + /{usr/,usr/lib/postgresql/*/,opt/pgsql*/}bin/pg_ctl PUx -> postgresql_akonadi, + /{usr/,usr/lib/postgresql/*/}bin/pg_upgrade PUx -> postgresql_akonadi, + /usr/sbin/mysqld PUx -> mysqld_akonadi, + /usr/share/mime/mime.cache r, + /usr/share/mime/packages/ r, + /usr/share/mime/types r, + /usr/share/qt/translations/* r, + /usr/share/mysql/** r, + @{PROC}/sys/kernel/core_pattern r, + @{PROC}/sys/kernel/random/boot_id r, + owner @{xdg_config_home}/* r, + owner @{xdg_config_home}/akonadi* rw, + owner @{xdg_config_home}/QtProject/qtlogging.ini r, + owner @{xdg_config_home}/akonadi/ rw, + owner @{xdg_config_home}/akonadi/** rwl, + owner @{xdg_config_home}/akonadi/akonadiconnectionrc wl, + owner @{xdg_config_home}/akonadi/akonadiconnectionrc.lock rwk, + owner @{xdg_config_home}/akonadi/akonadiserverrc.lock rwk, + owner @{xdg_data_home}/mime/mime.cache r, + owner @{xdg_data_home}/mime/packages/ r, + owner @{xdg_data_home}/mime/types r, + owner @{xdg_data_home}/akonadi/ rw, + owner @{xdg_data_home}/akonadi/* rwlk, + owner @{xdg_data_home}/akonadi/** rwk, + owner @{PROC}/@{pid}/loginuid r, + owner @{PROC}/@{pid}/mounts r, + owner @{PROC}/[0-9]*/stat r, + owner /{,var/}run/user/@{uid}/akonadi** rwk, + owner /{,var/}run/user/@{uid}/kdeinit** rwk, + owner /{,var/}run/user/@{uid}/kcrash** rwk, + owner /tmp/#[0-9]* m, +} diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt new file mode 100644 index 0000000..750ee2d --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,48 @@ +find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Test DBus) + +if(${EXECUTABLE_OUTPUT_PATH}) + set(PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH}) +else() + set(PREVIOUS_EXEC_OUTPUT_PATH .) +endif() +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) +set(TEST_RESULT_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/testresults) +file(MAKE_DIRECTORY ${TEST_RESULT_OUTPUT_PATH}) + +option(AKONADI_TESTS_XML "Use XML files for the test results, instead of plain text." FALSE) +option(AKONADI_RUN_SQLITE_ISOLATED_TESTS "Run isolated tests with sqlite3 as backend" TRUE) +option(AKONADI_RUN_MYSQL_ISOLATED_TESTS "Run isolated tests with MySQL as backend" TRUE) +option(AKONADI_RUN_PGSQL_ISOLATED_TESTS "Run isolated tests with PostgreSQL as backend" TRUE) + +kde_enable_exceptions() + +# convenience macro to add akonadi qtestlib unit-tests +macro(add_akonadi_test _source) + set(_test ${_source} ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp) + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} ${_test}) + set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2") + target_link_libraries(${_name} akonaditestfake Qt::Test KF5::AkonadiPrivate KF5::I18n) +endmacro() + +# convenience macro to add akonadi qtestlib unit-tests +macro(add_akonadi_test_widgets _source) + set(_test + ${_source} + ${CMAKE_BINARY_DIR}/src/widgets/akonadiwidgets_debug.cpp + ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp + ) + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} ${_test}) + set_tests_properties(${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2") + target_link_libraries(${_name} akonaditestfake Qt::Test KF5::AkonadiWidgets KF5::AkonadiPrivate) +endmacro() + +include(../KF5AkonadiMacros.cmake) + +add_subdirectory(private) +add_subdirectory(server) +add_subdirectory(libs) +add_subdirectory(akonadicontrol) +add_subdirectory(shared) +add_subdirectory(widgets) diff --git a/autotests/akonadicontrol/CMakeLists.txt b/autotests/akonadicontrol/CMakeLists.txt new file mode 100644 index 0000000..9f2b99b --- /dev/null +++ b/autotests/akonadicontrol/CMakeLists.txt @@ -0,0 +1,27 @@ +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +macro(add_unit_test _source) + set(_test ${_source} + ${Akonadi_BINARY_DIR}/src/akonadicontrol/akonadicontrol_debug.cpp + ${Akonadi_SOURCE_DIR}/src/akonadicontrol/agenttype.cpp + ) + + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} NAME_PREFIX "AkonadiControl-" ${_test}) + add_dependencies(${_name} akonadi_control) + target_include_directories(${_name} BEFORE PRIVATE $) + if (ENABLE_ASAN) + set_tests_properties(AkonadiControl-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + target_link_libraries(${_name} + akonadi_shared + KF5AkonadiPrivate + Qt::Test + KF5::ConfigCore + ${CMAKE_EXE_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_unit_test(agenttypetest.cpp) diff --git a/autotests/akonadicontrol/agenttypetest.cpp b/autotests/akonadicontrol/agenttypetest.cpp new file mode 100644 index 0000000..d187faf --- /dev/null +++ b/autotests/akonadicontrol/agenttypetest.cpp @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2016 Elvis Angelaccio + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include +#include + +#include + +Q_DECLARE_METATYPE(AgentType) + +class AgentTypeTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testLoad_data(); + void testLoad(); +}; + +void AgentTypeTest::testLoad_data() +{ + AgentType googleContactsResource; + googleContactsResource.exec = QStringLiteral("akonadi_googlecontacts_resource"); + googleContactsResource.mimeTypes = QStringList{QStringLiteral("text/directory"), QString()}; + googleContactsResource.capabilities = QStringList{AgentType::CapabilityResource}; + googleContactsResource.instanceCounter = 0; + googleContactsResource.identifier = QStringLiteral("akonadi_googlecontacts_resource"); + googleContactsResource.custom = + QVariantMap{{QStringLiteral("KAccounts"), QStringList{QStringLiteral("google-contacts"), QStringLiteral("google-calendar")}}, + {QStringLiteral("HasLocalStorage"), true}}; + googleContactsResource.launchMethod = AgentType::Process; + // We test an UTF-8 name within quotes. + googleContactsResource.name = QStringLiteral("\"Контакти Google\""); + // We also check whether an unquoted string with a comma is not parsed as a QStringList. See bug #330010 + googleContactsResource.comment = QStringLiteral("Доступ до ваших записів контактів, Google з KDE"); + googleContactsResource.icon = QStringLiteral("im-google"); + + QTest::addColumn("fileName"); + QTest::addColumn("expectedAgentType"); + + QTest::newRow("google contacts resource") << QFINDTESTDATA("data/akonaditestresource.desktop") << googleContactsResource; +} + +void AgentTypeTest::testLoad() +{ + QFETCH(QString, fileName); + QFETCH(AgentType, expectedAgentType); + + AgentType agentType; + QLocale::setDefault(QLocale::Ukrainian); + QVERIFY(agentType.load(fileName, nullptr)); + + QCOMPARE(agentType.exec, expectedAgentType.exec); + QCOMPARE(agentType.mimeTypes, expectedAgentType.mimeTypes); + QCOMPARE(agentType.capabilities, expectedAgentType.capabilities); + QCOMPARE(agentType.instanceCounter, expectedAgentType.instanceCounter); + QCOMPARE(agentType.identifier, expectedAgentType.identifier); + QCOMPARE(agentType.custom, expectedAgentType.custom); + QCOMPARE(agentType.launchMethod, expectedAgentType.launchMethod); + QCOMPARE(agentType.name, expectedAgentType.name); + QCOMPARE(agentType.comment, expectedAgentType.comment); + QCOMPARE(agentType.icon, expectedAgentType.icon); +} + +AKTEST_MAIN(AgentTypeTest) + +#include "agenttypetest.moc" diff --git a/autotests/akonadicontrol/data/akonaditestresource.desktop b/autotests/akonadicontrol/data/akonaditestresource.desktop new file mode 100644 index 0000000..78f27da --- /dev/null +++ b/autotests/akonadicontrol/data/akonaditestresource.desktop @@ -0,0 +1,95 @@ +[Desktop Entry] +Name=Google Contacts +Name[bg]=Контакти в Google +Name[bs]=Google kontakti +Name[ca]=Contactes de Google +Name[ca@valencia]=Contactes de Google +Name[cs]=Kontakty Google +Name[da]=Google-kontakter +Name[de]=Google-Kontakte +Name[el]=Google Επαφές +Name[en_GB]=Google Contacts +Name[es]=Contactos Google +Name[et]=Google'i kontaktid +Name[fi]=Google-yhteystiedot +Name[fr]=Contacts Google +Name[ga]=Teagmhálacha Google +Name[gl]=Google Contacts +Name[hu]=Google névjegyek +Name[ia]=Contactos de Google +Name[it]=Contatti Google +Name[kk]=Google контакттары +Name[km]=ទំនាក់ទំនង Google +Name[ko]=Google 연락처 +Name[lt]=Google kontaktai +Name[lv]=Google kontakti +Name[nb]=Google-kontakter +Name[nds]=Google-Kontakten +Name[nl]=Google contactpersonen +Name[pl]=Kontakty Google +Name[pt]=Contactos do Google +Name[pt_BR]=Contatos do Google +Name[ru]=Контакты Google +Name[sk]=Google kontakty +Name[sl]=Stiki Google +Name[sr]=Гуглови контакти +Name[sr@ijekavian]=Гуглови контакти +Name[sr@ijekavianlatin]=Googleovi kontakti +Name[sr@latin]=Googleovi kontakti +Name[sv]=Google kontakter +Name[tr]=Google Kişileri +Name[ug]=Google ئالاقەداشلىرى +Name[uk]="Контакти Google" +Name[x-test]=xxGoogle Contactsxx +Name[zh_CN]=Google 联系人 +Name[zh_TW]=Google 聯絡人 +Comment=Access your Google Contacts from KDE +Comment[bg]=Достъп до контактите ви в Google от KDE +Comment[bs]=Pristupite svojim Google kontaktima iz KDE +Comment[ca]=Accediu als contactes de Google des del KDE +Comment[ca@valencia]=Accediu als contactes de Google des del KDE +Comment[da]=Tilgå dine Google-kontakter fra KDE +Comment[de]=Greifen Sie in KDE auf Google-Kontakte zu +Comment[el]=Αποκτήστε πρόσβαση στις Google επαφές σας από το KDE +Comment[en_GB]=Access your Google Contacts from KDE +Comment[es]=Acceda a sus contactos Google desde KDE +Comment[et]=Oma Google'i kontaktide kasutamine otse KDE-st +Comment[fi]=Google-yhteystietoihin pääsy KDE:sta +Comment[fr]=Accès à vos contacts Google depuis KDE +Comment[gl]=Acceda aos seus contactos de Google desde KDE. +Comment[hu]=A Google névjegyeinek elérése a KDE-ből +Comment[ia]=Accede a tu Contactos de Google ab KDE +Comment[it]=Accedi ai tuoi contatti Google da KDE +Comment[kk]=Google контакттарына KDE-ден қатынау +Comment[km]=ចូល​ដំណើរការ​ទំនាក់ទំនង Google របស់​អ្នក​ពី KDE +Comment[ko]=KDE에서 Google 연락처에 접근하기 +Comment[lt]=Pasiekite savo Google kontaktus iš KDE +Comment[lv]=Piekļūstiet saviem Google kontaktiem no KDE +Comment[nb]=Bruk dine Google-kontakter fra KDE +Comment[nds]=Ut KDE op Dien Google-Kontakten togriepen +Comment[nl]=Heb toegang tot uw Google contactpersonen vanuit KDE +Comment[pl]=Uzyskaj dostęp do Kontaktów Google z KDE +Comment[pt]=Aceda aos seus contactos da Google a partir do KDE +Comment[pt_BR]=Acesse seus contatos do Google a partir do KDE +Comment[ru]=Доступ к контактам Google из KDE +Comment[sk]=Pristupuje k vašim Google kontaktom z KDE +Comment[sl]=Dostopajte do svojih stikov Google +Comment[sr]=Приступите својим контактима на Гуглу из КДЕ‑а +Comment[sr@ijekavian]=Приступите својим контактима на Гуглу из КДЕ‑а +Comment[sr@ijekavianlatin]=Pristupite svojim kontaktima na Googleu iz KDE‑a +Comment[sr@latin]=Pristupite svojim kontaktima na Googleu iz KDE‑a +Comment[sv]=Kom åt Google kontakter från KDE +Comment[tr]=Google Kişilerinize KDE'den erişin +Comment[uk]=Доступ до ваших записів контактів, Google з KDE +Comment[x-test]=xxAccess your Google Contacts from KDExx +Comment[zh_CN]=在 KDE 中访问您的 Google 联系人 +Comment[zh_TW]=用 KDE 存取您的 Google 聯絡人 +Type=AkonadiResource +Exec=akonadi_googlecontacts_resource +X-Akonadi-MimeTypes=text/directory, +X-Akonadi-Capabilities=Resource +X-Akonadi-Identifier=akonadi_googlecontacts_resource +X-Akonadi-Custom-KAccounts=google-contacts,google-calendar +X-Akonadi-Custom-HasLocalStorage=true +Icon=im-google + diff --git a/autotests/libs/CMakeLists.txt b/autotests/libs/CMakeLists.txt new file mode 100644 index 0000000..a3db7d7 --- /dev/null +++ b/autotests/libs/CMakeLists.txt @@ -0,0 +1,122 @@ +# akonadi test fake library +set(akonaditestfake_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml) +set_source_files_properties(${akonaditestfake_xml} PROPERTIES INCLUDE "protocol_p.h") +qt_add_dbus_interface( akonaditestfake_srcs ${akonaditestfake_xml} notificationsourceinterface ) + +add_library(akonaditestfake SHARED + ${akonaditestfake_srcs} + fakeakonadiservercommand.cpp + fakesession.cpp + fakemonitor.cpp + fakeserverdata.cpp + modelspy.cpp + fakeentitycache.cpp + inspectablemonitor.cpp + inspectablechangerecorder.cpp +) + +generate_export_header(akonaditestfake BASE_NAME akonaditestfake) + +target_link_libraries(akonaditestfake + Qt::DBus + KF5::AkonadiCore + Qt::Test + Qt::Widgets + Qt::Network + KF5::AkonadiPrivate + akonadi_shared +) + +add_executable(akonadi-firstrun + ../../src/core/firstrun.cpp + firstrunner.cpp + ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp +) +target_link_libraries( akonadi-firstrun Qt::Test Qt::Core KF5::AkonadiCore KF5::AkonadiPrivate KF5::ConfigCore Qt::Widgets) + +add_akonadi_test(itemhydratest.cpp) +add_akonadi_test(itemtest.cpp) +add_akonadi_test(itemserializertest.cpp) +add_akonadi_test(mimetypecheckertest.cpp) +add_akonadi_test(protocolhelpertest.cpp) +add_akonadi_test(entitytreemodeltest.cpp) +add_akonadi_test(monitornotificationtest.cpp) +add_akonadi_test(collectionutilstest.cpp) +add_akonadi_test(collectioncolorattributetest.cpp) +add_akonadi_test(entitydisplayattributetest.cpp) +add_akonadi_test(proxymodelstest.cpp) +add_akonadi_test_widgets(actionstatemanagertest.cpp) +add_akonadi_test_widgets(conflictresolvedialogtest.cpp) +add_akonadi_test(tagmodeltest.cpp) +add_akonadi_test(statisticsproxymodeltest.cpp) + +add_akonadi_test(sharedvaluepooltest.cpp) +add_akonadi_test(jobtest.cpp) +add_akonadi_test(tagtest_simple.cpp) +add_akonadi_test(cachepolicytest.cpp) + +# PORT FROM QJSON add_akonadi_test(searchquerytest.cpp) + +# qtestlib tests that need non-exported stuff from +#add_executable( resourceschedulertest resourceschedulertest.cpp ../src/agentbase/resourcescheduler.cpp ) +#add_test( resourceschedulertest resourceschedulertest ) +#ecm_mark_as_test(akonadi-resourceschedulertest) +#target_link_libraries(resourceschedulertest Qt::Test KF5::AkonadiAgentBase) + + +# testrunner tests +add_akonadi_isolated_test(SOURCE testenvironmenttest.cpp) +add_akonadi_isolated_test(SOURCE autoincrementtest.cpp) +add_akonadi_isolated_test(SOURCE attributefactorytest.cpp) +add_akonadi_isolated_test(SOURCE collectionpathresolvertest.cpp) +add_akonadi_isolated_test(SOURCE collectionattributetest.cpp) +add_akonadi_isolated_test(SOURCE itemfetchtest.cpp) +add_akonadi_isolated_test(SOURCE itemappendtest.cpp) +add_akonadi_isolated_test(SOURCE itemstoretest.cpp) +add_akonadi_isolated_test(SOURCE itemdeletetest.cpp) +add_akonadi_isolated_test(SOURCE entitycachetest.cpp) +add_akonadi_isolated_test(SOURCE monitortest.cpp) +#add_akonadi_isolated_test_advanced(monitorfiltertest.cpp "" "KF5::AkonadiPrivate") +# FIXME: this is constantly failling due to broken search: re-enable once the new search code is merged +#add_akonadi_isolated_test(SOURCE searchjobtest.cpp) +add_akonadi_isolated_test(SOURCE changerecordertest.cpp) +add_akonadi_isolated_test(SOURCE resourcetest.cpp) +add_akonadi_isolated_test(SOURCE subscriptiontest.cpp) +add_akonadi_isolated_test(SOURCE transactiontest.cpp) +add_akonadi_isolated_test(SOURCE itemcopytest.cpp) +add_akonadi_isolated_test(SOURCE itemmovetest.cpp) +add_akonadi_isolated_test(SOURCE invalidatecachejobtest.cpp) +add_akonadi_isolated_test(SOURCE collectioncreatetest.cpp) +add_akonadi_isolated_test(SOURCE collectioncopytest.cpp) +add_akonadi_isolated_test(SOURCE collectionmovetest.cpp) +add_akonadi_isolated_test( + SOURCE collectionsynctest.cpp + ADDITIONAL_SOURCES ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp + LINK_LIBRARIES KF5::I18n +) +add_akonadi_isolated_test(SOURCE itemsynctest.cpp) +add_akonadi_isolated_test(SOURCE linktest.cpp) +add_akonadi_isolated_test(SOURCE cachetest.cpp) +add_akonadi_isolated_test(SOURCE collectionjobtest.cpp) +add_akonadi_isolated_test(SOURCE collectionmodifytest.cpp) + +# FIXME: This is very unstable on Jenkins +#add_akonadi_isolated_test(servermanagertest.cpp) + +# Having a benchmark is cool if you have any reference to compare against, but this +# benchmark takes over 40 seconds and does not have any real value to us atm. Major +# performance regressions would be spotted by devs anyway, so disabling for now. +#add_akonadi_isolated_test(itembenchmark.cpp) +#add_akonadi_isolated_test(collectioncreator.cpp) + +add_akonadi_isolated_test(SOURCE gidtest.cpp) +add_akonadi_isolated_test(SOURCE lazypopulationtest.cpp) +add_akonadi_isolated_test(SOURCE favoriteproxytest.cpp LINK_LIBRARIES KF5::ConfigCore) +# FIXME: this is constantly failing due to broken search: re-enable once the new search code is merged +#add_akonadi_isolated_test( +# SOURCE itemsearchjobtest.cpp +# ADDITIONAL_SOURCES testsearchplugin/testsearchplugin.cpp) +add_akonadi_isolated_test(SOURCE tagtest.cpp ADDITIONAL_SOURCES ${CMAKE_BINARY_DIR}/src/core/akonadicore_debug.cpp) +add_akonadi_isolated_test(SOURCE tagsynctest.cpp) +add_akonadi_isolated_test(SOURCE relationtest.cpp) +add_akonadi_isolated_test(SOURCE etmpopulationtest.cpp) diff --git a/autotests/libs/actionstatemanagertest.cpp b/autotests/libs/actionstatemanagertest.cpp new file mode 100644 index 0000000..c3b581a --- /dev/null +++ b/autotests/libs/actionstatemanagertest.cpp @@ -0,0 +1,626 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include + +#include "../src/widgets/actionstatemanager_p.h" +#include "../src/widgets/standardactionmanager.h" + +#define QT_NO_CLIPBOARD // allow running without GUI +#include "../src/widgets/actionstatemanager.cpp" +#undef QT_NO_CLIPBOARD + +using StateMap = QHash; +Q_DECLARE_METATYPE(StateMap) + +using namespace Akonadi; + +class ActionStateManagerTest; + +class UnitActionStateManager : public ActionStateManager +{ +public: + explicit UnitActionStateManager(ActionStateManagerTest *receiver); + +protected: + bool hasResourceCapability(const Collection &collection, const QString &capability) const override; + +private: + ActionStateManagerTest *mReceiver = nullptr; +}; + +class ActionStateManagerTest : public QObject +{ + Q_OBJECT + +public: + ActionStateManagerTest() + { + rootCollection = Collection::root(); + const QString dummyMimeType(QStringLiteral("text/dummy")); + + resourceCollectionOne.setId(1); + resourceCollectionOne.setName(QStringLiteral("resourceCollectionOne")); + resourceCollectionOne.setRights(Collection::ReadOnly); + resourceCollectionOne.setParentCollection(rootCollection); + resourceCollectionOne.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionOne.setId(10); + folderCollectionOne.setName(QStringLiteral("folderCollectionOne")); + folderCollectionOne.setRights(Collection::ReadOnly); + folderCollectionOne.setParentCollection(resourceCollectionOne); + folderCollectionOne.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + resourceCollectionTwo.setId(2); + resourceCollectionTwo.setName(QStringLiteral("resourceCollectionTwo")); + resourceCollectionTwo.setRights(Collection::AllRights); + resourceCollectionTwo.setParentCollection(rootCollection); + resourceCollectionTwo.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionTwo.setId(20); + folderCollectionTwo.setName(QStringLiteral("folderCollectionTwo")); + folderCollectionTwo.setRights(Collection::AllRights); + folderCollectionTwo.setParentCollection(resourceCollectionTwo); + folderCollectionTwo.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + resourceCollectionThree.setId(3); + resourceCollectionThree.setName(QStringLiteral("resourceCollectionThree")); + resourceCollectionThree.setRights(Collection::AllRights); + resourceCollectionThree.setParentCollection(rootCollection); + resourceCollectionThree.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionThree.setId(30); + folderCollectionThree.setName(QStringLiteral("folderCollectionThree")); + folderCollectionThree.setRights(Collection::AllRights); + folderCollectionThree.setParentCollection(resourceCollectionThree); + folderCollectionThree.setContentMimeTypes(QStringList() << Collection::mimeType() << dummyMimeType); + + folderCollectionThree.setId(31); + folderCollectionThree.setName(QStringLiteral("folderCollectionThreeOne")); + folderCollectionThree.setRights(Collection::AllRights); + folderCollectionThree.setParentCollection(resourceCollectionThree); + + mCapabilityMap.insert(QStringLiteral("NoConfig"), Collection::List() << resourceCollectionThree); + mFavoriteCollectionMap.insert(folderCollectionThree.id()); + } + + bool hasResourceCapability(const Collection &collection, const QString &capability) const + { + return mCapabilityMap.value(capability).contains(collection); + } + +public Q_SLOTS: + void enableAction(int type, bool enable) + { + mStateMap.insert(static_cast(type), enable); + } + + void updatePluralLabel(int type, int count) + { + Q_UNUSED(type) + Q_UNUSED(count) + } + + bool isFavoriteCollection(const Akonadi::Collection &collection) + { + return mFavoriteCollectionMap.contains(collection.id()); + } + + void updateAlternatingAction(int action) + { + Q_UNUSED(action) + } + +private Q_SLOTS: + + void init() + { + mStateMap.clear(); + } + + void testCollectionSelected_data() + { + QTest::addColumn("collections"); + QTest::addColumn("stateMap"); + + { + Collection::List collectionList; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, false); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, false); + map.insert(StandardActionManager::CollectionProperties, false); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, false); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, false); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, false); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("nothing selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << rootCollection; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, false); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, false); + map.insert(StandardActionManager::CollectionProperties, false); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, false); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, false); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, false); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("root collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << resourceCollectionOne; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, true); + map.insert(StandardActionManager::ResourceProperties, true); + map.insert(StandardActionManager::SynchronizeResources, true); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, true); + + QTest::newRow("read-only resource collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << resourceCollectionTwo; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, true); + map.insert(StandardActionManager::ResourceProperties, true); + map.insert(StandardActionManager::SynchronizeResources, true); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, true); + + QTest::newRow("writable resource collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << resourceCollectionThree; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, true); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, true); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, true); + + QTest::newRow("non-configurable resource collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionOne; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("read-only folder collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionTwo; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, true); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, true); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, true); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, true); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, true); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, true); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, true); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("writable folder collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionThree; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, true); + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, true); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, true); + map.insert(StandardActionManager::RenameFavoriteCollection, true); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, true); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, true); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, true); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, true); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, true); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("favorite writable folder collection selected") << collectionList << map; + } + + { + Collection::List collectionList; + collectionList << folderCollectionThreeOne; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); // content mimetype is missing + map.insert(StandardActionManager::CopyCollections, true); + map.insert(StandardActionManager::DeleteCollections, true); + map.insert(StandardActionManager::SynchronizeCollections, false); + map.insert(StandardActionManager::CollectionProperties, true); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); // content mimetype is missing + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, true); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, true); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, true); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, true); + map.insert(StandardActionManager::MoveCollectionToDialog, true); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, true); + map.insert(StandardActionManager::MoveCollectionsToTrash, true); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, true); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("structural folder collection selected") << collectionList << map; + } + + // multiple collections + { + Collection::List collectionList; + collectionList << rootCollection << resourceCollectionTwo; + + StateMap map; + map.insert(StandardActionManager::CreateCollection, false); + map.insert(StandardActionManager::CopyCollections, false); + map.insert(StandardActionManager::DeleteCollections, false); + map.insert(StandardActionManager::SynchronizeCollections, true); + map.insert(StandardActionManager::CollectionProperties, false); + map.insert(StandardActionManager::CopyItems, false); + map.insert(StandardActionManager::Paste, false); + map.insert(StandardActionManager::DeleteItems, false); + map.insert(StandardActionManager::AddToFavoriteCollections, false); + map.insert(StandardActionManager::RemoveFromFavoriteCollections, false); + map.insert(StandardActionManager::RenameFavoriteCollection, false); + map.insert(StandardActionManager::CopyCollectionToMenu, false); + map.insert(StandardActionManager::CopyItemToMenu, false); + map.insert(StandardActionManager::MoveItemToMenu, false); + map.insert(StandardActionManager::MoveCollectionToMenu, false); + map.insert(StandardActionManager::CutItems, false); + map.insert(StandardActionManager::CutCollections, false); + map.insert(StandardActionManager::CreateResource, true); + map.insert(StandardActionManager::DeleteResources, false); + map.insert(StandardActionManager::ResourceProperties, false); + map.insert(StandardActionManager::SynchronizeResources, false); + map.insert(StandardActionManager::MoveItemToDialog, false); + map.insert(StandardActionManager::CopyItemToDialog, false); + map.insert(StandardActionManager::CopyCollectionToDialog, false); + map.insert(StandardActionManager::MoveCollectionToDialog, false); + map.insert(StandardActionManager::SynchronizeCollectionsRecursive, false); + map.insert(StandardActionManager::MoveCollectionsToTrash, false); + map.insert(StandardActionManager::MoveItemsToTrash, false); + map.insert(StandardActionManager::RestoreCollectionsFromTrash, false); + map.insert(StandardActionManager::RestoreItemsFromTrash, false); + map.insert(StandardActionManager::MoveToTrashRestoreCollection, false); + map.insert(StandardActionManager::MoveToTrashRestoreItem, false); + map.insert(StandardActionManager::SynchronizeCollectionTree, false); + + QTest::newRow("root collection and writable resource collection selected") << collectionList << map; + } + } + + void testCollectionSelected() + { + QFETCH(Collection::List, collections); + QFETCH(StateMap, stateMap); + + UnitActionStateManager manager(this); + Collection::List favoriteCollections; + if (collections.contains(folderCollectionThree)) { + favoriteCollections << folderCollectionThree; + } + + manager.updateState(collections, favoriteCollections, Item::List()); + + QCOMPARE(stateMap.count(), mStateMap.count()); + + QHashIterator it(stateMap); + while (it.hasNext()) { + it.next(); + // qDebug() << it.key(); + QVERIFY(mStateMap.contains(it.key())); + const bool expected = mStateMap.value(it.key()); + if (it.value() != expected) { + qWarning() << "Wrong state for" << it.key(); + } + QCOMPARE(it.value(), expected); + } + } + +private: + /** + * The structure of our fake collections: + * + * rootCollection + * | + * +- resourceCollectionOne + * | | + * | `folderCollectionOne + * | + * +- resourceCollectionTwo + * | | + * | `folderCollectionTwo + * | + * `- resourceCollectionThree + * | + * +-folderCollectionThree + * | + * `-folderCollectionThreeOne + */ + Collection rootCollection; + Collection resourceCollectionOne; + Collection resourceCollectionTwo; + Collection resourceCollectionThree; + Collection folderCollectionOne; + Collection folderCollectionTwo; + Collection folderCollectionThree; + Collection folderCollectionThreeOne; + + StateMap mStateMap; + QHash mCapabilityMap; + QSet mFavoriteCollectionMap; +}; + +UnitActionStateManager::UnitActionStateManager(ActionStateManagerTest *receiver) + : mReceiver(receiver) +{ + setReceiver(receiver); +} + +bool UnitActionStateManager::hasResourceCapability(const Collection &collection, const QString &capability) const +{ + return mReceiver->hasResourceCapability(collection, capability); +} + +QTEST_AKONADIMAIN(ActionStateManagerTest) + +#include "actionstatemanagertest.moc" diff --git a/autotests/libs/attributefactorytest.cpp b/autotests/libs/attributefactorytest.cpp new file mode 100644 index 0000000..012c608 --- /dev/null +++ b/autotests/libs/attributefactorytest.cpp @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "attributefactorytest.h" +#include "collectionpathresolver.h" +#include "testattribute.h" + +#include "attributefactory.h" +#include "collection.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(AttributeFactoryTest) + +static Collection res1; + +void AttributeFactoryTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + auto resolver = new CollectionPathResolver(QStringLiteral("res1"), this); + AKVERIFYEXEC(resolver); + res1 = Collection(resolver->collection()); +} + +void AttributeFactoryTest::testUnknownAttribute() +{ + // The attribute is currently not registered. + Item item; + item.setMimeType(QStringLiteral("text/directory")); + item.setPayload("payload"); + auto ta = new TestAttribute; + { + auto created = AttributeFactory::createAttribute(ta->type()); // DefaultAttribute + QVERIFY(created != nullptr); + delete created; + } + ta->data = "lalala"; + item.addAttribute(ta); + auto cjob = new ItemCreateJob(item, res1); + AKVERIFYEXEC(cjob); + int id = cjob->item().id(); + item = Item(id); + auto fjob = new ItemFetchJob(item); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAllAttributes(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + QVERIFY(item.hasAttribute()); // has DefaultAttribute + ta = item.attribute(); + QVERIFY(!ta); // but can't cast it to TestAttribute +} + +void AttributeFactoryTest::testRegisteredAttribute() +{ + AttributeFactory::registerAttribute(); + + Item item; + item.setMimeType(QStringLiteral("text/directory")); + item.setPayload("payload"); + auto ta = new TestAttribute; + { + auto created = AttributeFactory::createAttribute(ta->type()); + QVERIFY(created != nullptr); + delete created; + } + ta->data = "lalala"; + item.addAttribute(ta); + auto cjob = new ItemCreateJob(item, res1); + AKVERIFYEXEC(cjob); + int id = cjob->item().id(); + item = Item(id); + auto fjob = new ItemFetchJob(item); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAllAttributes(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + QVERIFY(item.hasAttribute()); + ta = item.attribute(); + QVERIFY(ta); + QCOMPARE(ta->data, QByteArray("lalala")); +} diff --git a/autotests/libs/attributefactorytest.h b/autotests/libs/attributefactorytest.h new file mode 100644 index 0000000..2498674 --- /dev/null +++ b/autotests/libs/attributefactorytest.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class AttributeFactoryTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testUnknownAttribute(); + void testRegisteredAttribute(); +}; + diff --git a/autotests/libs/autoincrementtest.cpp b/autotests/libs/autoincrementtest.cpp new file mode 100644 index 0000000..5b38ba0 --- /dev/null +++ b/autotests/libs/autoincrementtest.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2009 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "autoincrementtest.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "control.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(AutoIncrementTest) + +void AutoIncrementTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + + itemTargetCollection = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(itemTargetCollection.isValid()); + + collectionTargetCollection = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(collectionTargetCollection.isValid()); +} + +Akonadi::ItemCreateJob *AutoIncrementTest::createItemCreateJob() +{ + QByteArray payload("Hello world"); + Item item(-1); + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(payload); + return new ItemCreateJob(item, itemTargetCollection); +} + +Akonadi::CollectionCreateJob *AutoIncrementTest::createCollectionCreateJob(int number) +{ + Collection collection; + collection.setParentCollection(collectionTargetCollection); + collection.setName(QStringLiteral("testCollection") + QString::number(number)); + return new CollectionCreateJob(collection); +} + +void AutoIncrementTest::testItemAutoIncrement() +{ + QList itemsToDelete; + Item::Id lastId = -1; + + // Create 20 test items + for (int i = 0; i < 20; i++) { + ItemCreateJob *job = createItemCreateJob(); + AKVERIFYEXEC(job); + Item newItem = job->item(); + QVERIFY(newItem.id() > lastId); + lastId = newItem.id(); + itemsToDelete.append(newItem); + } + + // Delete the 20 items + for (const Item &item : std::as_const(itemsToDelete)) { + auto job = new ItemDeleteJob(item); + AKVERIFYEXEC(job); + } + + // Restart the server, then test item creation again. The new id of the item + // should be higher than all ids before. + AkonadiTest::restartAkonadiServer(); + ItemCreateJob *job = createItemCreateJob(); + AKVERIFYEXEC(job); + Item newItem = job->item(); + + QVERIFY(newItem.id() > lastId); +} + +void AutoIncrementTest::testCollectionAutoIncrement() +{ + Collection::List collectionsToDelete; + Collection::Id lastId = -1; + + // Create 20 test collections + for (int i = 0; i < 20; i++) { + CollectionCreateJob *job = createCollectionCreateJob(i); + AKVERIFYEXEC(job); + Collection newCollection = job->collection(); + QVERIFY(newCollection.id() > lastId); + lastId = newCollection.id(); + collectionsToDelete.append(newCollection); + } + + // Delete the 20 collections + for (const Collection &collection : std::as_const(collectionsToDelete)) { + auto job = new CollectionDeleteJob(collection); + AKVERIFYEXEC(job); + } + + // Restart the server, then test collection creation again. The new id of the collection + // should be higher than all ids before. + AkonadiTest::restartAkonadiServer(); + + CollectionCreateJob *job = createCollectionCreateJob(0); + AKVERIFYEXEC(job); + Collection newCollection = job->collection(); + + QVERIFY(newCollection.id() > lastId); +} diff --git a/autotests/libs/autoincrementtest.h b/autotests/libs/autoincrementtest.h new file mode 100644 index 0000000..0cd73ac --- /dev/null +++ b/autotests/libs/autoincrementtest.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2009 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include "collection.h" + +#include + +namespace Akonadi +{ +class CollectionCreateJob; +class ItemCreateJob; +} + +class AutoIncrementTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testItemAutoIncrement(); + void testCollectionAutoIncrement(); + +private: + Akonadi::ItemCreateJob *createItemCreateJob(); + Akonadi::CollectionCreateJob *createCollectionCreateJob(int number); + Akonadi::Collection itemTargetCollection; + Akonadi::Collection collectionTargetCollection; +}; + diff --git a/autotests/libs/cachepolicytest.cpp b/autotests/libs/cachepolicytest.cpp new file mode 100644 index 0000000..0bd1277 --- /dev/null +++ b/autotests/libs/cachepolicytest.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2017-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "cachepolicytest.h" +#include "cachepolicy.h" +#include + +CachePolicyTest::CachePolicyTest(QObject *parent) + : QObject(parent) +{ +} + +CachePolicyTest::~CachePolicyTest() +{ +} + +void CachePolicyTest::shouldHaveDefaultValue() +{ + Akonadi::CachePolicy c; + QVERIFY(c.inheritFromParent()); + QCOMPARE(c.intervalCheckTime(), -1); + QCOMPARE(c.cacheTimeout(), -1); + QVERIFY(!c.syncOnDemand()); + QVERIFY(c.localParts().isEmpty()); +} + +QTEST_MAIN(CachePolicyTest) diff --git a/autotests/libs/cachepolicytest.h b/autotests/libs/cachepolicytest.h new file mode 100644 index 0000000..a700dd8 --- /dev/null +++ b/autotests/libs/cachepolicytest.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2017-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class CachePolicyTest : public QObject +{ + Q_OBJECT +public: + explicit CachePolicyTest(QObject *parent = nullptr); + ~CachePolicyTest(); +private Q_SLOTS: + void shouldHaveDefaultValue(); +}; + diff --git a/autotests/libs/cachetest.cpp b/autotests/libs/cachetest.cpp new file mode 100644 index 0000000..3f0f9d6 --- /dev/null +++ b/autotests/libs/cachetest.cpp @@ -0,0 +1,142 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "item.h" +#include "itemcopyjob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "qtest_akonadi.h" + +#include + +using namespace Akonadi; + +class CacheTest : public QObject +{ + Q_OBJECT +private: + void enableAgent(const QString &id, bool enable) + { + auto instance = AgentManager::self()->instance(id); + QVERIFY(instance.isValid()); + + instance.setIsOnline(enable); + QTRY_COMPARE(Akonadi::AgentManager::self()->instance(id).isOnline(), enable); + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testRetrievalErrorBurst() // caused rare server crashs with old item retrieval code + { + Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + + enableAgent(QStringLiteral("akonadi_knut_resource_0"), false); + + auto fetch = new ItemFetchJob(col, this); + fetch->fetchScope().fetchFullPayload(true); + QVERIFY(!fetch->exec()); + } + + void testResourceRetrievalOnFetch_data() + { + QTest::addColumn("item"); + QTest::addColumn("resourceEnabled"); + + QTest::newRow("resource online") << Item(1) << true; + QTest::newRow("resource offline") << Item(2) << false; + } + + void testResourceRetrievalOnFetch() + { + QFETCH(Item, item); + QFETCH(bool, resourceEnabled); + + auto fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QVERIFY(!item.hasPayload()); + + enableAgent(QStringLiteral("akonadi_knut_resource_0"), resourceEnabled); + + fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + QCOMPARE(fetch->exec(), resourceEnabled); + if (resourceEnabled) { + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QVERIFY(item.hasPayload()); + QVERIFY(item.revision() > 0); // was changed by the resource delivering the payload + } + + fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.hasPayload(), resourceEnabled); + } + + void testResourceRetrievalOnCopy_data() + { + QTest::addColumn("item"); + QTest::addColumn("resourceEnabled"); + + QTest::newRow("online") << Item(3) << true; + QTest::newRow("offline") << Item(4) << false; + } + + void testResourceRetrievalOnCopy() + { + QFETCH(Item, item); + QFETCH(bool, resourceEnabled); + + auto fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QVERIFY(!item.hasPayload()); + + enableAgent(QStringLiteral("akonadi_knut_resource_0"), resourceEnabled); + + Collection dest(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(dest.isValid()); + + auto copy = new ItemCopyJob(item, dest, this); + QCOMPARE(copy->exec(), resourceEnabled); + + fetch = new ItemFetchJob(item, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.hasPayload(), resourceEnabled); + } +}; + +QTEST_AKONADIMAIN(CacheTest) + +#include "cachetest.moc" diff --git a/autotests/libs/changerecordertest.cpp b/autotests/libs/changerecordertest.cpp new file mode 100644 index 0000000..718e55e --- /dev/null +++ b/autotests/libs/changerecordertest.cpp @@ -0,0 +1,184 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "changerecorder.h" +#include "agentmanager.h" +#include "itemdeletejob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "testattribute.h" + +#include "qtest_akonadi.h" + +#include +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(QSet) + +class ChangeRecorderTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + qRegisterMetaType(); + qRegisterMetaType>(); + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + + settings = new QSettings(QStringLiteral("kde.org"), QStringLiteral("akonadi-changerecordertest"), this); + } + + // After each test + void cleanup() + { + // See ChangeRecorderPrivate::notificationsFileName() + QFile::remove(settings->fileName() + QStringLiteral("_changes.dat")); + } + + void testChangeRecorder_data() + { + QTest::addColumn("actions"); + + QTest::newRow("nothingToReplay") << (QStringList() << QStringLiteral("rn")); + QTest::newRow("nothingOneNothing") << (QStringList() << QStringLiteral("rn") << QStringLiteral("c2") << QStringLiteral("r2") << QStringLiteral("rn")); + QTest::newRow("multipleItems") << (QStringList() << QStringLiteral("c1") << QStringLiteral("c2") << QStringLiteral("c3") << QStringLiteral("r1") + << QStringLiteral("c4") << QStringLiteral("r2") << QStringLiteral("r3") << QStringLiteral("r4") + << QStringLiteral("rn")); + QTest::newRow("reload") << (QStringList() << QStringLiteral("c1") << QStringLiteral("c1") << QStringLiteral("c3") << QStringLiteral("reload") + << QStringLiteral("r1") << QStringLiteral("r1") << QStringLiteral("r3") << QStringLiteral("rn")); + QTest::newRow("more") << (QStringList() << QStringLiteral("c1") << QStringLiteral("c2") << QStringLiteral("c3") << QStringLiteral("reload") + << QStringLiteral("r1") << QStringLiteral("reload") << QStringLiteral("c4") << QStringLiteral("reload") + << QStringLiteral("r2") << QStringLiteral("reload") << QStringLiteral("r3") << QStringLiteral("r4") + << QStringLiteral("rn")); + // FIXME: Due to the event compression in the server we simply expect a removal signal + // QTest::newRow("modifyThenDelete") << (QStringList() << "c1" << "d1" << "r1" << "rn"); + } + + void testChangeRecorder() + { + QFETCH(QStringList, actions); + QString lastAction; + + auto rec = createChangeRecorder(); + QVERIFY(rec->isEmpty()); + for (const QString &action : std::as_const(actions)) { + qDebug() << action; + if (action == QLatin1String("rn")) { + replayNextAndExpectNothing(rec.get()); + } else if (action == QLatin1String("reload")) { + // Check saving and loading from disk + rec = createChangeRecorder(); + } else if (action.at(0) == QLatin1Char('c')) { + // c1 = "trigger change on item 1" + const int id = action.midRef(1).toInt(); + Q_ASSERT(id); + triggerChange(id); + if (action != lastAction) { + // enter event loop and wait for change notifications from the server + QVERIFY(AkonadiTest::akWaitForSignal(rec.get(), &ChangeRecorder::changesAdded, 1000)); + } + } else if (action.at(0) == QLatin1Char('d')) { + // d1 = "delete item 1" + const int id = action.midRef(1).toInt(); + Q_ASSERT(id); + triggerDelete(id); + QTest::qWait(500); + } else if (action.at(0) == QLatin1Char('r')) { + // r1 = "replayNext and expect to get itemChanged(1)" + const int id = action.midRef(1).toInt(); + Q_ASSERT(id); + replayNextAndProcess(rec.get(), id); + } else { + QVERIFY2(false, qPrintable(QStringLiteral("Unsupported: ") + action)); + } + lastAction = action; + } + QVERIFY(rec->isEmpty()); + } + +private: + void triggerChange(Akonadi::Item::Id uid) + { + static int s_num = 0; + Item item(uid); + auto attr = item.attribute(Item::AddIfMissing); + attr->data = QByteArray::number(++s_num); + auto job = new ItemModifyJob(item); + job->disableRevisionCheck(); + AKVERIFYEXEC(job); + } + + void triggerDelete(Akonadi::Item::Id uid) + { + Item item(uid); + auto job = new ItemDeleteJob(item); + AKVERIFYEXEC(job); + } + + void replayNextAndProcess(ChangeRecorder *rec, Akonadi::Item::Id expectedUid) + { + QSignalSpy nothingSpy(rec, &ChangeRecorder::nothingToReplay); + QVERIFY(nothingSpy.isValid()); + QSignalSpy itemChangedSpy(rec, &Monitor::itemChanged); + QVERIFY(itemChangedSpy.isValid()); + + rec->replayNext(); + if (itemChangedSpy.isEmpty()) { + QVERIFY(AkonadiTest::akWaitForSignal(rec, &Monitor::itemChanged, 1000)); + } + QCOMPARE(itemChangedSpy.count(), 1); + QCOMPARE(itemChangedSpy.at(0).at(0).value().id(), expectedUid); + + rec->changeProcessed(); + + QCOMPARE(nothingSpy.count(), 0); + } + + void replayNextAndExpectNothing(ChangeRecorder *rec) + { + QSignalSpy nothingSpy(rec, &ChangeRecorder::nothingToReplay); + QVERIFY(nothingSpy.isValid()); + QSignalSpy itemChangedSpy(rec, &Monitor::itemChanged); + QVERIFY(itemChangedSpy.isValid()); + + rec->replayNext(); // emits nothingToReplay immediately + + QCOMPARE(itemChangedSpy.count(), 0); + QCOMPARE(nothingSpy.count(), 1); + } + + std::unique_ptr createChangeRecorder() const + { + auto rec = std::make_unique(); + rec->setConfig(settings); + rec->setAllMonitored(); + rec->itemFetchScope().fetchFullPayload(); + rec->itemFetchScope().fetchAllAttributes(); + rec->itemFetchScope().setCacheOnly(true); + + // Ensure we listen to a signal, otherwise MonitorPrivate::isLazilyIgnored will ignore notifications + auto spy = new QSignalSpy(rec.get(), &Monitor::itemChanged); + spy->setParent(rec.get()); + + QSignalSpy readySpy(rec.get(), &Monitor::monitorReady); + if (!readySpy.wait()) { + QTest::qFail("Failed to wait for Monitor", __FILE__, __LINE__); + return nullptr; + } + + return rec; + } + + QSettings *settings = nullptr; +}; + +QTEST_AKONADIMAIN(ChangeRecorderTest) + +#include "changerecordertest.moc" diff --git a/autotests/libs/collectionattributetest.cpp b/autotests/libs/collectionattributetest.cpp new file mode 100644 index 0000000..7abfa53 --- /dev/null +++ b/autotests/libs/collectionattributetest.cpp @@ -0,0 +1,251 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionattributetest.h" +#include "collectionpathresolver.h" + +#include "attributefactory.h" +#include "collection.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionidentificationattribute.h" +#include "collectionmodifyjob.h" +#include "collectionrightsattribute_p.h" +#include "control.h" + +#include "qtest_akonadi.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(CollectionAttributeTest) + +class TestAttribute : public Attribute +{ +public: + TestAttribute() = default; + + explicit TestAttribute(const QByteArray &data) + : mData(data) + { + } + TestAttribute *clone() const override + { + return new TestAttribute(mData); + } + QByteArray type() const override + { + return "TESTATTRIBUTE"; + } + QByteArray serialized() const override + { + return mData; + } + void deserialize(const QByteArray &data) override + { + mData = data; + } + +private: + QByteArray mData; +}; + +static int parentColId = -1; + +void CollectionAttributeTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AttributeFactory::registerAttribute(); + + auto resolver = new CollectionPathResolver(QStringLiteral("res3"), this); + AKVERIFYEXEC(resolver); + parentColId = resolver->collection(); + QVERIFY(parentColId > 0); +} + +void CollectionAttributeTest::testAttributes_data() +{ + QTest::addColumn("attr1"); + QTest::addColumn("attr2"); + + QTest::newRow("basic") << QByteArray("foo") << QByteArray("bar"); + QTest::newRow("empty1") << QByteArray("") << QByteArray("non-empty"); +#if 0 // This one is failing on the CI with SQLite. Can't reproduce locally and + // it works with other DB backends, so I have no idea what is going on... + QTest::newRow("empty2") << QByteArray("non-empty") << QByteArray(""); +#endif + QTest::newRow("space") << QByteArray("foo bar") << QByteArray("bar foo"); + QTest::newRow("newline") << QByteArray("\n") << QByteArray("no newline"); + QTest::newRow("newline2") << QByteArray(" \\\n\\\nnn") << QByteArray("no newline"); + QTest::newRow("cr") << QByteArray("\r") << QByteArray("\\\r\n"); + QTest::newRow("quotes") << QByteArray(R"("quoted \ test")") << QByteArray("single \" quote \\"); + QTest::newRow("parenthesis") << QByteArray(")") << QByteArray("("); + QTest::newRow("binary") << QByteArray("\000") << QByteArray("\001"); +} + +void CollectionAttributeTest::testAttributes() +{ + QFETCH(QByteArray, attr1); + QFETCH(QByteArray, attr2); + + struct Cleanup { + explicit Cleanup(const Collection &col) + : m_col(col) + { + } + ~Cleanup() + { + // cleanup + auto del = new CollectionDeleteJob(m_col); + AKVERIFYEXEC(del); + } + Collection m_col; + }; + + // add a custom attribute + auto attr = new TestAttribute(); + attr->deserialize(attr1); + Collection col; + col.setName(QStringLiteral("attribute test")); + col.setParentCollection(Collection(parentColId)); + col.addAttribute(attr); + auto create = new CollectionCreateJob(col, this); + AKVERIFYEXEC(create); + col = create->collection(); + QVERIFY(col.isValid()); + Cleanup cleanup(col); + + attr = col.attribute(); + QVERIFY(attr != nullptr); + QCOMPARE(attr->serialized(), QByteArray(attr1)); + + auto list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + col = list->collections().at(0); + + QVERIFY(col.isValid()); + attr = col.attribute(); + QVERIFY(attr != nullptr); + QCOMPARE(attr->serialized(), QByteArray(attr1)); + + auto attrB = new TestAttribute(); + attrB->deserialize(attr2); + col.addAttribute(attrB); + attrB = col.attribute(); + QVERIFY(attrB != nullptr); + QCOMPARE(attrB->serialized(), QByteArray(attr2)); + + attrB->deserialize(attr1); + col.addAttribute(attrB); + attrB = col.attribute(); + QVERIFY(attrB != nullptr); + QCOMPARE(attrB->serialized(), QByteArray(attr1)); + + // this will mark the attribute as modified in the storage, but should not create trouble further down + QVERIFY(!col.attribute("does_not_exist")); + + // modify a custom attribute + col.attribute(Collection::AddIfMissing)->deserialize(attr2); + auto modify = new CollectionModifyJob(col, this); + AKVERIFYEXEC(modify); + + list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + col = list->collections().at(0); + + QVERIFY(col.isValid()); + attr = col.attribute(); + QVERIFY(attr != nullptr); + QCOMPARE(attr->serialized(), QByteArray(attr2)); + + // delete a custom attribute + col.removeAttribute(); + modify = new CollectionModifyJob(col, this); + AKVERIFYEXEC(modify); + + list = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + col = list->collections().at(0); + + QVERIFY(col.isValid()); + attr = col.attribute(); + QVERIFY(attr == nullptr); + + // Give the knut resource a bit of time to modify the collection and add a remote ID (after adding) + // and reparent attributes (after modifying). + // Otherwise we can delete it faster than it can do that, and we end up with a confusing warning + // "No such collection" from the resource's modify job. + QTest::qWait(100); // ideally we'd loop over "fetch and check there's a remote id" +} + +void CollectionAttributeTest::testDefaultAttributes() +{ + Collection col; + QCOMPARE(col.attributes().count(), 0); + Attribute *attr = AttributeFactory::createAttribute("TYPE"); + QVERIFY(attr); + attr->deserialize("VALUE"); + col.addAttribute(attr); + QCOMPARE(col.attributes().count(), 1); + QVERIFY(col.hasAttribute("TYPE")); + QCOMPARE(col.attribute("TYPE")->serialized(), QByteArray("VALUE")); +} + +void CollectionAttributeTest::testCollectionRightsAttribute() +{ + CollectionRightsAttribute attribute; + Collection::Rights rights; + + QCOMPARE(attribute.rights(), rights); + + for (int mask = 0; mask <= Collection::AllRights; ++mask) { + rights = Collection::AllRights; + rights &= mask; + QCOMPARE(rights, mask); + + attribute.setRights(rights); + QCOMPARE(attribute.rights(), rights); + + QByteArray data = attribute.serialized(); + attribute.deserialize(data); + QCOMPARE(attribute.rights(), rights); + } +} + +void CollectionAttributeTest::testCollectionIdentificationAttribute() +{ + QByteArray id("identifier"); + QByteArray ns("namespace"); + CollectionIdentificationAttribute attribute(id, ns); + QCOMPARE(attribute.identifier(), id); + QCOMPARE(attribute.collectionNamespace(), ns); + + QByteArray result = attribute.serialized(); + CollectionIdentificationAttribute parsed; + parsed.deserialize(result); + qDebug() << parsed.identifier() << parsed.collectionNamespace() << result; + QCOMPARE(parsed.identifier(), id); + QCOMPARE(parsed.collectionNamespace(), ns); +} + +void CollectionAttributeTest::testDetach() +{ + // GIVEN a collection with an attribute + Collection col; + col.attribute(Akonadi::Collection::AddIfMissing); + Collection col2 = col; // and a copy, so that non-const access detaches + + // WHEN + auto attr = col2.attribute(Akonadi::Collection::AddIfMissing); + auto attr2 = col2.attribute(); + + // THEN + QCOMPARE(attr, attr2); +} diff --git a/autotests/libs/collectionattributetest.h b/autotests/libs/collectionattributetest.h new file mode 100644 index 0000000..7ff5837 --- /dev/null +++ b/autotests/libs/collectionattributetest.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class CollectionAttributeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testAttributes_data(); + void testAttributes(); + void testDefaultAttributes(); + void testCollectionRightsAttribute(); + void testCollectionIdentificationAttribute(); + void testDetach(); +}; + diff --git a/autotests/libs/collectioncolorattributetest.cpp b/autotests/libs/collectioncolorattributetest.cpp new file mode 100644 index 0000000..84455c4 --- /dev/null +++ b/autotests/libs/collectioncolorattributetest.cpp @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2016 Sandro Knauß + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectioncolorattribute.h" + +#include + +#include + +using namespace Akonadi; + +class CollectionColorAttributeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDeserialize_data() + { + QTest::addColumn("input"); + QTest::addColumn("color"); + QTest::addColumn("output"); + + QTest::newRow("empty") << QByteArray("") << QColor() << QByteArray(""); + QTest::newRow("white") << QByteArray("white") << QColor("#ffffff") << QByteArray("#ffffffff"); + QTest::newRow("#123") << QByteArray("#123") << QColor("#112233") << QByteArray("#ff112233"); + QTest::newRow("#123456") << QByteArray("#123456") << QColor("#123456") << QByteArray("#ff123456"); + QTest::newRow("#1234567") << QByteArray("#1234567") << QColor() << QByteArray(""); + QTest::newRow("#12345678") << QByteArray("#12345678") << QColor("#12345678") << QByteArray("#12345678"); + QTest::newRow("#ff345678") << QByteArray("#ff123456") << QColor("#123456") << QByteArray("#ff123456"); + } + + void testDeserialize() + { + QFETCH(QByteArray, input); + QFETCH(QColor, color); + QFETCH(QByteArray, output); + + auto attr = new CollectionColorAttribute(); + attr->deserialize(input); + QCOMPARE(attr->color(), color); + + QCOMPARE(attr->serialized(), output); + + CollectionColorAttribute *copy = attr->clone(); + QCOMPARE(copy->serialized(), output); + + delete attr; + delete copy; + } +}; + +QTEST_MAIN(CollectionColorAttributeTest) + +#include "collectioncolorattributetest.moc" diff --git a/autotests/libs/collectioncopytest.cpp b/autotests/libs/collectioncopytest.cpp new file mode 100644 index 0000000..ccb60dc --- /dev/null +++ b/autotests/libs/collectioncopytest.cpp @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectioncopyjob.h" +#include "collectionfetchjob.h" +#include "control.h" +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "qtest_akonadi.h" + +#include + +using namespace Akonadi; + +class CollectionCopyTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + + Control::start(); + // switch target resources offline to reduce interference from them + foreach (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) { // krazy:exclude=foreach + if (agent.identifier() == QLatin1String("akonadi_knut_resource_2")) { + agent.setIsOnline(false); + } + } + } + + void testCopy() + { + const Collection target(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Collection source(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(target.isValid()); + QVERIFY(source.isValid()); + + // obtain reference listing + auto fetch = new CollectionFetchJob(source, CollectionFetchJob::Base); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->collections().count(), 1); + source = fetch->collections().first(); + QVERIFY(source.isValid()); + + fetch = new CollectionFetchJob(source, CollectionFetchJob::Recursive); + AKVERIFYEXEC(fetch); + QMap referenceData; + Collection::List cols = fetch->collections(); + cols << source; + for (const Collection &c : std::as_const(cols)) { + auto job = new ItemFetchJob(c, this); + AKVERIFYEXEC(job); + referenceData.insert(c, job->items()); + } + + // actually copy the collection + auto copy = new CollectionCopyJob(source, target); + AKVERIFYEXEC(copy); + + // list destination and check if everything has arrived + auto list = new CollectionFetchJob(target, CollectionFetchJob::Recursive); + AKVERIFYEXEC(list); + cols = list->collections(); + QCOMPARE(cols.count(), referenceData.count()); + for (QMap::ConstIterator it = referenceData.constBegin(), end = referenceData.constEnd(); it != end; ++it) { + QVERIFY(!cols.contains(it.key())); + Collection col; + for (const Collection &c : std::as_const(cols)) { + if (it.key().name() == c.name()) { + col = c; + } + } + + QVERIFY(col.isValid()); + QCOMPARE(col.resource(), QStringLiteral("akonadi_knut_resource_2")); + QVERIFY(col.remoteId().isEmpty()); + auto job = new ItemFetchJob(col, this); + job->fetchScope().fetchFullPayload(); + job->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), it.value().count()); + foreach (const Item &item, job->items()) { + QVERIFY(!it.value().contains(item)); + QVERIFY(item.remoteId().isEmpty()); + QVERIFY(item.hasPayload()); + } + } + } + + void testIlleagalCopy() + { + // invalid source + auto copy = new CollectionCopyJob(Collection(), Collection(1)); + QVERIFY(!copy->exec()); + + // non-existing source + copy = new CollectionCopyJob(Collection(INT_MAX), Collection(1)); + QVERIFY(!copy->exec()); + + // invalid target + copy = new CollectionCopyJob(Collection(1), Collection()); + QVERIFY(!copy->exec()); + + // non-existing target + copy = new CollectionCopyJob(Collection(1), Collection(INT_MAX)); + QVERIFY(!copy->exec()); + } +}; + +QTEST_AKONADIMAIN(CollectionCopyTest) + +#include "collectioncopytest.moc" diff --git a/autotests/libs/collectioncreatetest.cpp b/autotests/libs/collectioncreatetest.cpp new file mode 100644 index 0000000..d447921 --- /dev/null +++ b/autotests/libs/collectioncreatetest.cpp @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2017 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "entitydisplayattribute.h" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class CollectionCreateTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testCreateCollection() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy spy(monitor.get(), &Monitor::collectionAdded); + + Collection col; + col.setName(QLatin1String("test_collection")); + col.setContentMimeTypes({Collection::mimeType()}); + col.setParentCollection(Collection(AkonadiTest::collectionIdFromPath(QLatin1String("res1")))); + col.setRights(Collection::AllRights); + + auto cj = new CollectionCreateJob(col, this); + AKVERIFYEXEC(cj); + col = cj->collection(); + QVERIFY(col.isValid()); + + QTRY_COMPARE(spy.count(), 1); + auto ntfCol = spy.at(0).at(0).value(); + QCOMPARE(col, ntfCol); + + auto dj = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(dj); + } +}; + +QTEST_AKONADIMAIN(CollectionCreateTest) + +#include "collectioncreatetest.moc" diff --git a/autotests/libs/collectioncreator.cpp b/autotests/libs/collectioncreator.cpp new file mode 100644 index 0000000..5a5e43d --- /dev/null +++ b/autotests/libs/collectioncreator.cpp @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2006, 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "collectionpathresolver.h" +#include "transactionjobs.h" + +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class CollectionCreator : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + } + + void createCollections_data() + { + QTest::addColumn("count"); + QTest::addColumn("useTransaction"); + + QList counts = QList() << 1 << 10 << 100 << 1000; + QList transactions = QList() << false << true; + foreach (int count, counts) { + foreach (bool transaction, transactions) { // krazy:exclude=foreach + QTest::newRow( + QString::fromLatin1("%1-%2").arg(count).arg(transaction ? QLatin1String("trans") : QLatin1String("notrans")).toLatin1().constData()) + << count << transaction; + } + } + } + + void createCollections() + { + QFETCH(int, count); + QFETCH(bool, useTransaction); + + const Collection parent(AkonadiTest::collectionIdFromPath(QLatin1String("res3"))); + QVERIFY(parent.isValid()); + + static int index = 0; + Job *lastJob = 0; + QBENCHMARK { + if (useTransaction) { + lastJob = new TransactionBeginJob(this); + } + for (int i = 0; i < count; ++i) { + Collection col; + col.setParentCollection(parent); + col.setName(QLatin1String("col") + QString::number(++index)); + lastJob = new CollectionCreateJob(col, this); + } + if (useTransaction) { + lastJob = new TransactionCommitJob(this); + } + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob *)), 15000); + } + } +}; + +QTEST_AKONADIMAIN(CollectionCreator) + +#include "collectioncreator.moc" diff --git a/autotests/libs/collectionjobtest.cpp b/autotests/libs/collectionjobtest.cpp new file mode 100644 index 0000000..6acbb6f --- /dev/null +++ b/autotests/libs/collectionjobtest.cpp @@ -0,0 +1,895 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionjobtest.h" + +#include + +#include "testattribute.h" +#include + +#include "agentinstance.h" +#include "agentmanager.h" +#include "attributefactory.h" +#include "cachepolicy.h" +#include "collection.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "collectionstatistics.h" +#include "collectionstatisticsjob.h" +#include "collectionutils.h" +#include "control.h" +#include "item.h" +#include "resourceselectjob_p.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(CollectionJobTest) + +void CollectionJobTest::initTestCase() +{ + qRegisterMetaType(); + AttributeFactory::registerAttribute(); + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); +} + +static Collection findCol(const Collection::List &list, const QString &name) +{ + foreach (const Collection &col, list) + if (col.name() == name) { + return col; + } + return Collection(); +} + +// list compare which ignores the order +template static void compareLists(const QList &l1, const QList &l2) +{ + QCOMPARE(l1.count(), l2.count()); + for (const T &entry : l1) { + QVERIFY(l2.contains(entry)); + } +} + +template static void compareLists(const QVector &l1, const QVector &l2) +{ + QCOMPARE(l1.count(), l2.count()); + for (const T &entry : l1) { + QVERIFY(l2.contains(entry)); + } +} + +template +static T *extractAttribute(const QList &attrs) +{ + T dummy; + for (Attribute *attr : attrs) { + if (attr->type() == dummy.type()) { + return dynamic_cast(attr); + } + } + return 0; +} + +static Collection::Id res1ColId = 6; // -1; +static Collection::Id res2ColId = 7; //-1; +static Collection::Id res3ColId = -1; +static Collection::Id searchColId = -1; + +void CollectionJobTest::testTopLevelList() +{ + // non-recursive top-level list + auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); + AKVERIFYEXEC(job); + Collection::List list = job->collections(); + + // check if everything is there and has the correct types and attributes + QCOMPARE(list.count(), 4); + Collection col; + + col = findCol(list, QStringLiteral("res1")); + QVERIFY(col.isValid()); + res1ColId = col.id(); // for the next test + QVERIFY(res1ColId > 0); + QVERIFY(CollectionUtils::isResource(col)); + QCOMPARE(col.parentCollection(), Collection::root()); + QCOMPARE(col.resource(), QStringLiteral("akonadi_knut_resource_0")); + + QVERIFY(findCol(list, QStringLiteral("res2")).isValid()); + res2ColId = findCol(list, QStringLiteral("res2")).id(); + QVERIFY(res2ColId > 0); + QVERIFY(findCol(list, QStringLiteral("res3")).isValid()); + res3ColId = findCol(list, QStringLiteral("res3")).id(); + QVERIFY(res3ColId > 0); + + col = findCol(list, QStringLiteral("Search")); + searchColId = col.id(); + QVERIFY(col.isValid()); + QVERIFY(CollectionUtils::isVirtualParent(col)); + QCOMPARE(col.resource(), QStringLiteral("akonadi_search_resource")); +} + +void CollectionJobTest::testFolderList() +{ + // recursive list of physical folders + auto job = new CollectionFetchJob(Collection(res1ColId), CollectionFetchJob::Recursive); + QSignalSpy spy(job, &CollectionFetchJob::collectionsReceived); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + Collection::List list = job->collections(); + + int count = 0; + for (int i = 0; i < spy.count(); ++i) { + auto l = spy[i][0].value(); + for (int j = 0; j < l.count(); ++j) { + QVERIFY(list.count() > count + j); + QCOMPARE(list[count + j].id(), l[j].id()); + } + count += l.count(); + } + QCOMPARE(count, list.count()); + + // check if everything is there + QCOMPARE(list.count(), 4); + Collection col; + QStringList contentTypes; + + col = findCol(list, QStringLiteral("foo")); + QVERIFY(col.isValid()); + QCOMPARE(col.parentCollection().id(), res1ColId); + QVERIFY(CollectionUtils::isFolder(col)); + contentTypes << QStringLiteral("message/rfc822") << QStringLiteral("text/calendar") << QStringLiteral("text/directory") + << QStringLiteral("application/octet-stream") << QStringLiteral("inode/directory"); + compareLists(col.contentMimeTypes(), contentTypes); + + QVERIFY(findCol(list, QStringLiteral("bar")).isValid()); + QCOMPARE(findCol(list, QStringLiteral("bar")).parentCollection(), col); + QVERIFY(findCol(list, QStringLiteral("bla")).isValid()); +} + +class ResultSignalTester : public QObject +{ + Q_OBJECT +public: + QStringList receivedSignals; +public Q_SLOTS: + void onCollectionsReceived(const Akonadi::Collection::List & /*unused*/) + { + receivedSignals << QStringLiteral("collectionsReceived"); + } + + void onCollectionRetrievalDone(KJob * /*unused*/) + { + receivedSignals << QStringLiteral("result"); + } +}; + +void CollectionJobTest::testSignalOrder() +{ + Akonadi::Collection::List toFetch; + toFetch << Collection(res1ColId); + toFetch << Collection(res2ColId); + auto job = new CollectionFetchJob(toFetch, CollectionFetchJob::Recursive); + ResultSignalTester spy; + connect(job, &CollectionFetchJob::collectionsReceived, &spy, &ResultSignalTester::onCollectionsReceived); + connect(job, &KJob::result, &spy, &ResultSignalTester::onCollectionRetrievalDone); + AKVERIFYEXEC(job); + + QCOMPARE(spy.receivedSignals.size(), 2); + QCOMPARE(spy.receivedSignals.at(0), QStringLiteral("collectionsReceived")); + QCOMPARE(spy.receivedSignals.at(1), QStringLiteral("result")); +} + +void CollectionJobTest::testNonRecursiveFolderList() +{ + auto job = new CollectionFetchJob(Collection(res1ColId), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Collection::List list = job->collections(); + + QCOMPARE(list.count(), 1); + QVERIFY(findCol(list, QStringLiteral("res1")).isValid()); +} + +void CollectionJobTest::testEmptyFolderList() +{ + auto job = new CollectionFetchJob(Collection(res3ColId), CollectionFetchJob::FirstLevel); + AKVERIFYEXEC(job); + Collection::List list = job->collections(); + + QCOMPARE(list.count(), 0); +} + +void CollectionJobTest::testSearchFolderList() +{ + auto job = new CollectionFetchJob(Collection(searchColId), CollectionFetchJob::FirstLevel); + AKVERIFYEXEC(job); + Collection::List list = job->collections(); + + QCOMPARE(list.count(), 0); +} + +void CollectionJobTest::testResourceFolderList() +{ + // non-existing resource + auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); + job->fetchScope().setResource(QStringLiteral("i_dont_exist")); + QVERIFY(!job->exec()); + + // recursive listing of all collections of an existing resource + job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); + job->fetchScope().setResource(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(job); + + Collection::List list = job->collections(); + QCOMPARE(list.count(), 5); + QVERIFY(findCol(list, QStringLiteral("res1")).isValid()); + QVERIFY(findCol(list, QStringLiteral("foo")).isValid()); + QVERIFY(findCol(list, QStringLiteral("bar")).isValid()); + QVERIFY(findCol(list, QStringLiteral("bla")).isValid()); + int fooId = findCol(list, QStringLiteral("foo")).id(); + + // limited listing of a resource + job = new CollectionFetchJob(Collection(fooId), CollectionFetchJob::Recursive); + job->fetchScope().setResource(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(job); + + list = job->collections(); + QCOMPARE(list.count(), 3); + QVERIFY(findCol(list, QStringLiteral("bar")).isValid()); + QVERIFY(findCol(list, QStringLiteral("bla")).isValid()); +} + +void CollectionJobTest::testMimeTypeFilter() +{ + auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); + job->fetchScope().setContentMimeTypes(QStringList() << QStringLiteral("message/rfc822")); + AKVERIFYEXEC(job); + + Collection::List list = job->collections(); + QCOMPARE(list.count(), 2); + QVERIFY(findCol(list, QStringLiteral("res1")).isValid()); + QVERIFY(findCol(list, QStringLiteral("foo")).isValid()); + int fooId = findCol(list, QStringLiteral("foo")).id(); + + // limited listing of a resource + job = new CollectionFetchJob(Collection(fooId), CollectionFetchJob::Recursive); + job->fetchScope().setContentMimeTypes(QStringList() << QStringLiteral("message/rfc822")); + AKVERIFYEXEC(job); + + list = job->collections(); + QCOMPARE(list.count(), 0); + + // non-existing mimetype + job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + job->fetchScope().setContentMimeTypes(QStringList() << QStringLiteral("something/non-existing")); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 0); +} + +void CollectionJobTest::testCreateDeleteFolder_data() +{ + QTest::addColumn("collection"); + QTest::addColumn("creatable"); + + Collection col; + QTest::newRow("empty") << col << false; + col.setName(QStringLiteral("new folder")); + col.parentCollection().setId(res3ColId); + QTest::newRow("simple") << col << true; + + col.parentCollection().setId(res3ColId); + col.setName(QStringLiteral("foo")); + QTest::newRow("existing in different resource") << col << true; + + col.setName(QStringLiteral("mail folder")); + QStringList mimeTypes; + mimeTypes << QStringLiteral("inode/directory") << QStringLiteral("message/rfc822"); + col.setContentMimeTypes(mimeTypes); + col.setRemoteId(QStringLiteral("remote id")); + CachePolicy policy; + policy.setInheritFromParent(false); + policy.setIntervalCheckTime(60); + policy.setLocalParts({QStringLiteral("PLD:ENVELOPE")}); + policy.setSyncOnDemand(true); + policy.setCacheTimeout(120); + col.setCachePolicy(policy); + QTest::newRow("complex") << col << true; + + col = Collection(); + col.setName(QStringLiteral("New Folder")); + col.parentCollection().setId(searchColId); + QTest::newRow("search folder") << col << false; + + col.parentCollection().setId(res2ColId); + col.setName(QStringLiteral("foo2")); + QTest::newRow("already existing") << col << false; + + col.parentCollection().setId(res2ColId); // Sibling of collection 'foo2' + col.setName(QStringLiteral("foo2 ")); + QTest::newRow("name of an sibling with an additional ending space") << col << true; + + col.setName(QStringLiteral("Bla")); + col.parentCollection().setId(2); + QTest::newRow("already existing with different case") << col << true; + + auto resolver = new CollectionPathResolver(QStringLiteral("res2/foo2"), this); + AKVERIFYEXEC(resolver); + col.parentCollection().setId(resolver->collection()); + col.setName(QStringLiteral("new folder")); + QTest::newRow("parent noinferior") << col << false; + + col.parentCollection().setId(INT_MAX); + QTest::newRow("missing parent") << col << false; + + col = Collection(); + col.setName(QStringLiteral("rid parent")); + col.parentCollection().setRemoteId(QStringLiteral("8")); + QTest::newRow("rid parent") << col << false; // missing resource context +} + +void CollectionJobTest::testCreateDeleteFolder() +{ + QFETCH(Collection, collection); + QFETCH(bool, creatable); + + auto createJob = new CollectionCreateJob(collection, this); + QCOMPARE(createJob->exec(), creatable); + if (!creatable) { + return; + } + + Collection createdCol = createJob->collection(); + QVERIFY(createdCol.isValid()); + QCOMPARE(createdCol.name(), collection.name()); + QCOMPARE(createdCol.parentCollection(), collection.parentCollection()); + QCOMPARE(createdCol.remoteId(), collection.remoteId()); + QCOMPARE(createdCol.cachePolicy(), collection.cachePolicy()); + + auto listJob = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(listJob); + Collection listedCol = findCol(listJob->collections(), collection.name()); + QCOMPARE(listedCol, createdCol); + QCOMPARE(listedCol.remoteId(), collection.remoteId()); + QCOMPARE(listedCol.cachePolicy(), collection.cachePolicy()); + + // fetch parent to compare inherited collection properties + Collection parentCol = Collection::root(); + if (collection.parentCollection().isValid()) { + auto listJob = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::Base, this); + AKVERIFYEXEC(listJob); + QCOMPARE(listJob->collections().count(), 1); + parentCol = listJob->collections().first(); + } + + if (collection.contentMimeTypes().isEmpty()) { + compareLists(listedCol.contentMimeTypes(), parentCol.contentMimeTypes()); + } else { + compareLists(listedCol.contentMimeTypes(), collection.contentMimeTypes()); + } + + if (collection.resource().isEmpty()) { + QCOMPARE(listedCol.resource(), parentCol.resource()); + } else { + QCOMPARE(listedCol.resource(), collection.resource()); + } + + auto delJob = new CollectionDeleteJob(createdCol, this); + AKVERIFYEXEC(delJob); + + listJob = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(listJob); + QVERIFY(!findCol(listJob->collections(), collection.name()).isValid()); +} + +void CollectionJobTest::testIllegalDeleteFolder() +{ + // non-existing folder + auto del = new CollectionDeleteJob(Collection(INT_MAX), this); + QVERIFY(!del->exec()); + + // root + del = new CollectionDeleteJob(Collection::root(), this); + QVERIFY(!del->exec()); +} + +void CollectionJobTest::testStatistics() +{ + // empty folder + auto statistics = new CollectionStatisticsJob(Collection(res1ColId), this); + AKVERIFYEXEC(statistics); + + CollectionStatistics s = statistics->statistics(); + QCOMPARE(s.count(), 0LL); + QCOMPARE(s.unreadCount(), 0LL); + + // folder with attributes and content + auto resolver = new CollectionPathResolver(QStringLiteral("res1/foo"), this); + AKVERIFYEXEC(resolver); + statistics = new CollectionStatisticsJob(Collection(resolver->collection()), this); + AKVERIFYEXEC(statistics); + + s = statistics->statistics(); + QCOMPARE(s.count(), 15LL); + QCOMPARE(s.unreadCount(), 14LL); +} + +void CollectionJobTest::testModify_data() +{ + QTest::addColumn("uid"); + QTest::addColumn("rid"); + + QTest::newRow("uid") << AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo")) << QString(); + QTest::newRow("rid") << -1LL << QStringLiteral("10"); +} + +#define RESET_COLLECTION_ID \ + col.setId(uid); \ + if (!rid.isEmpty()) \ + col.setRemoteId(rid) + +void CollectionJobTest::testModify() +{ + QFETCH(qint64, uid); + QFETCH(QString, rid); + + if (!rid.isEmpty()) { + auto rjob = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(rjob); + } + + const QStringList reference = {QStringLiteral("text/calendar"), + QStringLiteral("text/directory"), + QStringLiteral("message/rfc822"), + QStringLiteral("application/octet-stream"), + QStringLiteral("inode/directory")}; + + Collection col; + RESET_COLLECTION_ID; + + // test noop modify + auto mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); + + auto ljob = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 1); + col = ljob->collections().first(); + compareLists(col.contentMimeTypes(), reference); + + // test clearing content types + RESET_COLLECTION_ID; + col.setContentMimeTypes(QStringList()); + mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); + + ljob = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 1); + col = ljob->collections().first(); + QVERIFY(col.contentMimeTypes().isEmpty()); + + // test setting contnet types + RESET_COLLECTION_ID; + col.setContentMimeTypes(reference); + mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); + + ljob = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 1); + col = ljob->collections().first(); + compareLists(col.contentMimeTypes(), reference); + + // add attribute + RESET_COLLECTION_ID; + col.attribute(Collection::AddIfMissing)->data = "new"; + mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); + + ljob = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(ljob); + QVERIFY(ljob->collections().first().hasAttribute()); + QCOMPARE(ljob->collections().first().attribute()->data, QByteArray("new")); + + // modify existing attribute + RESET_COLLECTION_ID; + col.attribute()->data = "modified"; + mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); + + ljob = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(ljob); + QVERIFY(ljob->collections().first().hasAttribute()); + QCOMPARE(ljob->collections().first().attribute()->data, QByteArray("modified")); + + // renaming + RESET_COLLECTION_ID; + col.setName(QStringLiteral("foo (renamed)")); + mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); + + ljob = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 1); + col = ljob->collections().first(); + QCOMPARE(col.name(), QStringLiteral("foo (renamed)")); + + RESET_COLLECTION_ID; + col.setName(QStringLiteral("foo")); + mod = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mod); +} + +#undef RESET_COLLECTION_ID + +void CollectionJobTest::testIllegalModify() +{ + // non-existing collection + Collection col(INT_MAX); + col.parentCollection().setId(res1ColId); + auto mod = new CollectionModifyJob(col, this); + QVERIFY(!mod->exec()); + + // rename to already existing name + col = Collection(res1ColId); + col.setName(QStringLiteral("res2")); + mod = new CollectionModifyJob(col, this); + QVERIFY(!mod->exec()); +} + +void CollectionJobTest::testUtf8CollectionName_data() +{ + QTest::addColumn("folderName"); + + QTest::newRow("Umlaut") << QString::fromUtf8("ä"); + QTest::newRow("Garbage") << QString::fromUtf8("đ→³}đþøæſð"); + QTest::newRow("Utf8") << QString::fromUtf8("日本語"); +} + +void CollectionJobTest::testUtf8CollectionName() +{ + QFETCH(QString, folderName); + + // create collection + Collection col; + col.parentCollection().setId(res3ColId); + col.setName(folderName); + auto create = new CollectionCreateJob(col, this); + AKVERIFYEXEC(create); + col = create->collection(); + QVERIFY(col.isValid()); + QCOMPARE(col.name(), folderName); + + // list parent + auto list = new CollectionFetchJob(Collection(res3ColId), CollectionFetchJob::Recursive, this); + AKVERIFYEXEC(list); + QCOMPARE(list->collections().count(), 1); + QCOMPARE(list->collections().first(), col); + QCOMPARE(list->collections().first().name(), col.name()); + + // modify collection + col.setContentMimeTypes({QStringLiteral("message/rfc822")}); + auto modify = new CollectionModifyJob(col, this); + AKVERIFYEXEC(modify); + + // collection statistics + auto statistics = new CollectionStatisticsJob(col, this); + AKVERIFYEXEC(statistics); + CollectionStatistics s = statistics->statistics(); + QCOMPARE(s.count(), 0LL); + QCOMPARE(s.unreadCount(), 0LL); + + // delete collection + auto del = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(del); +} + +void CollectionJobTest::testMultiList() +{ + Collection::List req; + req << Collection(res1ColId) << Collection(res2ColId); + auto job = new CollectionFetchJob(req, this); + AKVERIFYEXEC(job); + + Collection::List res; + res = job->collections(); + compareLists(res, req); +} + +void CollectionJobTest::testMultiListInvalid() +{ + Collection::List req; + req << Collection(res1ColId) << Collection(1234567) << Collection(res2ColId); + auto job = new CollectionFetchJob(req, this); + QVERIFY(!job->exec()); + // not all available collections are fetched + QVERIFY(job->collections().count() != 2); + + job = new CollectionFetchJob(req, this); + job->fetchScope().setIgnoreRetrievalErrors(true); + QVERIFY(!job->exec()); + Collection::List res; + res = job->collections(); + req = Collection::List() << Collection(res1ColId) << Collection(res2ColId); + compareLists(res, req); +} + +void CollectionJobTest::testRecursiveMultiList() +{ + Akonadi::Collection::List toFetch; + toFetch << Collection(res1ColId); + toFetch << Collection(res2ColId); + auto job = new CollectionFetchJob(toFetch, CollectionFetchJob::Recursive); + QSignalSpy spy(job, &CollectionFetchJob::collectionsReceived); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + + Collection::List list = job->collections(); + + int count = 0; + for (int i = 0; i < spy.count(); ++i) { + auto l = spy[i][0].value(); + for (int j = 0; j < l.count(); ++j) { + QVERIFY(list.count() > count + j); + QCOMPARE(list[count + j].id(), l[j].id()); + } + count += l.count(); + } + QCOMPARE(count, list.count()); + + // check if everything is there + QCOMPARE(list.count(), 4 + 2); + QVERIFY(findCol(list, QStringLiteral("foo")).isValid()); + QVERIFY(findCol(list, QStringLiteral("bar")).isValid()); + QVERIFY(findCol(list, QStringLiteral("bla")).isValid()); // There are two bla folders, but we only check for one. + QVERIFY(findCol(list, QStringLiteral("foo2")).isValid()); + QVERIFY(findCol(list, QStringLiteral("space folder")).isValid()); +} + +void CollectionJobTest::testNonOverlappingRootList() +{ + Akonadi::Collection::List toFetch; + toFetch << Collection(res1ColId); + toFetch << Collection(res2ColId); + auto job = new CollectionFetchJob(toFetch, CollectionFetchJob::NonOverlappingRoots); + QSignalSpy spy(job, &CollectionFetchJob::collectionsReceived); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + + Collection::List list = job->collections(); + + int count = 0; + for (int i = 0; i < spy.count(); ++i) { + auto l = spy[i][0].value(); + for (int j = 0; j < l.count(); ++j) { + QVERIFY(list.count() > count + j); + QCOMPARE(list[count + j].id(), l[j].id()); + } + count += l.count(); + } + QCOMPARE(count, list.count()); + + // check if everything is there + QCOMPARE(list.count(), 2); + QVERIFY(findCol(list, QStringLiteral("res1")).isValid()); + QVERIFY(findCol(list, QStringLiteral("res2")).isValid()); +} + +void CollectionJobTest::testRidFetch() +{ + Collection col; + col.setRemoteId(QStringLiteral("10")); + + auto job = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + job->fetchScope().setResource(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().count(), 1); + col = job->collections().first(); + QVERIFY(col.isValid()); + QCOMPARE(col.remoteId(), QString::fromLatin1("10")); +} + +void CollectionJobTest::testRidCreateDelete_data() +{ + QTest::addColumn("remoteId"); + QTest::newRow("ASCII") << QString::fromUtf8("MY REMOTE ID"); + QTest::newRow("LATIN1") << QString::fromUtf8("MY REMÖTE ID"); + QTest::newRow("UTF8") << QString::fromUtf8("MY REMOTE 検索表"); +} + +void CollectionJobTest::testRidCreateDelete() +{ + QFETCH(QString, remoteId); + Collection collection; + collection.setName(QStringLiteral("rid create")); + collection.parentCollection().setRemoteId(QStringLiteral("8")); + collection.setRemoteId(remoteId); + + auto resSel = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_2")); + AKVERIFYEXEC(resSel); + + auto createJob = new CollectionCreateJob(collection, this); + AKVERIFYEXEC(createJob); + + Collection createdCol = createJob->collection(); + QVERIFY(createdCol.isValid()); + QCOMPARE(createdCol.name(), collection.name()); + + auto listJob = new CollectionFetchJob(Collection(res3ColId), CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(listJob); + Collection listedCol = findCol(listJob->collections(), collection.name()); + QCOMPARE(listedCol, createdCol); + QCOMPARE(listedCol.name(), collection.name()); + + QVERIFY(!collection.isValid()); + auto delJob = new CollectionDeleteJob(collection, this); + AKVERIFYEXEC(delJob); + + listJob = new CollectionFetchJob(Collection(res3ColId), CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(listJob); + QVERIFY(!findCol(listJob->collections(), collection.name()).isValid()); +} + +void CollectionJobTest::testAncestorRetrieval() +{ + Collection col; + col.setRemoteId(QStringLiteral("10")); + + auto job = new CollectionFetchJob(col, CollectionFetchJob::Base, this); + job->fetchScope().setResource(QStringLiteral("akonadi_knut_resource_0")); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().count(), 1); + col = job->collections().first(); + QVERIFY(col.isValid()); + QVERIFY(col.parentCollection().isValid()); + QCOMPARE(col.parentCollection().remoteId(), QStringLiteral("6")); + QCOMPARE(col.parentCollection().parentCollection(), Collection::root()); + + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); + AKVERIFYEXEC(select); + Collection col2(col); + col2.setId(-1); // make it invalid but keep the ancestor chain + job = new CollectionFetchJob(col2, CollectionFetchJob::Base, this); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().count(), 1); + col2 = job->collections().first(); + QVERIFY(col2.isValid()); + QCOMPARE(col, col2); +} + +void CollectionJobTest::testAncestorAttributeRetrieval() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName(QStringLiteral("base")); + baseCol.attribute(Collection::AddIfMissing)->data = "new"; + auto create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName(QStringLiteral("enabled")); + auto create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + + auto job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + job->fetchScope().ancestorFetchScope().setFetchIdOnly(false); + job->fetchScope().ancestorFetchScope().fetchAttribute(); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.parentCollection().hasAttribute(), true); + } + + // Cleanup + auto deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +void CollectionJobTest::testListPreference() +{ + Akonadi::Collection baseCol; + { + baseCol.setParentCollection(Akonadi::Collection(res1ColId)); + baseCol.setName(QStringLiteral("base")); + auto create = new Akonadi::CollectionCreateJob(baseCol); + AKVERIFYEXEC(create); + baseCol = create->collection(); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setEnabled(true); + col.setName(QStringLiteral("enabled")); + auto create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + + auto job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), true); + QCOMPARE(result.localListPreference(Collection::ListDisplay), Collection::ListDefault); + QCOMPARE(result.localListPreference(Collection::ListSync), Collection::ListDefault); + QCOMPARE(result.localListPreference(Collection::ListIndex), Collection::ListDefault); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName(QStringLiteral("disabledPref")); + col.setEnabled(true); + col.setLocalListPreference(Collection::ListDisplay, Collection::ListDisabled); + col.setLocalListPreference(Collection::ListSync, Collection::ListDisabled); + col.setLocalListPreference(Collection::ListIndex, Collection::ListDisabled); + auto create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + auto job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), true); + QCOMPARE(result.localListPreference(Collection::ListDisplay), Collection::ListDisabled); + QCOMPARE(result.localListPreference(Collection::ListSync), Collection::ListDisabled); + QCOMPARE(result.localListPreference(Collection::ListIndex), Collection::ListDisabled); + } + { + Akonadi::Collection col; + col.setParentCollection(baseCol); + col.setName(QStringLiteral("enabledPref")); + col.setEnabled(false); + col.setLocalListPreference(Collection::ListDisplay, Collection::ListEnabled); + col.setLocalListPreference(Collection::ListSync, Collection::ListEnabled); + col.setLocalListPreference(Collection::ListIndex, Collection::ListEnabled); + auto create = new Akonadi::CollectionCreateJob(col); + AKVERIFYEXEC(create); + auto job = new CollectionFetchJob(create->collection(), CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Akonadi::Collection result = job->collections().first(); + QCOMPARE(result.enabled(), false); + QCOMPARE(result.localListPreference(Collection::ListDisplay), Collection::ListEnabled); + QCOMPARE(result.localListPreference(Collection::ListSync), Collection::ListEnabled); + QCOMPARE(result.localListPreference(Collection::ListIndex), Collection::ListEnabled); + } + + // Check list filter + { + auto job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Display); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + { + auto job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Sync); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + { + auto job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Index); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + { + auto job = new CollectionFetchJob(baseCol, CollectionFetchJob::FirstLevel); + job->fetchScope().setListFilter(CollectionFetchScope::Enabled); + AKVERIFYEXEC(job); + QCOMPARE(job->collections().size(), 2); + } + + // Cleanup + auto deleteJob = new CollectionDeleteJob(baseCol); + AKVERIFYEXEC(deleteJob); +} + +#include "collectionjobtest.moc" diff --git a/autotests/libs/collectionjobtest.h b/autotests/libs/collectionjobtest.h new file mode 100644 index 0000000..8a4e981 --- /dev/null +++ b/autotests/libs/collectionjobtest.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class CollectionJobTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testTopLevelList(); + void testFolderList(); + void testSignalOrder(); + void testNonRecursiveFolderList(); + void testEmptyFolderList(); + void testSearchFolderList(); + void testResourceFolderList(); + void testMimeTypeFilter(); + void testCreateDeleteFolder_data(); + void testCreateDeleteFolder(); + void testIllegalDeleteFolder(); + void testStatistics(); + void testModify_data(); + void testModify(); + void testIllegalModify(); + void testUtf8CollectionName_data(); + void testUtf8CollectionName(); + void testMultiList(); + void testMultiListInvalid(); + void testRecursiveMultiList(); + void testNonOverlappingRootList(); + void testRidFetch(); + void testRidCreateDelete_data(); + void testRidCreateDelete(); + void testAncestorRetrieval(); + void testAncestorAttributeRetrieval(); + void testListPreference(); +}; + diff --git a/autotests/libs/collectionmodifytest.cpp b/autotests/libs/collectionmodifytest.cpp new file mode 100644 index 0000000..d63fefd --- /dev/null +++ b/autotests/libs/collectionmodifytest.cpp @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "entitydisplayattribute.h" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class CollectionModifyTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testModifyCollection() + { + Collection col; + col.setName(QLatin1String("test_collection")); + col.setContentMimeTypes({Collection::mimeType()}); + col.setParentCollection(Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1")))); + col.setRights(Collection::AllRights); + + auto cj = new CollectionCreateJob(col, this); + AKVERIFYEXEC(cj); + col = cj->collection(); + QVERIFY(col.isValid()); + + auto attr = col.attribute(Collection::AddIfMissing); + attr->setDisplayName(QStringLiteral("Test Collection")); + col.setContentMimeTypes({Collection::mimeType(), QStringLiteral("application/octet-stream")}); + + auto mj = new CollectionModifyJob(col, this); + AKVERIFYEXEC(mj); + + auto fj = new CollectionFetchJob(col, CollectionFetchJob::Base); + AKVERIFYEXEC(fj); + QCOMPARE(fj->collections().count(), 1); + const Collection actual = fj->collections().at(0); + + QCOMPARE(actual.id(), col.id()); + QCOMPARE(actual.name(), col.name()); + QCOMPARE(actual.displayName(), col.displayName()); + QCOMPARE(actual.contentMimeTypes(), col.contentMimeTypes()); + QCOMPARE(actual.parentCollection(), col.parentCollection()); + QCOMPARE(actual.rights(), col.rights()); + + auto dj = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(dj); + } +}; + +QTEST_AKONADIMAIN(CollectionModifyTest) + +#include "collectionmodifytest.moc" diff --git a/autotests/libs/collectionmovetest.cpp b/autotests/libs/collectionmovetest.cpp new file mode 100644 index 0000000..d80dced --- /dev/null +++ b/autotests/libs/collectionmovetest.cpp @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2006, 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include "collectionfetchjob.h" +#include "collectionmovejob.h" +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "qtest_akonadi.h" + +#include + +using namespace Akonadi; + +class CollectionMoveTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testIllegalMove_data() + { + QTest::addColumn("source"); + QTest::addColumn("destination"); + + const Collection res1(AkonadiTest::collectionIdFromPath(QStringLiteral("res1"))); + const Collection res1foo(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + const Collection res1bla(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bar/bla"))); + + QTest::newRow("non-existing-target") << res1 << Collection(INT_MAX); + QTest::newRow("root") << Collection::root() << res1; + QTest::newRow("move-into-child") << res1 << res1foo; + QTest::newRow("same-name-in-target") << res1bla << res1foo; + QTest::newRow("non-existing-source") << Collection(INT_MAX) << res1; + } + + void testIllegalMove() + { + QFETCH(Collection, source); + QFETCH(Collection, destination); + QVERIFY(source.isValid()); + QVERIFY(destination.isValid()); + + auto mod = new CollectionMoveJob(source, destination, this); + QVERIFY(!mod->exec()); + } + + void testMove_data() + { + QTest::addColumn("source"); + QTest::addColumn("destination"); + QTest::addColumn("crossResource"); + + QTest::newRow("inter-resource") << Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1"))) + << Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2"))) << true; + QTest::newRow("intra-resource") << Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bla"))) + << Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bar/bla"))) << false; + } + + // TODO: test signals + void testMove() + { + QFETCH(Collection, source); + QFETCH(Collection, destination); + QFETCH(bool, crossResource); + QVERIFY(source.isValid()); + QVERIFY(destination.isValid()); + + auto fetch = new CollectionFetchJob(source, CollectionFetchJob::Base, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->collections().count(), 1); + source = fetch->collections().first(); + + // obtain reference listing + fetch = new CollectionFetchJob(source, CollectionFetchJob::Recursive); + AKVERIFYEXEC(fetch); + QHash referenceData; + foreach (const Collection &c, fetch->collections()) { + auto job = new ItemFetchJob(c, this); + AKVERIFYEXEC(job); + referenceData.insert(c, job->items()); + } + + // move collection + auto mod = new CollectionMoveJob(source, destination, this); + AKVERIFYEXEC(mod); + + // check if source was modified correctly + auto ljob = new CollectionFetchJob(source, CollectionFetchJob::Base); + AKVERIFYEXEC(ljob); + Collection::List list = ljob->collections(); + + QCOMPARE(list.count(), 1); + Collection col = list.first(); + QCOMPARE(col.name(), source.name()); + QCOMPARE(col.parentCollection(), destination); + + // list destination and check if everything is still there + ljob = new CollectionFetchJob(destination, CollectionFetchJob::Recursive); + AKVERIFYEXEC(ljob); + list = ljob->collections(); + + QVERIFY(list.count() >= referenceData.count()); + for (QHash::ConstIterator it = referenceData.constBegin(); it != referenceData.constEnd(); ++it) { + QVERIFY(list.contains(it.key())); + if (crossResource) { + QVERIFY(list[list.indexOf(it.key())].resource() != it.key().resource()); + } else { + QCOMPARE(list[list.indexOf(it.key())].resource(), it.key().resource()); + } + auto job = new ItemFetchJob(it.key(), this); + job->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), it.value().count()); + foreach (const Item &item, job->items()) { + QVERIFY(it.value().contains(item)); + QVERIFY(item.hasPayload()); + } + } + + // cleanup + mod = new CollectionMoveJob(col, source.parentCollection(), this); + AKVERIFYEXEC(mod); + } +}; + +QTEST_AKONADIMAIN(CollectionMoveTest) + +#include "collectionmovetest.moc" diff --git a/autotests/libs/collectionpathresolvertest.cpp b/autotests/libs/collectionpathresolvertest.cpp new file mode 100644 index 0000000..35950c9 --- /dev/null +++ b/autotests/libs/collectionpathresolvertest.cpp @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionpathresolvertest.h" +#include "collectionpathresolver.h" +#include "control.h" + +#include "qtest_akonadi.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(CollectionPathResolverTest) + +void CollectionPathResolverTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); +} + +void CollectionPathResolverTest::testPathResolver() +{ + auto resolver = new CollectionPathResolver(QStringLiteral("/res1/foo/bar/bla"), this); + AKVERIFYEXEC(resolver); + int col = resolver->collection(); + QVERIFY(col > 0); + + resolver = new CollectionPathResolver(Collection(col), this); + AKVERIFYEXEC(resolver); + QCOMPARE(resolver->path(), QStringLiteral("res1/foo/bar/bla")); +} + +void CollectionPathResolverTest::testRoot() +{ + auto resolver = new CollectionPathResolver(CollectionPathResolver::pathDelimiter(), this); + AKVERIFYEXEC(resolver); + QCOMPARE(resolver->collection(), Collection::root().id()); + + resolver = new CollectionPathResolver(Collection::root(), this); + AKVERIFYEXEC(resolver); + QVERIFY(resolver->path().isEmpty()); +} + +void CollectionPathResolverTest::testFailure() +{ + auto resolver = new CollectionPathResolver(QStringLiteral("/I/do not/exist"), this); + QVERIFY(!resolver->exec()); + + resolver = new CollectionPathResolver(Collection(INT_MAX), this); + QVERIFY(!resolver->exec()); +} diff --git a/autotests/libs/collectionpathresolvertest.h b/autotests/libs/collectionpathresolvertest.h new file mode 100644 index 0000000..4942732 --- /dev/null +++ b/autotests/libs/collectionpathresolvertest.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class CollectionPathResolverTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testPathResolver(); + void testRoot(); + void testFailure(); +}; + diff --git a/autotests/libs/collectionsynctest.cpp b/autotests/libs/collectionsynctest.cpp new file mode 100644 index 0000000..5636bb3 --- /dev/null +++ b/autotests/libs/collectionsynctest.cpp @@ -0,0 +1,446 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "collectionsync_p.h" +#include "control.h" +#include "entitydisplayattribute.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" + +#include + +#include +#include + +using namespace Akonadi; + +class CollectionSyncTest : public QObject +{ + Q_OBJECT +private: + Collection::List fetchCollections(const QString &res) + { + auto fetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + fetch->fetchScope().setResource(res); + fetch->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + if (!fetch->exec()) { + qWarning() << "CollectionFetchJob failed!"; + return Collection::List(); + } + return fetch->collections(); + } + + void makeTestData() + { + QTest::addColumn("hierarchicalRIDs"); + QTest::addColumn("resource"); + + QTest::newRow("akonadi_knut_resource_0 global RID") << false << "akonadi_knut_resource_0"; + QTest::newRow("akonadi_knut_resource_1 global RID") << false << "akonadi_knut_resource_1"; + QTest::newRow("akonadi_knut_resource_2 global RID") << false << "akonadi_knut_resource_2"; + + QTest::newRow("akonadi_knut_resource_0 hierarchical RID") << true << "akonadi_knut_resource_0"; + QTest::newRow("akonadi_knut_resource_1 hierarchical RID") << true << "akonadi_knut_resource_1"; + QTest::newRow("akonadi_knut_resource_2 hierarchical RID") << true << "akonadi_knut_resource_2"; + } + + Collection createCollection(const QString &name, const QString &remoteId, const Collection &parent) + { + Collection c; + c.setName(name); + c.setRemoteId(remoteId); + c.setParentCollection(parent); + c.setResource(QStringLiteral("akonadi_knut_resource_0")); + c.setContentMimeTypes(QStringList() << Collection::mimeType()); + return c; + } + + Collection::List prepareBenchmark() + { + Collection::List collections = fetchCollections(QStringLiteral("akonadi_knut_resource_0")); + + auto resJob = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + Q_ASSERT(resJob->exec()); + + Collection root; + Q_FOREACH (const Collection &col, collections) { + if (col.parentCollection() == Collection::root()) { + root = col; + break; + } + } + Q_ASSERT(root.isValid()); + + // we must build on top of existing collections, because only resource is + // allowed to create top-level collection + Collection::List baseCollections; + for (int i = 0; i < 20; ++i) { + baseCollections << createCollection(QStringLiteral("Base Col %1").arg(i), QStringLiteral("/baseCol%1").arg(i), root); + } + collections += baseCollections; + + const Collection shared = createCollection(QStringLiteral("Shared collections"), QStringLiteral("/shared"), root); + baseCollections << shared; + collections << shared; + for (int i = 0; i < 10000; ++i) { + const Collection col = createCollection(QStringLiteral("Shared Col %1").arg(i), QStringLiteral("/shared%1").arg(i), shared); + collections << col; + for (int j = 0; j < 6; ++j) { + collections << createCollection(QStringLiteral("Shared Subcol %1-%2").arg(i).arg(j), QStringLiteral("/shared%1-%2").arg(i).arg(j), col); + } + } + return collections; + } + + CollectionSync *prepareBenchmarkSyncer(const Collection::List &collections) + { + auto syncer = new CollectionSync(QStringLiteral("akonadi_knut_resource_0")); + connect(syncer, SIGNAL(percent(KJob *, ulong)), this, SLOT(syncBenchmarkProgress(KJob *, ulong))); + syncer->setHierarchicalRemoteIds(false); + syncer->setRemoteCollections(collections); + return syncer; + } + + void cleanupBenchmark(const Collection::List &collections) + { + Collection::List baseCols; + for (const Collection &col : collections) { + if (col.remoteId().startsWith(QLatin1String("/baseCol")) || col.remoteId() == QLatin1String("/shared")) { + baseCols << col; + } + } + for (const Collection &col : std::as_const(baseCols)) { + auto del = new CollectionDeleteJob(col); + AKVERIFYEXEC(del); + } + } + +public Q_SLOTS: + void syncBenchmarkProgress(KJob *job, ulong percent) + { + Q_UNUSED(job) + qDebug() << "CollectionSync progress:" << percent << "%"; + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + qRegisterMetaType(); + } + + void testFullSync_data() + { + makeTestData(); + } + + void testFullSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + auto syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setRemoteCollections(origCols); + AKVERIFYEXEC(syncer); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + } + + void testFullStreamingSync_data() + { + makeTestData(); + } + + void testFullStreamingSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + auto syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setAutoDelete(false); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + QTest::qWait(10); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origCols.count(); ++i) { + Collection::List l; + l << origCols[i]; + syncer->setRemoteCollections(l); + if (i < origCols.count() - 1) { + QTest::qWait(10); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + syncer->retrievalDone(); + QTRY_COMPARE(spy.count(), 1); + QCOMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->errorText(), QString()); + QCOMPARE(job->error(), 0); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + + delete syncer; + } + + void testIncrementalSync_data() + { + makeTestData(); + } + + void testIncrementalSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + if (resource == QLatin1String("akonadi_knut_resource_2")) { + QSKIP("test requires more than one collection", SkipSingle); + } + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + auto syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setRemoteCollections(origCols, Collection::List()); + AKVERIFYEXEC(syncer); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + + // Find leaf collections that we can delete + Collection::List leafCols = resultCols; + for (auto iter = leafCols.begin(); iter != leafCols.end();) { + bool found = false; + Q_FOREACH (const Collection &c, resultCols) { + if (c.parentCollection().id() == iter->id()) { + iter = leafCols.erase(iter); + found = true; + break; + } + } + if (!found) { + ++iter; + } + } + QVERIFY(!leafCols.isEmpty()); + Collection::List delCols; + delCols << leafCols.first(); + resultCols.removeOne(leafCols.first()); + + // ### not implemented yet I guess +#if 0 + Collection colWithOnlyRemoteId; + colWithOnlyRemoteId.setRemoteId(resultCols.front().remoteId()); + delCols << colWithOnlyRemoteId; + resultCols.pop_front(); +#endif + +#if 0 + // ### should this work? + Collection colWithRandomRemoteId; + colWithRandomRemoteId.setRemoteId(KRandom::randomString(100)); + delCols << colWithRandomRemoteId; +#endif + + syncer = new CollectionSync(resource, this); + syncer->setRemoteCollections(resultCols, delCols); + AKVERIFYEXEC(syncer); + + Collection::List resultCols2 = fetchCollections(resource); + QCOMPARE(resultCols2.count(), resultCols.count()); + } + + void testIncrementalStreamingSync_data() + { + makeTestData(); + } + + void testIncrementalStreamingSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + auto syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setAutoDelete(false); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + QTest::qWait(10); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origCols.count(); ++i) { + Collection::List l; + l << origCols[i]; + syncer->setRemoteCollections(l, Collection::List()); + if (i < origCols.count() - 1) { + QTest::qWait(10); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + syncer->retrievalDone(); + QTRY_COMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->errorText(), QString()); + QCOMPARE(job->error(), 0); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + + delete syncer; + } + + void testEmptyIncrementalSync_data() + { + makeTestData(); + } + + void testEmptyIncrementalSync() + { + QFETCH(bool, hierarchicalRIDs); + QFETCH(QString, resource); + + Collection::List origCols = fetchCollections(resource); + QVERIFY(!origCols.isEmpty()); + + auto syncer = new CollectionSync(resource, this); + syncer->setHierarchicalRemoteIds(hierarchicalRIDs); + syncer->setRemoteCollections(Collection::List(), Collection::List()); + AKVERIFYEXEC(syncer); + + Collection::List resultCols = fetchCollections(resource); + QCOMPARE(resultCols.count(), origCols.count()); + } + + void testAttributeChanges_data() + { + QTest::addColumn("keepLocalChanges"); + QTest::newRow("keep local changes") << true; + QTest::newRow("overwrite local changes") << false; + } + + void testAttributeChanges() + { + QFETCH(bool, keepLocalChanges); + const QString resource(QStringLiteral("akonadi_knut_resource_0")); + Collection col = fetchCollections(resource).first(); + col.attribute(Akonadi::Collection::AddIfMissing)->setDisplayName(QStringLiteral("foo")); + col.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("foo")); + { + auto job = new CollectionModifyJob(col); + AKVERIFYEXEC(job); + } + + col.attribute()->setDisplayName(QStringLiteral("default")); + col.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("default")); + + auto syncer = new CollectionSync(resource, this); + if (keepLocalChanges) { + syncer->setKeepLocalChanges(QSet() << "ENTITYDISPLAY" + << "CONTENTMIMETYPES"); + } else { + syncer->setKeepLocalChanges(QSet()); + } + + syncer->setRemoteCollections(Collection::List() << col, Collection::List()); + AKVERIFYEXEC(syncer); + + { + auto job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base); + AKVERIFYEXEC(job); + Collection resultCol = job->collections().first(); + if (keepLocalChanges) { + QCOMPARE(resultCol.displayName(), QString::fromLatin1("foo")); + QVERIFY(resultCol.contentMimeTypes().contains(QLatin1String("foo"))); + } else { + QCOMPARE(resultCol.displayName(), QString::fromLatin1("default")); + QVERIFY(resultCol.contentMimeTypes().contains(QLatin1String("default"))); + } + } + } + + void testCancelation() + { + const QString resource(QStringLiteral("akonadi_knut_resource_0")); + Collection col = fetchCollections(resource).first(); + + auto syncer = new CollectionSync(resource, this); + syncer->setStreamingEnabled(true); + syncer->setRemoteCollections({col}, {}); + + QSignalSpy spy(syncer, &CollectionSync::result); + QVERIFY(spy.isValid()); + + syncer->rollback(); + + QTRY_VERIFY(!spy.empty()); + QVERIFY(syncer->error()); + } + +// Disabled by default, because they take ~15 minutes to complete +#if 0 + void benchmarkInitialSync() + { + const Collection::List collections = prepareBenchmark(); + + CollectionSync *syncer = prepareBenchmarkSyncer(collections); + + QBENCHMARK_ONCE { + AKVERIFYEXEC(syncer); + } + + cleanupBenchmark(collections); + } + + void benchmarkIncrementalSync() + { + const Collection::List collections = prepareBenchmark(); + + // First populate Akonadi with Collections + CollectionSync *syncer = prepareBenchmarkSyncer(collections); + AKVERIFYEXEC(syncer); + + // Now create a new syncer to benchmark the incremental sync + syncer = prepareBenchmarkSyncer(collections); + + QBENCHMARK_ONCE { + AKVERIFYEXEC(syncer); + } + + cleanupBenchmark(collections); + } +#endif +}; + +QTEST_AKONADIMAIN(CollectionSyncTest) + +#include "collectionsynctest.moc" diff --git a/autotests/libs/collectionutilstest.cpp b/autotests/libs/collectionutilstest.cpp new file mode 100644 index 0000000..0f034a3 --- /dev/null +++ b/autotests/libs/collectionutilstest.cpp @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "../collectionutils.h" +#include "collection.h" +#include + +using namespace Akonadi; + +class CollectionUtilsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testHasValidHierarchicalRID_data() + { + QTest::addColumn("collection"); + QTest::addColumn("isHRID"); + + QTest::newRow("empty") << Collection() << false; + QTest::newRow("root") << Collection::root() << true; + Collection c; + c.setParentCollection(Collection::root()); + QTest::newRow("one level not ok") << c << false; + c.setRemoteId(QStringLiteral("r1")); + QTest::newRow("one level ok") << c << true; + Collection c2; + c2.setParentCollection(c); + QTest::newRow("two level not ok") << c2 << false; + c2.setRemoteId(QStringLiteral("r2")); + QTest::newRow("two level ok") << c2 << true; + c2.parentCollection().setRemoteId(QString()); + QTest::newRow("mid RID missing") << c2 << false; + } + + void testHasValidHierarchicalRID() + { + QFETCH(Collection, collection); + QFETCH(bool, isHRID); + QCOMPARE(CollectionUtils::hasValidHierarchicalRID(collection), isHRID); + } + + void testPersistentParentCollection() + { + Collection col1(1); + Collection col2(2); + Collection col3(3); + + col2.setParentCollection(col3); + col1.setParentCollection(col2); + + Collection assigned = col1; + QCOMPARE(assigned.parentCollection(), col2); + QCOMPARE(assigned.parentCollection().parentCollection(), col3); + + Collection copied(col1); + QCOMPARE(copied.parentCollection(), col2); + QCOMPARE(copied.parentCollection().parentCollection(), col3); + } +}; + +QTEST_AKONADIMAIN(CollectionUtilsTest) + +#include "collectionutilstest.moc" diff --git a/autotests/libs/conflictresolvedialogtest.cpp b/autotests/libs/conflictresolvedialogtest.cpp new file mode 100644 index 0000000..5c5e0dc --- /dev/null +++ b/autotests/libs/conflictresolvedialogtest.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2017-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "conflictresolvedialogtest.h" +#include "../src/widgets/conflictresolvedialog_p.h" + +#include +#include +#include +#include + +QTEST_MAIN(ConflictResolveDialogTest) + +ConflictResolveDialogTest::ConflictResolveDialogTest(QObject *parent) + : QObject(parent) +{ +} + +void ConflictResolveDialogTest::shouldHaveDefaultValues() +{ + Akonadi::ConflictResolveDialog dlg; + + QVERIFY(!dlg.windowTitle().isEmpty()); + + auto takeLeftButton = dlg.findChild(QStringLiteral("takeLeftButton")); + QVERIFY(takeLeftButton); + QVERIFY(!takeLeftButton->text().isEmpty()); + + auto takeRightButton = dlg.findChild(QStringLiteral("takeRightButton")); + QVERIFY(takeRightButton); + QVERIFY(!takeRightButton->text().isEmpty()); + + auto keepBothButton = dlg.findChild(QStringLiteral("keepBothButton")); + QVERIFY(keepBothButton); + QVERIFY(!keepBothButton->text().isEmpty()); + QVERIFY(keepBothButton->isDefault()); + + auto mView = dlg.findChild(QStringLiteral("view")); + QVERIFY(mView); + QVERIFY(mView->toPlainText().isEmpty()); + + auto docuLabel = dlg.findChild(QStringLiteral("doculabel")); + QVERIFY(docuLabel); + QVERIFY(!docuLabel->text().isEmpty()); + QVERIFY(docuLabel->wordWrap()); + QCOMPARE(docuLabel->contextMenuPolicy(), Qt::NoContextMenu); +} diff --git a/autotests/libs/conflictresolvedialogtest.h b/autotests/libs/conflictresolvedialogtest.h new file mode 100644 index 0000000..2c4e985 --- /dev/null +++ b/autotests/libs/conflictresolvedialogtest.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2017-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ConflictResolveDialogTest : public QObject +{ + Q_OBJECT +public: + explicit ConflictResolveDialogTest(QObject *parent = nullptr); + ~ConflictResolveDialogTest() override = default; + +private Q_SLOTS: + void shouldHaveDefaultValues(); +}; + diff --git a/autotests/libs/entitycachetest.cpp b/autotests/libs/entitycachetest.cpp new file mode 100644 index 0000000..0a10597 --- /dev/null +++ b/autotests/libs/entitycachetest.cpp @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitycache_p.h" + +#include +#include + +using namespace Akonadi; + +class EntityCacheTest : public QObject +{ + Q_OBJECT +private: + template void testCache() + { + EntityCache cache(2); + QSignalSpy spy(&cache, SIGNAL(dataAvailable())); + QVERIFY(spy.isValid()); + + QVERIFY(!cache.isCached(1)); + QVERIFY(!cache.isRequested(1)); + QVERIFY(!cache.retrieve(1).isValid()); + + FetchScope scope; + scope.setAncestorRetrieval(FetchScope::All); + + cache.request(1, scope); + QVERIFY(!cache.isCached(1)); + QVERIFY(cache.isRequested(1)); + QVERIFY(!cache.retrieve(1).isValid()); + + QTRY_COMPARE(spy.count(), 1); + QVERIFY(cache.isCached(1)); + QVERIFY(cache.isRequested(1)); + const T e1 = cache.retrieve(1); + QCOMPARE(e1.id(), 1LL); + QVERIFY(e1.parentCollection().isValid()); + QVERIFY(!e1.parentCollection().remoteId().isEmpty() || e1.parentCollection() == Collection::root()); + + spy.clear(); + cache.request(2, FetchScope()); + cache.request(3, FetchScope()); + + QVERIFY(!cache.isCached(1)); + QVERIFY(!cache.isRequested(1)); + QVERIFY(cache.isRequested(2)); + QVERIFY(cache.isRequested(3)); + + cache.invalidate(2); + + QTRY_COMPARE(spy.count(), 2); + QVERIFY(cache.isCached(2)); + QVERIFY(cache.isCached(3)); + + const T e2 = cache.retrieve(2); + const T e3a = cache.retrieve(3); + QCOMPARE(e3a.id(), 3LL); + QVERIFY(!e2.isValid()); + + cache.invalidate(3); + const T e3b = cache.retrieve(3); + QVERIFY(!e3b.isValid()); + + spy.clear(); + // updating a cached entry removes it + cache.update(3, FetchScope()); + cache.update(3, FetchScope()); + QVERIFY(!cache.isCached(3)); + QVERIFY(!cache.isRequested(3)); + QVERIFY(!cache.retrieve(3).isValid()); + + // updating a pending entry re-fetches + cache.request(3, FetchScope()); + cache.update(3, FetchScope()); + QVERIFY(!cache.isCached(3)); + QVERIFY(cache.isRequested(3)); + cache.update(3, FetchScope()); + QVERIFY(!cache.isCached(3)); + QVERIFY(cache.isRequested(3)); + + QTRY_COMPARE(spy.count(), 3); + QVERIFY(cache.isCached(3)); + QVERIFY(cache.retrieve(3).isValid()); + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testCacheGeneric_data() + { + QTest::addColumn("collection"); + QTest::newRow("collection") << true; + QTest::newRow("item") << false; + } + + void testCacheGeneric() + { + QFETCH(bool, collection); + if (collection) { + testCache(); + } else { + testCache(); + } + } + + void testListCacheGeneric_data() + { + QTest::addColumn("collection"); + QTest::newRow("collection") << true; + QTest::newRow("item") << false; + } + + void testItemCache() + { + ItemCache cache(1); + QSignalSpy spy(&cache, &EntityCacheBase::dataAvailable); + QVERIFY(spy.isValid()); + + ItemFetchScope scope; + scope.fetchFullPayload(true); + cache.request(1, scope); + + QTRY_COMPARE(spy.count(), 1); + QVERIFY(cache.isCached(1)); + QVERIFY(cache.isRequested(1)); + const Item item = cache.retrieve(1); + QCOMPARE(item.id(), 1LL); + QVERIFY(item.hasPayload()); + } + + void testListCache_ensureCached() + { + ItemFetchScope scope; + + EntityListCache cache(3); + QSignalSpy spy(&cache, &EntityCacheBase::dataAvailable); + QVERIFY(spy.isValid()); + + cache.request(QList() << 1 << 2 << 3, scope); + QTRY_COMPARE(spy.count(), 1); + QVERIFY(cache.isCached(QList() << 1 << 2 << 3)); + + cache.ensureCached(QList() << 1 << 2 << 3 << 4, scope); + QTRY_COMPARE(spy.count(), 2); + QVERIFY(cache.isCached(QList() << 1 << 2 << 3 << 4)); + } +}; + +QTEST_AKONADIMAIN(EntityCacheTest) + +#include "entitycachetest.moc" diff --git a/autotests/libs/entitydisplayattributetest.cpp b/autotests/libs/entitydisplayattributetest.cpp new file mode 100644 index 0000000..3b53bdb --- /dev/null +++ b/autotests/libs/entitydisplayattributetest.cpp @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitydisplayattribute.h" + +#include + +#include + +using namespace Akonadi; + +class EntityDisplayAttributeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDeserialize_data() + { + QTest::addColumn("input"); + QTest::addColumn("name"); + QTest::addColumn("icon"); + QTest::addColumn("activeIcon"); + QTest::addColumn("output"); + + QTest::newRow("empty") << QByteArray(R"(("" ""))") << QString() << QString() << QString() << QByteArray(R"(("" "" "" ()))"); + QTest::newRow("name+icon") << QByteArray(R"(("name" "icon"))") << QStringLiteral("name") << QStringLiteral("icon") << QString() + << QByteArray(R"(("name" "icon" "" ()))"); + QTest::newRow("name+icon+activeIcon") << QByteArray(R"(("name" "icon" "activeIcon"))") << QStringLiteral("name") << QStringLiteral("icon") + << QStringLiteral("activeIcon") << QByteArray(R"(("name" "icon" "activeIcon" ()))"); + } + + void testDeserialize() + { + QFETCH(QByteArray, input); + QFETCH(QString, name); + QFETCH(QString, icon); + QFETCH(QString, activeIcon); + QFETCH(QByteArray, output); + + auto attr = new EntityDisplayAttribute(); + attr->deserialize(input); + QCOMPARE(attr->displayName(), name); + QCOMPARE(attr->iconName(), icon); + QCOMPARE(attr->activeIconName(), activeIcon); + + QCOMPARE(attr->serialized(), output); + + EntityDisplayAttribute *copy = attr->clone(); + QCOMPARE(copy->serialized(), output); + + delete attr; + delete copy; + } +}; + +QTEST_MAIN(EntityDisplayAttributeTest) + +#include "entitydisplayattributetest.moc" diff --git a/autotests/libs/entitytreemodeltest.cpp b/autotests/libs/entitytreemodeltest.cpp new file mode 100644 index 0000000..cc6492d --- /dev/null +++ b/autotests/libs/entitytreemodeltest.cpp @@ -0,0 +1,662 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "entitydisplayattribute.h" +#include "entitytreemodel.h" +#include "entitytreemodel_p.h" +#include "fakemonitor.h" +#include "fakeserverdata.h" +#include "fakesession.h" +#include "imapparser_p.h" +#include "modelspy.h" + +static const char serverContent1[] = + // The format of these lines are first a type, either 'C' or 'I' for Item and collection. + // The dashes show the depth in the hierarchy + // Collections have a list of mimetypes they can contain, followed by an optional + // displayName which is put into the EntityDisplayAttribute, followed by an optional order + // which is the order in which the collections are returned from the job to the ETM. + + "- C (inode/directory) 'Col 1' 4" + "- - C (text/directory, message/rfc822) 'Col 2' 3" + // Items just have the mimetype they contain in the payload. + "- - - I text/directory 'Item 1'" + "- - - I text/directory 'Item 2'" + "- - - I message/rfc822 'Item 3'" + "- - - I message/rfc822 'Item 4'" + "- - C (text/directory) 'Col 3' 3" + "- - - C (text/directory) 'Col 4' 2" + "- - - - C (text/directory) 'Col 5' 1" // <-- First collection to be returned + "- - - - - I text/directory 'Item 5'" + "- - - - - I text/directory 'Item 6'" + "- - - - I text/directory 'Item 7'" + "- - - I text/directory 'Item 8'" + "- - - I text/directory 'Item 9'" + "- - C (message/rfc822) 'Col 6' 3" + "- - - I message/rfc822 'Item 10'" + "- - - I message/rfc822 'Item 11'" + "- - C (text/directory, message/rfc822) 'Col 7' 3" + "- - - I text/directory 'Item 12'" + "- - - I text/directory 'Item 13'" + "- - - I message/rfc822 'Item 14'" + "- - - I message/rfc822 'Item 15'"; + +/** + * This test verifies that the ETM reacts as expected to signals from the monitor. + * + * The tested ETM is only talking to fake components so the reaction of the ETM to each signal can be tested. + * + * WARNING: This test does no handle jobs issued by the model. It simply shortcuts (calls emitResult) them, and the connected + * slots are never executed (because the eventloop is not run after emitResult is called). + * This test therefore only tests the reaction of the model to signals of the monitor and not the overall behaviour. + */ +class EntityTreeModelTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void testInitialFetch(); + void testCollectionMove_data(); + void testCollectionMove(); + void testCollectionAdded_data(); + void testCollectionAdded(); + void testCollectionRemoved_data(); + void testCollectionRemoved(); + void testCollectionChanged_data(); + void testCollectionChanged(); + void testItemMove_data(); + void testItemMove(); + void testItemAdded_data(); + void testItemAdded(); + void testItemRemoved_data(); + void testItemRemoved(); + void testItemChanged_data(); + void testItemChanged(); + void testRemoveCollectionOnChanged(); + +private: + QPair populateModel(const QString &serverContent, const QString &mimeType = QString()) + { + auto const fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + if (!mimeType.isEmpty()) { + fakeMonitor->setMimeTypeMonitored(mimeType); + } + auto const model = new EntityTreeModel(fakeMonitor, this); + + m_modelSpy = new ModelSpy{model, this}; + + auto const serverData = new FakeServerData(model, m_fakeSession, fakeMonitor, this); + serverData->setCommands(FakeJobResponse::interpret(serverData, serverContent)); + + // Give the model a chance to populate + QTest::qWait(100); + return qMakePair(serverData, model); + } + +private: + ModelSpy *m_modelSpy = nullptr; + FakeSession *m_fakeSession = nullptr; + QByteArray m_sessionName; +}; + +QModelIndex firstMatchedIndex(const QAbstractItemModel &model, const QString &pattern) +{ + if (pattern.isEmpty()) { + return {}; + } + const auto list = model.match(model.index(0, 0), Qt::DisplayRole, pattern, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + return list.first(); +} + +void EntityTreeModelTest::initTestCase() +{ + m_sessionName = "EntityTreeModelTest fake session"; + m_fakeSession = new FakeSession(m_sessionName, FakeSession::EndJobsImmediately); + m_fakeSession->setAsDefaultSession(); + + qRegisterMetaType("QModelIndex"); +} + +void EntityTreeModelTest::cleanupTestCase() +{ + delete m_fakeSession; +} + +void EntityTreeModelTest::testInitialFetch() +{ + auto const fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + auto const model = new EntityTreeModel(fakeMonitor, this); + + auto const serverData = new FakeServerData(model, m_fakeSession, fakeMonitor, this); + serverData->setCommands(FakeJobResponse::interpret(serverData, QString::fromLatin1(serverContent1))); + + m_modelSpy = new ModelSpy(model, this); + m_modelSpy->startSpying(); + + const QList expectedSignals{// First the model gets a signal about the first collection to be returned, which is not a top-level collection. + // It uses the parentCollection hierarchy to put placeholder collections in the model until the root is reached. + // Then it inserts only one row and emits the correct signals. After that, when the other collections + // arrive, dataChanged is emitted for them. + {RowsAboutToBeInserted, 0, 0}, + {RowsInserted, 0, 0}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 4")}}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 3")}}, + // New collections are prepended + {RowsAboutToBeInserted, 0, 0, QStringLiteral("Collection 1")}, + {RowsInserted, 0, 0, QStringLiteral("Collection 1"), QVariantList{QStringLiteral("Col 2")}}, + {RowsAboutToBeInserted, 0, 0, QStringLiteral("Collection 1")}, + {RowsInserted, 0, 0, QStringLiteral("Collection 1"), QVariantList{QStringLiteral("Col 6")}}, + {RowsAboutToBeInserted, 0, 0, QStringLiteral("Collection 1")}, + {RowsInserted, 0, 0, QStringLiteral("Collection 1"), QVariantList{QStringLiteral("Col 7")}}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 1")}}, + // The items in the collections are appended. + {RowsAboutToBeInserted, 0, 3, QStringLiteral("Col 2")}, + {RowsInserted, 0, 3, QStringLiteral("Col 2")}, + {RowsAboutToBeInserted, 0, 1, QStringLiteral("Col 5")}, + {RowsInserted, 0, 1, QStringLiteral("Col 5")}, + {RowsAboutToBeInserted, 1, 1, QStringLiteral("Col 4")}, + {RowsInserted, 1, 1, QStringLiteral("Col 4")}, + {RowsAboutToBeInserted, 1, 2, QStringLiteral("Col 3")}, + {RowsInserted, 1, 2, QStringLiteral("Col 3")}, + {RowsAboutToBeInserted, 0, 1, QStringLiteral("Col 6")}, + {RowsInserted, 0, 1, QStringLiteral("Col 6")}, + {RowsAboutToBeInserted, 0, 3, QStringLiteral("Col 7")}, + {RowsInserted, 0, 3, QStringLiteral("Col 7")}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 1")}}, + {DataChanged, 3, 3, QVariantList{QStringLiteral("Col 3")}}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 5")}}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 4")}}, + {DataChanged, 2, 2, QVariantList{QStringLiteral("Col 2")}}, + {DataChanged, 1, 1, QVariantList{QStringLiteral("Col 7")}}, + {DataChanged, 0, 0, QVariantList{QStringLiteral("Col 6")}}}; + m_modelSpy->setExpectedSignals(expectedSignals); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(10); + + // We get all the signals we expected. + QTRY_VERIFY(m_modelSpy->expectedSignals().isEmpty()); + + QTest::qWait(10); + // We didn't get signals we didn't expect. + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionMove_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("movedCollection"); + QTest::addColumn("targetCollection"); + + QTest::newRow("move-collection01") << serverContent1 << "Col 5" + << "Col 1"; + QTest::newRow("move-collection02") << serverContent1 << "Col 5" + << "Col 2"; + QTest::newRow("move-collection03") << serverContent1 << "Col 5" + << "Col 3"; + QTest::newRow("move-collection04") << serverContent1 << "Col 5" + << "Col 6"; + QTest::newRow("move-collection05") << serverContent1 << "Col 5" + << "Col 7"; + QTest::newRow("move-collection06") << serverContent1 << "Col 3" + << "Col 2"; + QTest::newRow("move-collection07") << serverContent1 << "Col 3" + << "Col 6"; + QTest::newRow("move-collection08") << serverContent1 << "Col 3" + << "Col 7"; + QTest::newRow("move-collection09") << serverContent1 << "Col 7" + << "Col 2"; + QTest::newRow("move-collection10") << serverContent1 << "Col 7" + << "Col 5"; + QTest::newRow("move-collection11") << serverContent1 << "Col 7" + << "Col 4"; + QTest::newRow("move-collection12") << serverContent1 << "Col 7" + << "Col 3"; +} + +void EntityTreeModelTest::testCollectionMove() +{ + QFETCH(QString, serverContent); + QFETCH(QString, movedCollection); + QFETCH(QString, targetCollection); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto movedIndex = firstMatchedIndex(*model, movedCollection); + Q_ASSERT(movedIndex.isValid()); + const auto sourceCollection = movedIndex.parent().data().toString(); + const auto sourceRow = movedIndex.row(); + + auto const moveCommand = new FakeCollectionMovedCommand(movedCollection, sourceCollection, targetCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({moveCommand}); + + const QList expectedSignals{{RowsAboutToBeMoved, sourceRow, sourceRow, sourceCollection, 0, targetCollection, {movedCollection}}, + {RowsMoved, sourceRow, sourceRow, sourceCollection, 0, targetCollection, {movedCollection}}}; + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionAdded_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("addedCollection"); + QTest::addColumn("parentCollection"); + + QTest::newRow("add-collection01") << serverContent1 << "new Collection" + << "Col 1"; + QTest::newRow("add-collection02") << serverContent1 << "new Collection" + << "Col 2"; + QTest::newRow("add-collection03") << serverContent1 << "new Collection" + << "Col 3"; + QTest::newRow("add-collection04") << serverContent1 << "new Collection" + << "Col 4"; + QTest::newRow("add-collection05") << serverContent1 << "new Collection" + << "Col 5"; + QTest::newRow("add-collection06") << serverContent1 << "new Collection" + << "Col 6"; + QTest::newRow("add-collection07") << serverContent1 << "new Collection" + << "Col 7"; +} + +void EntityTreeModelTest::testCollectionAdded() +{ + QFETCH(QString, serverContent); + QFETCH(QString, addedCollection); + QFETCH(QString, parentCollection); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + + auto const addCommand = new FakeCollectionAddedCommand(addedCollection, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({addCommand}); + + const QList expectedSignals{ + {RowsAboutToBeInserted, 0, 0, parentCollection, QVariantList{addedCollection}}, + {RowsInserted, 0, 0, parentCollection, QVariantList{addedCollection}}, + // The data changed signal comes from the item fetch job that is triggered because we have ImmediatePopulation enabled + {DataChanged, 0, 0, parentCollection, QVariantList{addedCollection}}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionRemoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("removedCollection"); + + // The test suite doesn't handle this case yet. + // QTest::newRow("remove-collection01") << serverContent1 << "Col 1"; + QTest::newRow("remove-collection02") << serverContent1 << "Col 2"; + QTest::newRow("remove-collection03") << serverContent1 << "Col 3"; + QTest::newRow("remove-collection04") << serverContent1 << "Col 4"; + QTest::newRow("remove-collection05") << serverContent1 << "Col 5"; + QTest::newRow("remove-collection06") << serverContent1 << "Col 6"; + QTest::newRow("remove-collection07") << serverContent1 << "Col 7"; +} + +void EntityTreeModelTest::testCollectionRemoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, removedCollection); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto removedIndex = firstMatchedIndex(*model, removedCollection); + const auto parentCollection = removedIndex.parent().data().toString(); + const auto sourceRow = removedIndex.row(); + + auto const removeCommand = new FakeCollectionRemovedCommand(removedCollection, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({removeCommand}); + + const QList expectedSignals{{RowsAboutToBeRemoved, sourceRow, sourceRow, parentCollection, QVariantList{removedCollection}}, + {RowsRemoved, sourceRow, sourceRow, parentCollection, QVariantList{removedCollection}}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testCollectionChanged_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("collectionName"); + QTest::addColumn("monitoredMimeType"); + + QTest::newRow("change-collection01") << serverContent1 << "Col 1" << QString(); + QTest::newRow("change-collection02") << serverContent1 << "Col 2" << QString(); + QTest::newRow("change-collection03") << serverContent1 << "Col 3" << QString(); + QTest::newRow("change-collection04") << serverContent1 << "Col 4" << QString(); + QTest::newRow("change-collection05") << serverContent1 << "Col 5" << QString(); + QTest::newRow("change-collection06") << serverContent1 << "Col 6" << QString(); + QTest::newRow("change-collection07") << serverContent1 << "Col 7" << QString(); + // Don't remove the parent due to a missing mimetype + QTest::newRow("change-collection08") << serverContent1 << "Col 1" << QStringLiteral("message/rfc822"); +} + +void EntityTreeModelTest::testCollectionChanged() +{ + QFETCH(QString, serverContent); + QFETCH(QString, collectionName); + QFETCH(QString, monitoredMimeType); // ##### TODO: this is unused. Is this test correct? + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto changedIndex = firstMatchedIndex(*model, collectionName); + const auto parentCollection = changedIndex.parent().data().toString(); + qDebug() << parentCollection; + const auto changedRow = changedIndex.row(); + + auto const changeCommand = new FakeCollectionChangedCommand(collectionName, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({changeCommand}); + + const QList expectedSignals{{DataChanged, changedRow, changedRow, parentCollection, QVariantList{collectionName}}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemMove_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("movedItem"); + QTest::addColumn("targetCollection"); + + QTest::newRow("move-item01") << serverContent1 << "Item 1" + << "Col 7"; + QTest::newRow("move-item02") << serverContent1 << "Item 5" + << "Col 4"; // Move item to grandparent. + QTest::newRow("move-item03") << serverContent1 << "Item 7" + << "Col 5"; // Move item to sibling. + QTest::newRow("move-item04") << serverContent1 << "Item 8" + << "Col 5"; // Move item to nephew + QTest::newRow("move-item05") << serverContent1 << "Item 8" + << "Col 6"; // Move item to uncle + QTest::newRow("move-item02") << serverContent1 << "Item 5" + << "Col 3"; // Move item to great-grandparent. +} + +void EntityTreeModelTest::testItemMove() +{ + QFETCH(QString, serverContent); + QFETCH(QString, movedItem); + QFETCH(QString, targetCollection); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto movedIndex = firstMatchedIndex(*model, movedItem); + const auto sourceCollection = movedIndex.parent().data().toString(); + const auto sourceRow = movedIndex.row(); + + const auto targetIndex = firstMatchedIndex(*model, targetCollection); + const auto targetRow = model->rowCount(targetIndex); + + auto const moveCommand = new FakeItemMovedCommand(movedItem, sourceCollection, targetCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({moveCommand}); + + const QList expectedSignals{ + // Currently moves are implemented as remove + insert in the ETM. + {RowsAboutToBeRemoved, sourceRow, sourceRow, sourceCollection, QVariantList{movedItem}}, + {RowsRemoved, sourceRow, sourceRow, sourceCollection, QVariantList{movedItem}}, + {RowsAboutToBeInserted, targetRow, targetRow, targetCollection, QVariantList{movedItem}}, + {RowsInserted, targetRow, targetRow, targetCollection, QVariantList{movedItem}}, + // {RowsAboutToBeMoved, sourceRow, sourceRow, sourceCollection, targetRow, targetCollection, QVariantList{movedItem}}, + // {RowsMoved, sourceRow, sourceRow, sourceCollection, targetRow, targetCollection, QVariantList{movedItem}}, + }; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemAdded_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("addedItem"); + QTest::addColumn("parentCollection"); + + QTest::newRow("add-item01") << serverContent1 << "new Item" + << "Col 1"; + QTest::newRow("add-item02") << serverContent1 << "new Item" + << "Col 2"; + QTest::newRow("add-item03") << serverContent1 << "new Item" + << "Col 3"; + QTest::newRow("add-item04") << serverContent1 << "new Item" + << "Col 4"; + QTest::newRow("add-item05") << serverContent1 << "new Item" + << "Col 5"; + QTest::newRow("add-item06") << serverContent1 << "new Item" + << "Col 6"; + QTest::newRow("add-item07") << serverContent1 << "new Item" + << "Col 7"; +} + +void EntityTreeModelTest::testItemAdded() +{ + QFETCH(QString, serverContent); + QFETCH(QString, addedItem); + QFETCH(QString, parentCollection); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto parentIndex = firstMatchedIndex(*model, parentCollection); + const auto targetRow = model->rowCount(parentIndex); + + auto const addedCommand = new FakeItemAddedCommand(addedItem, parentCollection, serverData); + + m_modelSpy->startSpying(); + + serverData->setCommands({addedCommand}); + + const QList expectedSignals{{RowsAboutToBeInserted, targetRow, targetRow, parentCollection, QVariantList{addedItem}}, + {RowsInserted, targetRow, targetRow, parentCollection, QVariantList{addedItem}}}; + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemRemoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("removedItem"); + + QTest::newRow("remove-item01") << serverContent1 << "Item 1"; + QTest::newRow("remove-item02") << serverContent1 << "Item 2"; + QTest::newRow("remove-item03") << serverContent1 << "Item 3"; + QTest::newRow("remove-item04") << serverContent1 << "Item 4"; + QTest::newRow("remove-item05") << serverContent1 << "Item 5"; + QTest::newRow("remove-item06") << serverContent1 << "Item 6"; + QTest::newRow("remove-item07") << serverContent1 << "Item 7"; + QTest::newRow("remove-item08") << serverContent1 << "Item 8"; + QTest::newRow("remove-item09") << serverContent1 << "Item 9"; + QTest::newRow("remove-item10") << serverContent1 << "Item 10"; + QTest::newRow("remove-item11") << serverContent1 << "Item 11"; + QTest::newRow("remove-item12") << serverContent1 << "Item 12"; + QTest::newRow("remove-item13") << serverContent1 << "Item 13"; + QTest::newRow("remove-item14") << serverContent1 << "Item 14"; + QTest::newRow("remove-item15") << serverContent1 << "Item 15"; +} + +void EntityTreeModelTest::testItemRemoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, removedItem); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto removedIndex = firstMatchedIndex(*model, removedItem); + const auto sourceCollection = removedIndex.parent().data().toString(); + const auto sourceRow = removedIndex.row(); + + auto const removeCommand = new FakeItemRemovedCommand(removedItem, sourceCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({removeCommand}); + + const QList expectedSignals{{RowsAboutToBeRemoved, sourceRow, sourceRow, sourceCollection, QVariantList{removedItem}}, + {RowsRemoved, sourceRow, sourceRow, sourceCollection, QVariantList{removedItem}}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testItemChanged_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("changedItem"); + + QTest::newRow("change-item01") << serverContent1 << "Item 1"; + QTest::newRow("change-item02") << serverContent1 << "Item 2"; + QTest::newRow("change-item03") << serverContent1 << "Item 3"; + QTest::newRow("change-item04") << serverContent1 << "Item 4"; + QTest::newRow("change-item05") << serverContent1 << "Item 5"; + QTest::newRow("change-item06") << serverContent1 << "Item 6"; + QTest::newRow("change-item07") << serverContent1 << "Item 7"; + QTest::newRow("change-item08") << serverContent1 << "Item 8"; + QTest::newRow("change-item09") << serverContent1 << "Item 9"; + QTest::newRow("change-item10") << serverContent1 << "Item 10"; + QTest::newRow("change-item11") << serverContent1 << "Item 11"; + QTest::newRow("change-item12") << serverContent1 << "Item 12"; + QTest::newRow("change-item13") << serverContent1 << "Item 13"; + QTest::newRow("change-item14") << serverContent1 << "Item 14"; + QTest::newRow("change-item15") << serverContent1 << "Item 15"; +} + +void EntityTreeModelTest::testItemChanged() +{ + QFETCH(QString, serverContent); + QFETCH(QString, changedItem); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto changedIndex = firstMatchedIndex(*model, changedItem); + const auto parentCollection = changedIndex.parent().data().toString(); + const auto sourceRow = changedIndex.row(); + + auto const changeCommand = new FakeItemChangedCommand(changedItem, parentCollection, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({changeCommand}); + + const QList expectedSignals{{DataChanged, sourceRow, sourceRow, QVariantList{changedItem}}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void EntityTreeModelTest::testRemoveCollectionOnChanged() +{ + const auto serverContent = QStringLiteral( + "- C (inode/directory, text/directory) 'Col 1' 2" + "- - C (text/directory) 'Col 2' 1" + "- - - I text/directory 'Item 1'"); + const auto collectionName = QStringLiteral("Col 2"); + const auto monitoredMimeType = QStringLiteral("text/directory"); + + const auto testDrivers = populateModel(serverContent, monitoredMimeType); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto changedIndex = firstMatchedIndex(*model, collectionName); + auto changedCol = changedIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); + changedCol.setContentMimeTypes({QStringLiteral("foobar")}); + const auto parentCollection = changedIndex.parent().data().toString(); + + auto const changeCommand = new FakeCollectionChangedCommand(changedCol, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({changeCommand}); + + const QList expectedSignals{ + {RowsAboutToBeRemoved, changedIndex.row(), changedIndex.row(), parentCollection, QVariantList{collectionName}}, + {RowsRemoved, changedIndex.row(), changedIndex.row(), parentCollection, QVariantList{collectionName}}, + }; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +#include "entitytreemodeltest.moc" + +QTEST_MAIN(EntityTreeModelTest) diff --git a/autotests/libs/etmpopulationtest.cpp b/autotests/libs/etmpopulationtest.cpp new file mode 100644 index 0000000..a321350 --- /dev/null +++ b/autotests/libs/etmpopulationtest.cpp @@ -0,0 +1,337 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "changerecorder_p.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "control.h" +#include "entitytreemodel.h" +#include "entitytreemodel_p.h" +#include "itemcreatejob.h" +#include "monitor_p.h" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class ModelSignalSpy : public QObject +{ + Q_OBJECT +public: + explicit ModelSignalSpy(QAbstractItemModel &model) + { + connect(&model, &QAbstractItemModel::rowsInserted, this, &ModelSignalSpy::onRowsInserted); + connect(&model, &QAbstractItemModel::rowsRemoved, this, &ModelSignalSpy::onRowsRemoved); + connect(&model, &QAbstractItemModel::rowsMoved, this, &ModelSignalSpy::onRowsMoved); + connect(&model, &QAbstractItemModel::dataChanged, this, &ModelSignalSpy::onDataChanged); + connect(&model, &QAbstractItemModel::layoutChanged, this, &ModelSignalSpy::onLayoutChanged); + connect(&model, &QAbstractItemModel::modelReset, this, &ModelSignalSpy::onModelReset); + } + + QStringList mSignals; + QModelIndex parent; + int start; + int end; + +public Q_SLOTS: + void onRowsInserted(const QModelIndex &p, int s, int e) + { + qDebug() << "rowsInserted( parent =" << p << ", start = " << s << ", end = " << e << ", data = " << p.data().toString() << ")"; + mSignals << QStringLiteral("rowsInserted"); + parent = p; + start = s; + end = e; + } + void onRowsRemoved(const QModelIndex &p, int s, int e) + { + qDebug() << "rowsRemoved( parent = " << p << ", start = " << s << ", end = " << e << ")"; + mSignals << QStringLiteral("rowsRemoved"); + parent = p; + start = s; + end = e; + } + void onRowsMoved(const QModelIndex & /*unused*/, int /*unused*/, int /*unused*/, const QModelIndex & /*unused*/, int /*unused*/) + { + mSignals << QStringLiteral("rowsMoved"); + } + void onDataChanged(const QModelIndex &tl, const QModelIndex &br) + { + qDebug() << "dataChanged( topLeft =" << tl << "(" << tl.data().toString() << "), bottomRight =" << br << "(" << br.data().toString() << ") )"; + mSignals << QStringLiteral("dataChanged"); + } + void onLayoutChanged() + { + mSignals << QStringLiteral("layoutChanged"); + } + void onModelReset() + { + mSignals << QStringLiteral("modelReset"); + } +}; + +class InspectableETM : public EntityTreeModel +{ +public: + explicit InspectableETM(ChangeRecorder *monitor, QObject *parent = nullptr) + : EntityTreeModel(monitor, parent) + { + } + EntityTreeModelPrivate *etmPrivate() + { + return d_ptr; + } +}; + +QModelIndex getIndex(const QString &string, EntityTreeModel *model) +{ + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, string, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); +} + +Akonadi::Collection createCollection(const QString &name, const Akonadi::Collection &parent, bool enabled = true, const QStringList &mimeTypes = QStringList()) +{ + Akonadi::Collection col; + col.setParentCollection(parent); + col.setName(name); + col.setEnabled(enabled); + col.setContentMimeTypes(mimeTypes); + + auto create = new CollectionCreateJob(col); + create->exec(); + if (create->error()) { + qWarning() << create->errorString(); + } + return create->collection(); +} + +/** + * This is a test for the initial population of the ETM. + */ +class EtmPopulationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testMonitoringCollectionsPreset(); + void testMonitoringCollections(); + void testFullPopulation(); + void testAddMonitoringCollections(); + void testRemoveMonitoringCollections(); + void testDisplayFilter(); + void testLoadingOfHiddenCollection(); + +private: + Collection res; + QString mainCollectionName; + Collection monitorCol; + Collection col1; + Collection col2; + Collection col3; + Collection col4; +}; + +void EtmPopulationTest::initTestCase() +{ + qRegisterMetaType("Akonadi::Collection::Id"); + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + + res = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + + mainCollectionName = QStringLiteral("main"); + monitorCol = createCollection(mainCollectionName, res); + QVERIFY(monitorCol.isValid()); + col1 = createCollection(QStringLiteral("col1"), monitorCol); + QVERIFY(col1.isValid()); + col2 = createCollection(QStringLiteral("col2"), monitorCol); + QVERIFY(col2.isValid()); + col3 = createCollection(QStringLiteral("col3"), monitorCol); + QVERIFY(col3.isValid()); + col4 = createCollection(QStringLiteral("col4"), col2); + QVERIFY(col4.isValid()); +} + +void EtmPopulationTest::testMonitoringCollectionsPreset() +{ + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(col1, true); + changeRecorder->setCollectionMonitored(col2, true); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testMonitoringCollections() +{ + auto changeRecorder = new ChangeRecorder(this); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + Akonadi::Collection::List monitored; + monitored << col1 << col2; + model->setCollectionsMonitored(monitored); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testFullPopulation() +{ + auto changeRecorder = new ChangeRecorder(this); + // changeRecorder->setCollectionMonitored(Akonadi::Collection::root()); + changeRecorder->setAllMonitored(true); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testAddMonitoringCollections() +{ + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(col1, true); + changeRecorder->setCollectionMonitored(col2, true); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + // The main collection may be loaded a little later since it is in the fetchAncestors path + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + + model->setCollectionMonitored(col3, true); + + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QTRY_VERIFY(getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col3"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testRemoveMonitoringCollections() +{ + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(col1, true); + changeRecorder->setCollectionMonitored(col2, true); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + // The main collection may be loaded a little later since it is in the fetchAncestors path + QTRY_VERIFY(getIndex(mainCollectionName, model).isValid()); + + model->setCollectionMonitored(col2, false); + + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col2"), model).isValid()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col4"), model).isValid()); + + QTRY_VERIFY(getIndex(QStringLiteral("col1"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(QStringLiteral("col2"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(mainCollectionName, model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); + QTRY_VERIFY(!getIndex(QStringLiteral("col4"), model).data(Akonadi::EntityTreeModel::IsPopulatedRole).toBool()); +} + +void EtmPopulationTest::testDisplayFilter() +{ + Collection col5 = createCollection(QStringLiteral("col5"), monitorCol, false); + QVERIFY(col5.isValid()); + + auto changeRecorder = new ChangeRecorder(this); + auto model = new InspectableETM(changeRecorder, this); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + model->setListFilter(Akonadi::CollectionFetchScope::Display); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(mainCollectionName, model).isValid()); + QVERIFY(getIndex(QStringLiteral("col1"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col2"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col3"), model).isValid()); + QVERIFY(getIndex(QStringLiteral("col4"), model).isValid()); + QVERIFY(!getIndex(QStringLiteral("col5"), model).isValid()); + + auto deleteJob = new Akonadi::CollectionDeleteJob(col5); + AKVERIFYEXEC(deleteJob); +} + +/* + * Col5 and it's ancestors should be included although the ancestors don't match the mimetype filter. + */ +void EtmPopulationTest::testLoadingOfHiddenCollection() +{ + Collection col5 = createCollection(QStringLiteral("col5"), monitorCol, false, QStringList() << QStringLiteral("application/test")); + QVERIFY(col5.isValid()); + + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setMimeTypeMonitored(QStringLiteral("application/test"), true); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(EntityTreeModel::ImmediatePopulation); + model->setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + QTRY_VERIFY(model->isCollectionTreeFetched()); + QVERIFY(getIndex(QStringLiteral("col5"), model).isValid()); + + auto deleteJob = new Akonadi::CollectionDeleteJob(col5); + AKVERIFYEXEC(deleteJob); +} + +#include "etmpopulationtest.moc" + +QTEST_AKONADIMAIN(EtmPopulationTest) diff --git a/autotests/libs/fakeakonadiservercommand.cpp b/autotests/libs/fakeakonadiservercommand.cpp new file mode 100644 index 0000000..d654256 --- /dev/null +++ b/autotests/libs/fakeakonadiservercommand.cpp @@ -0,0 +1,492 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fakeakonadiservercommand.h" + +#include +#include + +#include "akranges.h" +#include "entitydisplayattribute.h" +#include "fakeserverdata.h" +#include "tagattribute.h" + +using namespace Akonadi; +using namespace AkRanges; + +FakeAkonadiServerCommand::FakeAkonadiServerCommand(FakeAkonadiServerCommand::Type type, FakeServerData *serverData) + : m_type(type) + , m_serverData(serverData) + , m_model(serverData->model()) +{ + connectForwardingSignals(); +} + +bool FakeAkonadiServerCommand::isTagSignal(const QByteArray &signal) const +{ + return signal.startsWith("emit_tag") || signal.startsWith("emit_monitoredTag"); +} + +bool FakeAkonadiServerCommand::isItemSignal(const QByteArray &signal) const +{ + return signal.startsWith("emit_item") || signal.startsWith("emit_monitoredItem"); +} + +bool FakeAkonadiServerCommand::isCollectionSignal(const QByteArray &signal) const +{ + return signal.startsWith("emit_collection") || signal.startsWith("emit_monitoredCollection"); +} + +void FakeAkonadiServerCommand::connectForwardingSignals() +{ + const auto mo = FakeAkonadiServerCommand::metaObject(); + for (int methodIndex = 0; methodIndex < mo->methodCount(); ++methodIndex) { + const QMetaMethod mm = mo->method(methodIndex); + const QByteArray signature = mm.methodSignature(); + if (mm.methodType() == QMetaMethod::Signal) { + if ((qobject_cast(m_model) && isTagSignal(signature)) + || (qobject_cast(m_model) && (isCollectionSignal(signature) || isItemSignal(signature)))) { + const int modelSlotIndex = m_model->metaObject()->indexOfSlot(signature.mid(5).constData()); + if (modelSlotIndex < 0) { + qWarning() << "Slot not found in" << m_model->metaObject()->className() << ":" << signature.mid(5).constData(); + Q_ASSERT(modelSlotIndex >= 0); + } + mo->connect(this, methodIndex, m_model, modelSlotIndex); + } + } + } +} + +Collection FakeAkonadiServerCommand::getCollectionByDisplayName(const QString &displayName) const +{ + Q_ASSERT(qobject_cast(m_model)); + QModelIndexList list = m_model->match(m_model->index(0, 0), Qt::DisplayRole, displayName, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return Collection(); + } + return list.first().data(EntityTreeModel::CollectionRole).value(); +} + +Item FakeAkonadiServerCommand::getItemByDisplayName(const QString &displayName) const +{ + Q_ASSERT(qobject_cast(m_model)); + QModelIndexList list = m_model->match(m_model->index(0, 0), Qt::DisplayRole, displayName, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return Item(); + } + return list.first().data(EntityTreeModel::ItemRole).value(); +} + +Tag FakeAkonadiServerCommand::getTagByDisplayName(const QString &displayName) const +{ + Q_ASSERT(qobject_cast(m_model)); + QModelIndexList list = m_model->match(m_model->index(0, 0), Qt::DisplayRole, displayName, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return Tag(); + } + + return list.first().data(TagModel::TagRole).value(); +} + +void FakeJobResponse::doCommand() +{ + if (m_type == RespondToCollectionFetch) { + Q_EMIT emit_collectionsFetched(m_collections | Views::values | Actions::toQVector); + } else if (m_type == RespondToItemFetch) { + setProperty("FetchCollectionId", m_parentCollection.id()); + Q_EMIT emit_itemsFetched(m_items | Views::values | Actions::toQVector); + } else if (m_type == RespondToTagFetch) { + Q_EMIT emit_tagsFetched(m_tags | Views::values | Actions::toQVector); + } +} + +QList FakeJobResponse::tokenize(const QString &treeString) +{ + QStringList parts = treeString.split(QLatin1Char('-')); + + QList tokens; + const QStringList::const_iterator begin = parts.constBegin(); + const QStringList::const_iterator end = parts.constEnd(); + + QStringList::const_iterator it = begin; + ++it; + for (; it != end; ++it) { + Token token; + if (it->trimmed().isEmpty()) { + token.type = Token::Branch; + } else { + token.type = Token::Leaf; + token.content = it->trimmed(); + } + tokens.append(token); + } + return tokens; +} + +QList FakeJobResponse::interpret(FakeServerData *fakeServerData, const QString &serverData) +{ + QList list; + const QList response = parseTreeString(fakeServerData, serverData); + + for (FakeJobResponse *command : response) { + list.append(command); + } + return list; +} + +QList FakeJobResponse::parseTreeString(FakeServerData *fakeServerData, const QString &treeString) +{ + int depth = 0; + + QList collectionResponseList; + QHash itemResponseMap; + QList tagResponseList; + + Collection::List recentCollections; + Tag::List recentTags; + + recentCollections.append(Collection::root()); + recentTags.append(Tag()); + + QList tokens = tokenize(treeString); + while (!tokens.isEmpty()) { + Token token = tokens.takeFirst(); + + if (token.type == Token::Branch) { + ++depth; + continue; + } + Q_ASSERT(token.type == Token::Leaf); + parseEntityString(collectionResponseList, itemResponseMap, tagResponseList, recentCollections, recentTags, fakeServerData, token.content, depth); + + depth = 0; + } + return collectionResponseList + tagResponseList; +} + +void FakeJobResponse::parseEntityString(QList &collectionResponseList, + QHash &itemResponseMap, + QList &tagResponseList, + Collection::List &recentCollections, + Tag::List &recentTags, + FakeServerData *fakeServerData, + const QString &_entityString, + int depth) +{ + QString entityString = _entityString; + if (entityString.startsWith(QLatin1Char('C'))) { + Collection collection; + entityString.remove(0, 2); + Q_ASSERT(entityString.startsWith(QLatin1Char('('))); + entityString.remove(0, 1); + QStringList parts = entityString.split(QLatin1Char(')')); + + if (!parts.first().isEmpty()) { + QString typesString = parts.takeFirst(); + + QStringList types = typesString.split(QLatin1Char(',')); + types.replaceInStrings(QStringLiteral(" "), QLatin1String("")); + collection.setContentMimeTypes(types); + } else { + parts.removeFirst(); + } + + collection.setId(fakeServerData->nextCollectionId()); + collection.setName(QStringLiteral("Collection %1").arg(collection.id())); + collection.setRemoteId(QStringLiteral("remoteId %1").arg(collection.id())); + + if (depth == 0) { + collection.setParentCollection(Collection::root()); + } else { + collection.setParentCollection(recentCollections.at(depth)); + } + + if (recentCollections.size() == (depth + 1)) { + recentCollections.append(collection); + } else { + recentCollections[depth + 1] = collection; + } + + int order = 0; + if (!parts.first().isEmpty()) { + QString displayName; + QString optionalSection = parts.first().trimmed(); + if (optionalSection.startsWith(QLatin1Char('\''))) { + optionalSection.remove(0, 1); + QStringList optionalParts = optionalSection.split(QLatin1Char('\'')); + displayName = optionalParts.takeFirst(); + auto eda = new EntityDisplayAttribute(); + eda->setDisplayName(displayName); + collection.addAttribute(eda); + optionalSection = optionalParts.first(); + } + + QString orderString = optionalSection.trimmed(); + if (!orderString.isEmpty()) { + bool ok; + order = orderString.toInt(&ok); + Q_ASSERT(ok); + } + } else { + order = 1; + } + while (collectionResponseList.size() < order) { + collectionResponseList.append(new FakeJobResponse(recentCollections[depth], FakeJobResponse::RespondToCollectionFetch, fakeServerData)); + } + collectionResponseList[order - 1]->appendCollection(collection); + } + if (entityString.startsWith(QLatin1Char('I'))) { + Item item; + int order = 0; + entityString.remove(0, 2); + entityString = entityString.trimmed(); + QString type; + int iFirstSpace = entityString.indexOf(QLatin1Char(' ')); + type = entityString.left(iFirstSpace); + entityString = entityString.remove(0, iFirstSpace + 1).trimmed(); + if (iFirstSpace > 0 && !entityString.isEmpty()) { + QString displayName; + QString optionalSection = entityString; + if (optionalSection.startsWith(QLatin1Char('\''))) { + optionalSection.remove(0, 1); + QStringList optionalParts = optionalSection.split(QLatin1Char('\'')); + displayName = optionalParts.takeFirst(); + auto eda = new EntityDisplayAttribute(); + eda->setDisplayName(displayName); + item.addAttribute(eda); + optionalSection = optionalParts.first(); + } + QString orderString = optionalSection.trimmed(); + if (!orderString.isEmpty()) { + bool ok; + order = orderString.toInt(&ok); + Q_ASSERT(ok); + } + } else { + type = entityString; + } + Q_UNUSED(order) + + item.setMimeType(type); + item.setId(fakeServerData->nextItemId()); + item.setRemoteId(QStringLiteral("RId_%1 %2").arg(item.id()).arg(type)); + item.setParentCollection(recentCollections.at(depth)); + + Collection::Id colId = recentCollections[depth].id(); + if (!itemResponseMap.contains(colId)) { + auto newResponse = new FakeJobResponse(recentCollections[depth], FakeJobResponse::RespondToItemFetch, fakeServerData); + itemResponseMap.insert(colId, newResponse); + collectionResponseList.append(newResponse); + } + itemResponseMap[colId]->appendItem(item); + } + if (entityString.startsWith(QLatin1Char('T'))) { + Tag tag; + int order = 0; + entityString.remove(0, 2); + entityString = entityString.trimmed(); + int iFirstSpace = entityString.indexOf(QLatin1Char(' ')); + QString type = entityString.left(iFirstSpace); + entityString = entityString.remove(0, iFirstSpace + 1).trimmed(); + tag.setType(type.toLatin1()); + + if (iFirstSpace > 0 && !entityString.isEmpty()) { + QString displayName; + QString optionalSection = entityString; + if (optionalSection.startsWith(QLatin1Char('\''))) { + optionalSection.remove(0, 1); + QStringList optionalParts = optionalSection.split(QLatin1Char('\'')); + displayName = optionalParts.takeFirst(); + auto ta = new TagAttribute(); + ta->setDisplayName(displayName); + tag.addAttribute(ta); + optionalSection = optionalParts.first(); + } + QString orderString = optionalSection.trimmed(); + if (!orderString.isEmpty()) { + bool ok; + order = orderString.toInt(&ok); + Q_ASSERT(ok); + } + } else { + type = entityString; + } + + tag.setId(fakeServerData->nextTagId()); + tag.setRemoteId("RID_" + QByteArray::number(tag.id()) + ' ' + type.toLatin1()); + tag.setType(type.toLatin1()); + + if (depth == 0) { + tag.setParent(Tag()); + } else { + tag.setParent(recentTags.at(depth)); + } + + if (recentTags.size() == (depth + 1)) { + recentTags.append(tag); + } else { + recentTags[depth + 1] = tag; + } + + while (tagResponseList.size() < order) { + tagResponseList.append(new FakeJobResponse(recentTags[depth], FakeJobResponse::RespondToTagFetch, fakeServerData)); + } + tagResponseList[order - 1]->appendTag(tag); + } +} + +void FakeCollectionMovedCommand::doCommand() +{ + Collection collection = getCollectionByDisplayName(m_collectionName); + Collection source = getCollectionByDisplayName(m_sourceName); + Collection target = getCollectionByDisplayName(m_targetName); + + Q_ASSERT(collection.isValid()); + Q_ASSERT(source.isValid()); + Q_ASSERT(target.isValid()); + + collection.setParentCollection(target); + + Q_EMIT emit_monitoredCollectionMoved(collection, source, target); +} + +void FakeCollectionAddedCommand::doCommand() +{ + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(parent.isValid()); + + Collection collection; + collection.setId(m_serverData->nextCollectionId()); + collection.setName(QStringLiteral("Collection %1").arg(collection.id())); + collection.setRemoteId(QStringLiteral("remoteId %1").arg(collection.id())); + collection.setParentCollection(parent); + + auto eda = new EntityDisplayAttribute(); + eda->setDisplayName(m_collectionName); + collection.addAttribute(eda); + + Q_EMIT emit_monitoredCollectionAdded(collection, parent); +} + +void FakeCollectionRemovedCommand::doCommand() +{ + Collection collection = getCollectionByDisplayName(m_collectionName); + + Q_ASSERT(collection.isValid()); + + Q_EMIT emit_monitoredCollectionRemoved(collection); +} + +void FakeCollectionChangedCommand::doCommand() +{ + if (m_collection.isValid()) { + Q_EMIT emit_monitoredCollectionChanged(m_collection); + return; + } + Collection collection = getCollectionByDisplayName(m_collectionName); + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(collection.isValid()); + + Q_EMIT emit_monitoredCollectionChanged(collection); +} + +void FakeItemMovedCommand::doCommand() +{ + Item item = getItemByDisplayName(m_itemName); + Collection source = getCollectionByDisplayName(m_sourceName); + Collection target = getCollectionByDisplayName(m_targetName); + + Q_ASSERT(item.isValid()); + Q_ASSERT(source.isValid()); + Q_ASSERT(target.isValid()); + + item.setParentCollection(target); + + Q_EMIT emit_monitoredItemMoved(item, source, target); +} + +void FakeItemAddedCommand::doCommand() +{ + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(parent.isValid()); + + Item item; + item.setId(m_serverData->nextItemId()); + item.setRemoteId(QStringLiteral("remoteId %1").arg(item.id())); + item.setParentCollection(parent); + + auto eda = new EntityDisplayAttribute(); + eda->setDisplayName(m_itemName); + item.addAttribute(eda); + + Q_EMIT emit_monitoredItemAdded(item, parent); +} + +void FakeItemRemovedCommand::doCommand() +{ + Item item = getItemByDisplayName(m_itemName); + + Q_ASSERT(item.isValid()); + + Q_EMIT emit_monitoredItemRemoved(item); +} + +void FakeItemChangedCommand::doCommand() +{ + Item item = getItemByDisplayName(m_itemName); + Collection parent = getCollectionByDisplayName(m_parentName); + + Q_ASSERT(item.isValid()); + Q_ASSERT(parent.isValid()); + + Q_EMIT emit_monitoredItemChanged(item, QSet()); +} + +void FakeTagAddedCommand::doCommand() +{ + const Tag parent = getTagByDisplayName(m_parentName); + + Tag tag; + tag.setId(m_serverData->nextTagId()); + tag.setName(m_tagName); + tag.setRemoteId("remoteId " + QByteArray::number(tag.id())); + tag.setParent(parent); + + Q_EMIT emit_monitoredTagAdded(tag); +} + +void FakeTagChangedCommand::doCommand() +{ + const Tag tag = getTagByDisplayName(m_tagName); + + Q_ASSERT(tag.isValid()); + + Q_EMIT emit_monitoredTagChanged(tag); +} + +void FakeTagMovedCommand::doCommand() +{ + Tag tag = getTagByDisplayName(m_tagName); + Tag newParent = getTagByDisplayName(m_newParent); + + Q_ASSERT(tag.isValid()); + + tag.setParent(newParent); + + Q_EMIT emit_monitoredTagChanged(tag); +} + +void FakeTagRemovedCommand::doCommand() +{ + const Tag tag = getTagByDisplayName(m_tagName); + + Q_ASSERT(tag.isValid()); + + Q_EMIT emit_monitoredTagRemoved(tag); +} diff --git a/autotests/libs/fakeakonadiservercommand.h b/autotests/libs/fakeakonadiservercommand.h new file mode 100644 index 0000000..0272665 --- /dev/null +++ b/autotests/libs/fakeakonadiservercommand.h @@ -0,0 +1,389 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonaditestfake_export.h" +#include "collection.h" +#include "entitytreemodel.h" +#include "item.h" +#include "tag.h" +#include "tagmodel.h" + +class FakeServerData; + +class AKONADITESTFAKE_EXPORT FakeAkonadiServerCommand : public QObject +{ + Q_OBJECT +public: + enum Type { + Notification, + RespondToCollectionFetch, + RespondToItemFetch, + RespondToTagFetch, + }; + + FakeAkonadiServerCommand(Type type, FakeServerData *serverData); + + ~FakeAkonadiServerCommand() override = default; + + Type respondTo() const + { + return m_type; + } + Akonadi::Collection fetchCollection() const + { + return m_parentCollection; + } + + Type m_type; + + virtual void doCommand() = 0; + +Q_SIGNALS: + void emit_itemsFetched(const Akonadi::Item::List &list); + void emit_collectionsFetched(const Akonadi::Collection::List &list); + void emit_tagsFetched(const Akonadi::Tag::List &tags); + + void emit_monitoredCollectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &target); + void emit_monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + void emit_monitoredCollectionRemoved(const Akonadi::Collection &collection); + void emit_monitoredCollectionChanged(const Akonadi::Collection &collection); + + void emit_monitoredItemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &target); + void emit_monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &parent); + void emit_monitoredItemRemoved(const Akonadi::Item &item); + void emit_monitoredItemChanged(const Akonadi::Item &item, const QSet &parts); + + void emit_monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + void emit_monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + void emit_monitoredTagAdded(const Akonadi::Tag &tag); + void emit_monitoredTagChanged(const Akonadi::Tag &tag); + void emit_monitoredTagRemoved(const Akonadi::Tag &tag); + +protected: + Akonadi::Collection getCollectionByDisplayName(const QString &displayName) const; + Akonadi::Item getItemByDisplayName(const QString &displayName) const; + Akonadi::Tag getTagByDisplayName(const QString &displayName) const; + + bool isItemSignal(const QByteArray &signature) const; + bool isCollectionSignal(const QByteArray &signature) const; + bool isTagSignal(const QByteArray &signature) const; + +protected: + FakeServerData *m_serverData = nullptr; + QAbstractItemModel *m_model = nullptr; + Akonadi::Collection m_parentCollection; + Akonadi::Tag m_parentTag; + QHash m_collections; + QHash m_items; + QHash> m_childElements; + QHash m_tags; + +private: + void connectForwardingSignals(); +}; + +class AKONADITESTFAKE_EXPORT FakeMonitorCommand : public FakeAkonadiServerCommand +{ + Q_OBJECT +public: + explicit FakeMonitorCommand(FakeServerData *serverData) + : FakeAkonadiServerCommand(Notification, serverData) + { + } +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionMovedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeCollectionMovedCommand(const QString &collection, const QString &source, const QString &target, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_sourceName(source) + , m_targetName(target) + { + } + + void doCommand() override; + +private: + QString m_collectionName; + QString m_sourceName; + QString m_targetName; +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionAddedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeCollectionAddedCommand(const QString &collection, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_collectionName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionRemovedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeCollectionRemovedCommand(const QString &collection, const QString &source, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_parentName(source) + { + } + + void doCommand() override; + +private: + QString m_collectionName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeCollectionChangedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeCollectionChangedCommand(const QString &collection, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collectionName(collection) + , m_parentName(parent) + { + } + + FakeCollectionChangedCommand(const Akonadi::Collection &collection, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_collection(collection) + { + } + + void doCommand() override; + +private: + Akonadi::Collection m_collection; + QString m_collectionName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeItemMovedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeItemMovedCommand(const QString &item, const QString &source, const QString &target, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_sourceName(source) + , m_targetName(target) + { + } + + void doCommand() override; + +private: + QString m_itemName; + QString m_sourceName; + QString m_targetName; +}; + +class AKONADITESTFAKE_EXPORT FakeItemAddedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeItemAddedCommand(const QString &item, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_itemName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeItemRemovedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeItemRemovedCommand(const QString &item, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_itemName; + QString m_parentName; + FakeServerData *m_serverData = nullptr; +}; + +class AKONADITESTFAKE_EXPORT FakeItemChangedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeItemChangedCommand(const QString &item, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_itemName(item) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_itemName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeTagAddedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeTagAddedCommand(const QString &tag, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_tagName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeTagChangedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeTagChangedCommand(const QString &tag, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_tagName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeTagMovedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeTagMovedCommand(const QString &tag, const QString &oldParent, const QString &newParent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_oldParent(oldParent) + , m_newParent(newParent) + { + } + + void doCommand() override; + +private: + QString m_tagName; + QString m_oldParent; + QString m_newParent; +}; + +class AKONADITESTFAKE_EXPORT FakeTagRemovedCommand : public FakeMonitorCommand +{ + Q_OBJECT +public: + FakeTagRemovedCommand(const QString &tag, const QString &parent, FakeServerData *serverData) + : FakeMonitorCommand(serverData) + , m_tagName(tag) + , m_parentName(parent) + { + } + + void doCommand() override; + +private: + QString m_tagName; + QString m_parentName; +}; + +class AKONADITESTFAKE_EXPORT FakeJobResponse : public FakeAkonadiServerCommand +{ + Q_OBJECT + struct Token { + enum Type { + Branch, + Leaf, + }; + Type type; + QString content; + }; + +public: + FakeJobResponse(const Akonadi::Collection &parentCollection, Type respondTo, FakeServerData *serverData) + : FakeAkonadiServerCommand(respondTo, serverData) + { + m_parentCollection = parentCollection; + } + + FakeJobResponse(const Akonadi::Tag &parentTag, Type respondTo, FakeServerData *serverData) + : FakeAkonadiServerCommand(respondTo, serverData) + { + m_parentTag = parentTag; + } + + void appendCollection(const Akonadi::Collection &collection) + { + m_collections.insert(collection.id(), collection); + m_childElements[collection.parentCollection().id()].append(collection.id()); + } + void appendItem(const Akonadi::Item &item) + { + m_items.insert(item.id(), item); + } + + void appendTag(const Akonadi::Tag &tag) + { + m_tags.insert(tag.id(), tag); + } + + void doCommand() override; + + static QList interpret(FakeServerData *fakeServerData, const QString &input); + +private: + static QList parseTreeString(FakeServerData *fakeServerData, const QString &treeString); + static QList tokenize(const QString &treeString); + static void parseEntityString(QList &collectionResponseList, + QHash &itemResponseMap, + QList &tagResponseList, + Akonadi::Collection::List &recentCollections, + Akonadi::Tag::List &recentTags, + FakeServerData *fakeServerData, + const QString &entityString, + int depth); +}; + diff --git a/autotests/libs/fakeentitycache.cpp b/autotests/libs/fakeentitycache.cpp new file mode 100644 index 0000000..8995864 --- /dev/null +++ b/autotests/libs/fakeentitycache.cpp @@ -0,0 +1,7 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakeentitycache.h" diff --git a/autotests/libs/fakeentitycache.h b/autotests/libs/fakeentitycache.h new file mode 100644 index 0000000..f665484 --- /dev/null +++ b/autotests/libs/fakeentitycache.h @@ -0,0 +1,174 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditestfake_export.h" +#include "collectionfetchscope.h" +#include "itemfetchscope.h" +#include "monitor_p.h" +#include "notificationsource_p.h" +#include "private/protocol_p.h" + +template class FakeEntityCache : public Cache +{ +public: + FakeEntityCache(Akonadi::Session *session = nullptr, QObject *parent = nullptr) + : Cache(0, session, parent) + { + } + + void setData(const QHash &data) + { + m_data = data; + } + + void insert(T t) + { + m_data.insert(t.id(), t); + } + + void emitDataAvailable() + { + Q_EMIT Cache::dataAvailable(); + } + + T retrieve(typename T::Id id) const override + { + return m_data.value(id); + } + + void request(typename T::Id id, const typename Cache::FetchScope &scope) override + { + Q_UNUSED(id) + Q_UNUSED(scope) + } + + bool ensureCached(typename T::Id id, const typename Cache::FetchScope &scope) override + { + Q_UNUSED(scope) + return m_data.contains(id); + } + +private: + QHash m_data; +}; +using FakeCollectionCache = FakeEntityCache; +using FakeItemCache = FakeEntityCache; + +class AKONADITESTFAKE_EXPORT FakeNotificationSource : public QObject +{ + Q_OBJECT +public: + explicit FakeNotificationSource(QObject *parent = nullptr) + : QObject(parent) + { + } + +public Q_SLOTS: + void setAllMonitored(bool allMonitored) + { + Q_UNUSED(allMonitored) + } + void setMonitoredCollection(qlonglong id, bool monitored) + { + Q_UNUSED(id) + Q_UNUSED(monitored) + } + void setMonitoredItem(qlonglong id, bool monitored) + { + Q_UNUSED(id) + Q_UNUSED(monitored) + } + void setMonitoredResource(const QByteArray &resource, bool monitored) + { + Q_UNUSED(resource) + Q_UNUSED(monitored) + } + void setMonitoredMimeType(const QString &mimeType, bool monitored) + { + Q_UNUSED(mimeType) + Q_UNUSED(monitored) + } + void setIgnoredSession(const QByteArray &session, bool ignored) + { + Q_UNUSED(session) + Q_UNUSED(ignored) + } + + void setSession(const QByteArray &session) + { + Q_UNUSED(session) + } +}; + +class AKONADITESTFAKE_EXPORT FakeNotificationConnection : public Akonadi::Connection +{ + Q_OBJECT + +public: + explicit FakeNotificationConnection(Akonadi::CommandBuffer *buffer) + : Connection(Connection::NotificationConnection, "", buffer) + , mBuffer(buffer) + { + } + + virtual ~FakeNotificationConnection() + { + } + + void emitNotify(const Akonadi::Protocol::ChangeNotificationPtr &ntf) + { + Akonadi::CommandBufferLocker locker(mBuffer); + mBuffer->enqueue(3, ntf); + } + +private: + Akonadi::CommandBuffer *mBuffer; +}; + +class FakeMonitorDependenciesFactory : public Akonadi::ChangeNotificationDependenciesFactory +{ +public: + FakeMonitorDependenciesFactory(FakeItemCache *itemCache_, FakeCollectionCache *collectionCache_) + : Akonadi::ChangeNotificationDependenciesFactory() + , itemCache(itemCache_) + , collectionCache(collectionCache_) + { + } + + Akonadi::Connection *createNotificationConnection(Akonadi::Session *parent, Akonadi::CommandBuffer *buffer) override + { + auto conn = new FakeNotificationConnection(buffer); + addConnection(parent, conn); + return conn; + } + + void destroyNotificationConnection(Akonadi::Session *parent, Akonadi::Connection *connection) override + { + Q_UNUSED(parent) + delete connection; + } + + Akonadi::CollectionCache *createCollectionCache(int maxCapacity, Akonadi::Session *session) override + { + Q_UNUSED(maxCapacity) + Q_UNUSED(session) + return collectionCache; + } + + Akonadi::ItemCache *createItemCache(int maxCapacity, Akonadi::Session *session) override + { + Q_UNUSED(maxCapacity) + Q_UNUSED(session) + return itemCache; + } + +private: + FakeItemCache *itemCache = nullptr; + FakeCollectionCache *collectionCache = nullptr; +}; + diff --git a/autotests/libs/fakemonitor.cpp b/autotests/libs/fakemonitor.cpp new file mode 100644 index 0000000..b30aea2 --- /dev/null +++ b/autotests/libs/fakemonitor.cpp @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakemonitor.h" +#include "changerecorder_p.h" + +#include "entitycache_p.h" + +#include + +using namespace Akonadi; + +class FakeMonitorPrivate : public ChangeRecorderPrivate +{ + Q_DECLARE_PUBLIC(FakeMonitor) +public: + explicit FakeMonitorPrivate(FakeMonitor *monitor) + : ChangeRecorderPrivate(nullptr, monitor) + { + } + + bool connectToNotificationManager() override + { + // Do nothing. This monitor should not connect to the notification manager. + return true; + } +}; + +FakeMonitor::FakeMonitor(QObject *parent) + : ChangeRecorder(new FakeMonitorPrivate(this), parent) +{ +} diff --git a/autotests/libs/fakemonitor.h b/autotests/libs/fakemonitor.h new file mode 100644 index 0000000..39b8208 --- /dev/null +++ b/autotests/libs/fakemonitor.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditestfake_export.h" +#include "changerecorder.h" + +using namespace Akonadi; + +class FakeMonitorPrivate; + +class AKONADITESTFAKE_EXPORT FakeMonitor : public Akonadi::ChangeRecorder +{ + Q_OBJECT +public: + explicit FakeMonitor(QObject *parent = nullptr); + +private: + Q_DECLARE_PRIVATE(FakeMonitor) +}; + diff --git a/autotests/libs/fakeserverdata.cpp b/autotests/libs/fakeserverdata.cpp new file mode 100644 index 0000000..9cbb7a1 --- /dev/null +++ b/autotests/libs/fakeserverdata.cpp @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fakeserverdata.h" + +#include "collectionfetchjob.h" +#include "itemfetchjob.h" + +#include + +FakeServerData::FakeServerData(EntityTreeModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent) + : QObject(parent) + , m_model(model) + , m_session(session) + , m_monitor(monitor) + , m_nextCollectionId(1) + , m_nextItemId(0) + , m_nextTagId(1) +{ + // can't use QueuedConnection here, because the Job might self-deleted before + // the slot gets called + connect(session, &FakeSession::jobAdded, [this](Akonadi::Job *job) { + Collection::Id fetchColId = job->property("FetchCollectionId").toULongLong(); + QTimer::singleShot(0, [this, fetchColId]() { + jobAdded(fetchColId); + }); + }); +} + +FakeServerData::FakeServerData(TagModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent) + : QObject(parent) + , m_model(model) + , m_session(session) + , m_monitor(monitor) + , m_nextCollectionId(1) + , m_nextItemId(0) + , m_nextTagId(1) +{ + connect(session, &FakeSession::jobAdded, [this](Akonadi::Job * /*unused*/) { + QTimer::singleShot(0, [this]() { + jobAdded(); + }); + }); +} + +void FakeServerData::setCommands(const QList &list) +{ + m_communicationQueue.clear(); + for (FakeAkonadiServerCommand *command : list) { + m_communicationQueue << command; + } +} + +void FakeServerData::processNotifications() +{ + while (!m_communicationQueue.isEmpty()) { + FakeAkonadiServerCommand::Type respondTo = m_communicationQueue.head()->respondTo(); + if (respondTo == FakeAkonadiServerCommand::Notification) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + delete command; + } else { + return; + } + } +} + +void FakeServerData::jobAdded(qint64 fetchColId) +{ + returnEntities(fetchColId); +} + +void FakeServerData::jobAdded() +{ + while (!m_communicationQueue.isEmpty() && m_communicationQueue.head()->respondTo() == FakeAkonadiServerCommand::RespondToTagFetch) { + returnTags(); + } + + processNotifications(); +} + +void FakeServerData::returnEntities(Collection::Id fetchColId) +{ + if (!returnCollections(fetchColId)) { + while (!m_communicationQueue.isEmpty() && m_communicationQueue.head()->respondTo() == FakeAkonadiServerCommand::RespondToItemFetch) { + returnItems(fetchColId); + } + } + + processNotifications(); +} + +bool FakeServerData::returnCollections(Collection::Id fetchColId) +{ + if (m_communicationQueue.isEmpty()) { + return true; + } + FakeAkonadiServerCommand::Type commType = m_communicationQueue.head()->respondTo(); + + Collection fetchCollection = m_communicationQueue.head()->fetchCollection(); + + if (commType == FakeAkonadiServerCommand::RespondToCollectionFetch && fetchColId == fetchCollection.id()) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + if (!m_communicationQueue.isEmpty()) { + returnEntities(fetchColId); + } + delete command; + return true; + } + return false; +} + +void FakeServerData::returnItems(Item::Id fetchColId) +{ + FakeAkonadiServerCommand::Type commType = m_communicationQueue.head()->respondTo(); + + if (commType == FakeAkonadiServerCommand::RespondToItemFetch) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + if (!m_communicationQueue.isEmpty()) { + returnEntities(fetchColId); + } + delete command; + } +} + +void FakeServerData::returnTags() +{ + FakeAkonadiServerCommand::Type commType = m_communicationQueue.head()->respondTo(); + + if (commType == FakeAkonadiServerCommand::RespondToTagFetch) { + FakeAkonadiServerCommand *command = m_communicationQueue.dequeue(); + command->doCommand(); + delete command; + } +} diff --git a/autotests/libs/fakeserverdata.h b/autotests/libs/fakeserverdata.h new file mode 100644 index 0000000..18d97ed --- /dev/null +++ b/autotests/libs/fakeserverdata.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "entitytreemodel.h" +#include "job.h" + +#include "akonaditestfake_export.h" +#include "fakeakonadiservercommand.h" +#include "fakemonitor.h" +#include "fakesession.h" + +using namespace Akonadi; + +class AKONADITESTFAKE_EXPORT FakeServerData : public QObject +{ + Q_OBJECT +public: + FakeServerData(EntityTreeModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent = nullptr); + FakeServerData(TagModel *model, FakeSession *session, FakeMonitor *monitor, QObject *parent = nullptr); + + void setCommands(const QList &list); + + Collection::Id nextCollectionId() const + { + return m_nextCollectionId++; + } + Item::Id nextItemId() const + { + return m_nextItemId++; + } + Tag::Id nextTagId() const + { + return m_nextTagId++; + } + + QAbstractItemModel *model() const + { + return m_model; + } + + void processNotifications(); + +private Q_SLOTS: + void jobAdded(qint64 fetchCollectionId); + void jobAdded(); + +private: + bool returnCollections(Collection::Id fetchColId); + void returnItems(Item::Id fetchColId); + void returnEntities(Collection::Id fetchColId); + void returnTags(); + +private: + QAbstractItemModel *m_model = nullptr; + FakeSession *m_session = nullptr; + FakeMonitor *m_monitor = nullptr; + + QList m_commandList; + QQueue m_communicationQueue; + + mutable Collection::Id m_nextCollectionId; + mutable Item::Id m_nextItemId; + mutable Tag::Id m_nextTagId; +}; + diff --git a/autotests/libs/fakesession.cpp b/autotests/libs/fakesession.cpp new file mode 100644 index 0000000..f3da8da --- /dev/null +++ b/autotests/libs/fakesession.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakesession.h" +#include "job.h" +#include "private/protocol_p.h" +#include "session_p.h" + +#include +#include + +class FakeSessionPrivate : public SessionPrivate +{ +public: + FakeSessionPrivate(FakeSession *parent, FakeSession::Mode mode) + : SessionPrivate(parent) + , q_ptr(parent) + , m_mode(mode) + { + protocolVersion = Protocol::version(); + } + + /* reimp */ + void init(const QByteArray &id) override + { + // trimmed down version of the real SessionPrivate::init(), without any server access + if (!id.isEmpty()) { + sessionId = id; + } else { + sessionId = QCoreApplication::instance()->applicationName().toUtf8() + '-' + QByteArray::number(qrand()); + } + + connected = false; + theNextTag = 1; + jobRunning = false; + + reconnect(); + } + + /* reimp */ + void reconnect() override + { + if (m_mode == FakeSession::EndJobsImmediately) { + return; + } + + // Like Session does: delay the actual disconnect+reconnect + QTimer::singleShot(10, q_ptr, [&]() { + socketDisconnected(); + Q_EMIT q_ptr->reconnected(); + connected = true; + startNext(); + }); + } + + /* reimp */ + void addJob(Job *job) override + { + Q_EMIT q_ptr->jobAdded(job); + // Return immediately so that no actual communication happens with the server and + // the started jobs are completed. + if (m_mode == FakeSession::EndJobsImmediately) { + endJob(job); + } else { + SessionPrivate::addJob(job); + } + } + + FakeSession *q_ptr; + FakeSession::Mode m_mode; +}; + +FakeSession::FakeSession(const QByteArray &sessionId, FakeSession::Mode mode, QObject *parent) + : Session(new FakeSessionPrivate(this, mode), sessionId, parent) +{ +} + +void FakeSession::setAsDefaultSession() +{ + d->setDefaultSession(this); +} diff --git a/autotests/libs/fakesession.h b/autotests/libs/fakesession.h new file mode 100644 index 0000000..2ea27e4 --- /dev/null +++ b/autotests/libs/fakesession.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditestfake_export.h" +#include "collection.h" +#include "session.h" + +using namespace Akonadi; + +class AKONADITESTFAKE_EXPORT FakeSession : public Session +{ + Q_OBJECT +public: + enum Mode { + EndJobsImmediately, + EndJobsManually, + }; + + explicit FakeSession(const QByteArray &sessionId = QByteArray(), Mode mode = EndJobsImmediately, QObject *parent = nullptr); + + /** Make this the default session returned by Akonadi::Session::defaultSession(). + * Note that ownership is taken over by the thread-local storage. + */ + void setAsDefaultSession(); + +Q_SIGNALS: + void jobAdded(Akonadi::Job *job); + + friend class FakeSessionPrivate; +}; + diff --git a/autotests/libs/favoriteproxytest.cpp b/autotests/libs/favoriteproxytest.cpp new file mode 100644 index 0000000..cf02924 --- /dev/null +++ b/autotests/libs/favoriteproxytest.cpp @@ -0,0 +1,236 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "changerecorder_p.h" +#include "collectioncreatejob.h" +#include "control.h" +#include "entitytreemodel.h" +#include "entitytreemodel_p.h" +#include "favoritecollectionsmodel.h" +#include "itemcreatejob.h" +#include "monitor_p.h" +#include "qtest_akonadi.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +class InspectableETM : public EntityTreeModel +{ +public: + explicit InspectableETM(ChangeRecorder *monitor, QObject *parent = nullptr) + : EntityTreeModel(monitor, parent) + { + } + EntityTreeModelPrivate *etmPrivate() + { + return d_ptr; + } + void reset() + { + beginResetModel(); + endResetModel(); + } +}; + +class FavoriteProxyTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testItemAdded(); + void testLoadConfig(); + void testInsertAfterModelCreation(); + +private: + InspectableETM *createETM(); +}; + +void FavoriteProxyTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Akonadi::Control::start(); + AkonadiTest::setAllResourcesOffline(); +} + +QModelIndex getIndex(const QString &string, EntityTreeModel *model) +{ + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, string, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); +} + +/** + * Since we have no sensible way to figure out if the model is fully populated, + * we use the brute force approach. + */ +bool waitForPopulation(const QModelIndex &idx, EntityTreeModel *model, int count) +{ + for (int i = 0; i < 500; i++) { + if (model->rowCount(idx) >= count) { + return true; + } + QTest::qWait(10); + } + return false; +} + +InspectableETM *FavoriteProxyTest::createETM() +{ + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + return model; +} + +/** + * Tests that the item is being referenced when added to the favorite proxy, and dereferenced when removed. + */ +void FavoriteProxyTest::testItemAdded() +{ + Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + + InspectableETM *model = createETM(); + + KConfigGroup configGroup(KSharedConfig::openConfig(), "favoritecollectionsmodeltest"); + + auto favoriteModel = new FavoriteCollectionsModel(model, configGroup, this); + + const int numberOfRootCollections = 4; + // Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(res3Index.isValid()); + + const auto favoriteCollection = res3Index.data(EntityTreeModel::CollectionRole).value(); + QVERIFY(favoriteCollection.isValid()); + + QVERIFY(!model->etmPrivate()->isMonitored(favoriteCollection.id())); + + // Ensure the collection is reference counted after being added to the favorite model + { + favoriteModel->addCollection(favoriteCollection); + // the collection is in the favorites model + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), + favoriteCollection.id()); + // the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + // the collection is not yet buffered though + QTRY_VERIFY(!model->etmPrivate()->isBuffered(favoriteCollection.id())); + } + + // Survive a reset + { + QSignalSpy resetSpy(model, &QAbstractItemModel::modelReset); + model->reset(); + QTRY_COMPARE(resetSpy.count(), 1); + // the collection is in the favorites model + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), + favoriteCollection.id()); + // the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + // the collection is not yet buffered though + QTRY_VERIFY(!model->etmPrivate()->isBuffered(favoriteCollection.id())); + } + + // Ensure the collection is no longer reference counted after being added to the favorite model, and moved to the buffer + { + favoriteModel->removeCollection(favoriteCollection); + // moved from being reference counted to being buffered + QTRY_VERIFY(model->etmPrivate()->isBuffered(favoriteCollection.id())); + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 0); + } +} + +void FavoriteProxyTest::testLoadConfig() +{ + InspectableETM *model = createETM(); + + const int numberOfRootCollections = 4; + // Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(res3Index.isValid()); + const auto favoriteCollection = res3Index.data(EntityTreeModel::CollectionRole).value(); + QVERIFY(favoriteCollection.isValid()); + + KConfigGroup configGroup(KSharedConfig::openConfig(), "favoritecollectionsmodeltest"); + configGroup.writeEntry("FavoriteCollectionIds", QList() << favoriteCollection.id()); + configGroup.writeEntry("FavoriteCollectionLabels", QStringList() << QStringLiteral("label1")); + + auto favoriteModel = new FavoriteCollectionsModel(model, configGroup, this); + + { + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), + favoriteCollection.id()); + // the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + } +} + +class Filter : public QSortFilterProxyModel +{ +public: + bool filterAcceptsRow(int /*source_row*/, const QModelIndex & /*source_parent*/) const override + { + return accepts; + } + bool accepts; +}; + +void FavoriteProxyTest::testInsertAfterModelCreation() +{ + InspectableETM *model = createETM(); + Filter filter; + filter.accepts = false; + filter.setSourceModel(model); + + const int numberOfRootCollections = 4; + // Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(res3Index.isValid()); + const auto favoriteCollection = res3Index.data(EntityTreeModel::CollectionRole).value(); + QVERIFY(favoriteCollection.isValid()); + + KConfigGroup configGroup(KSharedConfig::openConfig(), "favoritecollectionsmodeltest2"); + + auto favoriteModel = new FavoriteCollectionsModel(&filter, configGroup, this); + + // Make sure the filter is not letting anything through + QTest::qWait(0); + QCOMPARE(filter.rowCount(QModelIndex()), 0); + + // The collection is not in the model yet + favoriteModel->addCollection(favoriteCollection); + filter.accepts = true; + filter.invalidate(); + + { + QTRY_COMPARE(favoriteModel->rowCount(QModelIndex()), 1); + QTRY_COMPARE(favoriteModel->data(favoriteModel->index(0, 0, QModelIndex()), EntityTreeModel::CollectionIdRole).value(), + favoriteCollection.id()); + // the collection got referenced + QTRY_VERIFY(model->etmPrivate()->isMonitored(favoriteCollection.id())); + } +} + +#include "favoriteproxytest.moc" + +QTEST_AKONADIMAIN(FavoriteProxyTest) diff --git a/autotests/libs/firstrunner.cpp b/autotests/libs/firstrunner.cpp new file mode 100644 index 0000000..38de845 --- /dev/null +++ b/autotests/libs/firstrunner.cpp @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "firstrun_p.h" + +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + KAboutData aboutData(QStringLiteral("akonadi-firstrun"), QStringLiteral("Test akonadi-firstrun"), QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + auto f = new Akonadi::Firstrun(); + QObject::connect(f, &Akonadi::Firstrun::destroyed, &app, &QApplication::quit); + app.exec(); +} diff --git a/autotests/libs/gidtest.cpp b/autotests/libs/gidtest.cpp new file mode 100644 index 0000000..a7ffa61 --- /dev/null +++ b/autotests/libs/gidtest.cpp @@ -0,0 +1,185 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "gidtest.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectionfetchjob.h" +#include "control.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "qtest_akonadi.h" +#include "testattribute.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(GidTest) + +bool TestSerializer::deserialize(Akonadi::Item &item, const QByteArray &label, QIODevice &data, int version) +{ + qDebug() << item.id(); + if (label != Akonadi::Item::FullPayload) { + return false; + } + Q_UNUSED(version) + + item.setPayload(data.readAll()); + return true; +} + +void TestSerializer::serialize(const Akonadi::Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + qDebug(); + Q_ASSERT(label == Akonadi::Item::FullPayload); + Q_UNUSED(label) + Q_UNUSED(version) + data.write(item.payload()); +} + +QString TestSerializer::extractGid(const Akonadi::Item &item) const +{ + if (item.gid().isEmpty()) { + return item.url().url(); + } + return item.gid(); +} + +void GidTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + + ItemSerializer::overridePluginLookup(new TestSerializer); +} + +void GidTest::testSetAndFetch_data() +{ + QTest::addColumn("input"); + QTest::addColumn("toFetch"); + QTest::addColumn("expected"); + + Item item1(1); + item1.setGid(QStringLiteral("gid1")); + Item item2(2); + item2.setGid(QStringLiteral("gid2")); + Item toFetch; + toFetch.setGid(QStringLiteral("gid1")); + QTest::newRow("single") << (Item::List() << item1) << toFetch << (Item::List() << item1); + QTest::newRow("multi") << (Item::List() << item1 << item2) << toFetch << (Item::List() << item1); + { + Item item3(3); + item2.setGid(QStringLiteral("gid1")); + QTest::newRow("multi") << (Item::List() << item1 << item2 << item3) << toFetch << (Item::List() << item1 << item3); + } +} + +static void fetchAndSetGid(const Item &item) +{ + auto prefetchjob = new ItemFetchJob(item); + prefetchjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(prefetchjob); + Item fetchedItem = prefetchjob->items()[0]; + + // Write the gid to the db + fetchedItem.setGid(item.gid()); + auto store = new ItemModifyJob(fetchedItem); + store->setUpdateGid(true); + AKVERIFYEXEC(store); +} + +void GidTest::testSetAndFetch() +{ + QFETCH(Item::List, input); + QFETCH(Item, toFetch); + QFETCH(Item::List, expected); + + for (const Item &item : std::as_const(input)) { + fetchAndSetGid(item); + } + + auto fetch = new ItemFetchJob(toFetch, this); + fetch->fetchScope().setFetchGid(true); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), expected.size()); + for (const Item &item : std::as_const(expected)) { + QVERIFY(expected.removeOne(item)); + } + QVERIFY(expected.isEmpty()); +} + +void GidTest::testCreate() +{ + const int colId = AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bar")); + QVERIFY(colId > -1); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(QByteArray("test")); + item.setGid(QStringLiteral("createGid")); + auto createJob = new ItemCreateJob(item, Collection(colId), this); + AKVERIFYEXEC(createJob); + auto fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); +} + +void GidTest::testSetWithIgnorePayload() +{ + Item item(5); + auto prefetchjob = new ItemFetchJob(item); + prefetchjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(prefetchjob); + Item fetchedItem = prefetchjob->items()[0]; + QVERIFY(fetchedItem.gid().isEmpty()); + + // Write the gid to the db + fetchedItem.setGid(QStringLiteral("gid5")); + auto store = new ItemModifyJob(fetchedItem); + store->setIgnorePayload(true); + store->setUpdateGid(true); + AKVERIFYEXEC(store); + Item toFetch; + toFetch.setGid(QStringLiteral("gid5")); + auto fetch = new ItemFetchJob(toFetch, this); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); + QCOMPARE(fetched.at(0).id(), Item::Id(5)); +} + +void GidTest::testFetchScope() +{ + const int colId = AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bar")); + QVERIFY(colId > -1); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(QByteArray("test")); + item.setGid(QStringLiteral("createGid2")); + auto createJob = new ItemCreateJob(item, Collection(colId), this); + AKVERIFYEXEC(createJob); + { + auto fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); + QVERIFY(fetched.at(0).gid().isNull()); + } + { + auto fetch = new ItemFetchJob(item, this); + fetch->fetchScope().setFetchGid(true); + AKVERIFYEXEC(fetch); + Item::List fetched = fetch->items(); + QCOMPARE(fetched.count(), 1); + QVERIFY(!fetched.at(0).gid().isNull()); + } +} diff --git a/autotests/libs/gidtest.h b/autotests/libs/gidtest.h new file mode 100644 index 0000000..056d14a --- /dev/null +++ b/autotests/libs/gidtest.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class GidTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testSetAndFetch_data(); + void testSetAndFetch(); + void testCreate(); + void testSetWithIgnorePayload(); + void testFetchScope(); +}; + +#include "gidextractorinterface.h" +#include "itemserializer_p.h" +#include "itemserializerplugin.h" + +class TestSerializer : public QObject, public Akonadi::ItemSerializerPlugin, public Akonadi::GidExtractorInterface +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) + Q_INTERFACES(Akonadi::GidExtractorInterface) + +public: + bool deserialize(Akonadi::Item &item, const QByteArray &label, QIODevice &data, int version) override; + void serialize(const Akonadi::Item &item, const QByteArray &label, QIODevice &data, int &version) override; + QString extractGid(const Akonadi::Item &item) const override; +}; + diff --git a/autotests/libs/inspectablechangerecorder.cpp b/autotests/libs/inspectablechangerecorder.cpp new file mode 100644 index 0000000..ce0f2f5 --- /dev/null +++ b/autotests/libs/inspectablechangerecorder.cpp @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "inspectablechangerecorder.h" + +InspectableChangeRecorderPrivate::InspectableChangeRecorderPrivate(FakeMonitorDependenciesFactory *dependenciesFactory, InspectableChangeRecorder *parent) + : ChangeRecorderPrivate(dependenciesFactory, parent) +{ +} + +InspectableChangeRecorder::InspectableChangeRecorder(FakeMonitorDependenciesFactory *dependenciesFactory, QObject *parent) + : ChangeRecorder(new Akonadi::ChangeRecorderPrivate(dependenciesFactory, this), parent) +{ + QTimer::singleShot(0, this, &InspectableChangeRecorder::doConnectToNotificationManager); +} + +void InspectableChangeRecorder::doConnectToNotificationManager() +{ + d_ptr->connectToNotificationManager(); +} diff --git a/autotests/libs/inspectablechangerecorder.h b/autotests/libs/inspectablechangerecorder.h new file mode 100644 index 0000000..26c20c4 --- /dev/null +++ b/autotests/libs/inspectablechangerecorder.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "changerecorder.h" +#include "changerecorder_p.h" +#include "entitycache_p.h" + +#include "akonaditestfake_export.h" +#include "fakeakonadiservercommand.h" +#include "fakeentitycache.h" + +class InspectableChangeRecorder; + +class InspectableChangeRecorderPrivate : public Akonadi::ChangeRecorderPrivate +{ +public: + InspectableChangeRecorderPrivate(FakeMonitorDependenciesFactory *dependenciesFactory, InspectableChangeRecorder *parent); + ~InspectableChangeRecorderPrivate() override + { + } + + bool emitNotification(const Akonadi::Protocol::ChangeNotificationPtr &msg) override + { + // TODO: Check/Log + return Akonadi::ChangeRecorderPrivate::emitNotification(msg); + } +}; + +class AKONADITESTFAKE_EXPORT InspectableChangeRecorder : public Akonadi::ChangeRecorder +{ + Q_OBJECT +public: + explicit InspectableChangeRecorder(FakeMonitorDependenciesFactory *dependenciesFactory, QObject *parent = nullptr); + + FakeNotificationConnection *notificationConnection() const + { + return qobject_cast(d_ptr->ntfConnection); + } + + QQueue pendingNotifications() const + { + return d_ptr->pendingNotifications; + } + QQueue pipeline() const + { + return d_ptr->pipeline; + } + +Q_SIGNALS: + void dummySignal(); + +private Q_SLOTS: + void dispatchNotifications() + { + d_ptr->dispatchNotifications(); + } + + void doConnectToNotificationManager(); + +private: + struct MessageStruct { + enum Position { + Queued, + FilterPipelined, + Pipelined, + Emitted, + }; + Position position; + }; + QQueue m_messages; +}; + diff --git a/autotests/libs/inspectablemonitor.cpp b/autotests/libs/inspectablemonitor.cpp new file mode 100644 index 0000000..77efee5 --- /dev/null +++ b/autotests/libs/inspectablemonitor.cpp @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "inspectablemonitor.h" + +InspectableMonitorPrivate::InspectableMonitorPrivate(FakeMonitorDependenciesFactory *dependenciesFactory, InspectableMonitor *parent) + : Akonadi::MonitorPrivate(dependenciesFactory, parent) +{ +} + +void InspectableMonitor::doConnectToNotificationManager() +{ + d_ptr->connectToNotificationManager(); +} + +InspectableMonitor::InspectableMonitor(FakeMonitorDependenciesFactory *dependenciesFactory, QObject *parent) + : Monitor(new InspectableMonitorPrivate(dependenciesFactory, this), parent) +{ + // Make sure signals don't get optimized away. + // TODO: Make this parametrizable in the test class. + connect(this, &Akonadi::Monitor::itemAdded, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::itemChanged, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::itemLinked, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::itemMoved, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::itemRemoved, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::itemUnlinked, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::collectionAdded, this, &InspectableMonitor::dummySignal); + connect(this, SIGNAL(collectionChanged(Akonadi::Collection)), SIGNAL(dummySignal())); + connect(this, SIGNAL(collectionChanged(Akonadi::Collection, QSet)), SIGNAL(dummySignal())); + connect(this, &Akonadi::Monitor::collectionMoved, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::collectionRemoved, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::collectionStatisticsChanged, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::collectionSubscribed, this, &InspectableMonitor::dummySignal); + connect(this, &Akonadi::Monitor::collectionUnsubscribed, this, &InspectableMonitor::dummySignal); + + QTimer::singleShot(0, this, [this]() { + doConnectToNotificationManager(); + }); +} diff --git a/autotests/libs/inspectablemonitor.h b/autotests/libs/inspectablemonitor.h new file mode 100644 index 0000000..1de6a34 --- /dev/null +++ b/autotests/libs/inspectablemonitor.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entitycache_p.h" +#include "monitor.h" +#include "monitor_p.h" + +#include "akonaditestfake_export.h" +#include "fakeakonadiservercommand.h" +#include "fakeentitycache.h" + +class InspectableMonitor; + +class InspectableMonitorPrivate : public Akonadi::MonitorPrivate +{ +public: + InspectableMonitorPrivate(FakeMonitorDependenciesFactory *dependenciesFactory, InspectableMonitor *parent); + ~InspectableMonitorPrivate() override + { + } + + bool emitNotification(const Akonadi::Protocol::ChangeNotificationPtr &msg) override + { + // TODO: Check/Log + return Akonadi::MonitorPrivate::emitNotification(msg); + } +}; + +class AKONADITESTFAKE_EXPORT InspectableMonitor : public Akonadi::Monitor +{ + Q_OBJECT +public: + explicit InspectableMonitor(FakeMonitorDependenciesFactory *dependenciesFactory, QObject *parent = nullptr); + + FakeNotificationConnection *notificationConnection() const + { + return qobject_cast(d_ptr->ntfConnection); + } + + QQueue pendingNotifications() const + { + return d_ptr->pendingNotifications; + } + QQueue pipeline() const + { + return d_ptr->pipeline; + } + +Q_SIGNALS: + void dummySignal(); + +private Q_SLOTS: + void dispatchNotifications() + { + d_ptr->dispatchNotifications(); + } + + void doConnectToNotificationManager(); + +private: + struct MessageStruct { + enum Position { + Queued, + FilterPipelined, + Pipelined, + Emitted, + }; + Position position; + }; + QQueue m_messages; +}; + diff --git a/autotests/libs/invalidatecachejobtest.cpp b/autotests/libs/invalidatecachejobtest.cpp new file mode 100644 index 0000000..d860a32 --- /dev/null +++ b/autotests/libs/invalidatecachejobtest.cpp @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "invalidatecachejob_p.h" + +#include "collectionpathresolver.h" +#include "control.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class InvalidateCacheJobTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void shouldClearPayload(); +}; + +void InvalidateCacheJobTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); +} + +void InvalidateCacheJobTest::shouldClearPayload() +{ + // Find collection by name + Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + const int colId = col.id(); + QVERIFY(colId > 0); + + // Find item with remote id "C" + auto listJob = new ItemFetchJob(Collection(colId), this); + AKVERIFYEXEC(listJob); + const Item::List items = listJob->items(); + QVERIFY(!items.isEmpty()); + auto it = std::find_if(items.cbegin(), items.cend(), [](const Item &item) { + return item.remoteId() == QLatin1Char('C'); + }); + QVERIFY(it != items.cend()); + const Item::Id itemId = it->id(); + + // Fetch item, from resource, with payload + auto fetchJob = new ItemFetchJob(Item(itemId), this); + fetchJob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().payload(), "testmailbody2"); + + auto invCacheJob = new InvalidateCacheJob(Collection(colId), this); + AKVERIFYEXEC(invCacheJob); + + // Fetch item from cache, should have no payload anymore + auto fetchFromCacheJob = new ItemFetchJob(Item(itemId), this); + fetchFromCacheJob->fetchScope().fetchFullPayload(); + fetchFromCacheJob->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetchFromCacheJob); + QVERIFY(fetchFromCacheJob->items().first().payload().isEmpty()); + + // Fetch item from resource again + auto fetchAgainJob = new ItemFetchJob(Item(itemId), this); + fetchAgainJob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fetchAgainJob); + QCOMPARE(fetchAgainJob->items().first().payload(), "testmailbody2"); +} + +QTEST_AKONADIMAIN(InvalidateCacheJobTest) + +#include "invalidatecachejobtest.moc" diff --git a/autotests/libs/itemappendtest.cpp b/autotests/libs/itemappendtest.cpp new file mode 100644 index 0000000..3ef43f8 --- /dev/null +++ b/autotests/libs/itemappendtest.cpp @@ -0,0 +1,389 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemappendtest.h" +#include "qtest_akonadi.h" + +#include "control.h" +#include "testattribute.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "attributefactory.h" +#include "collectionfetchjob.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(ItemAppendTest) + +void ItemAppendTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + AttributeFactory::registerAttribute(); +} + +void ItemAppendTest::testItemAppend_data() +{ + QTest::addColumn("remoteId"); + + QTest::newRow("empty") << QString(); + QTest::newRow("non empty") << QStringLiteral("remote-id"); + QTest::newRow("whitespace") << QStringLiteral("remote id"); + QTest::newRow("quotes") << QStringLiteral("\"remote\" id"); + QTest::newRow("brackets") << QStringLiteral("[remote id]"); + QTest::newRow("RID length limit") << QStringLiteral("a").repeated(1024); +} + +void ItemAppendTest::testItemAppend() +{ + const Collection testFolder1(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + QFETCH(QString, remoteId); + Item ref; // for cleanup + + Item item(-1); + item.setRemoteId(remoteId); + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setFlag("TestFlag"); + item.setSize(3456); + auto job = new ItemCreateJob(item, testFolder1, this); + AKVERIFYEXEC(job); + ref = job->item(); + QCOMPARE(ref.parentCollection(), testFolder1); + + auto fjob = new ItemFetchJob(testFolder1, this); + fjob->fetchScope().setAncestorRetrieval(ItemFetchScope::Parent); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + QCOMPARE(fjob->items()[0], ref); + QCOMPARE(fjob->items()[0].remoteId(), remoteId); + QVERIFY(fjob->items()[0].flags().contains("TestFlag")); + QCOMPARE(fjob->items()[0].parentCollection(), ref.parentCollection()); + + qint64 size = 3456; + QCOMPARE(fjob->items()[0].size(), size); + + auto djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(testFolder1, this); + AKVERIFYEXEC(fjob); + QVERIFY(fjob->items().isEmpty()); +} + +void ItemAppendTest::testContent_data() +{ + QTest::addColumn("data"); + + QTest::newRow("null") << QByteArray(); + QTest::newRow("empty") << QByteArray(""); + QTest::newRow("nullbyte") << QByteArray("\0", 1); + QTest::newRow("nullbyte2") << QByteArray("\0X", 2); + QString utf8string = QStringLiteral("äöüß@€µøđ¢©®"); + QTest::newRow("utf8") << utf8string.toUtf8(); + QTest::newRow("newlines") << QByteArray("\nsome\n\nbreaked\ncontent\n\n"); + QByteArray b; + QTest::newRow("big") << b.fill('a', 1U << 20); + QTest::newRow("bignull") << b.fill('\0', 1U << 20); + QTest::newRow("bigcr") << b.fill('\r', 1U << 20); + QTest::newRow("biglf") << b.fill('\n', 1U << 20); +} + +void ItemAppendTest::testContent() +{ + const Collection testFolder1(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + QFETCH(QByteArray, data); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + if (!data.isNull()) { + item.setPayload(data); + } + + auto job = new ItemCreateJob(item, testFolder1, this); + AKVERIFYEXEC(job); + Item ref = job->item(); + + auto fjob = new ItemFetchJob(testFolder1, this); + fjob->fetchScope().setCacheOnly(true); + fjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + Item item2 = fjob->items().first(); + QCOMPARE(item2.hasPayload(), !data.isNull()); + if (item2.hasPayload()) { + QCOMPARE(item2.payload(), data); + } + + auto djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); +} + +void ItemAppendTest::testNewMimetype() +{ + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + Item item; + item.setMimeType(QStringLiteral("application/new-type")); + auto job = new ItemCreateJob(item, col, this); + AKVERIFYEXEC(job); + + item = job->item(); + QVERIFY(item.isValid()); + + auto fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + QCOMPARE(fetch->items().first().mimeType(), item.mimeType()); +} + +void ItemAppendTest::testIllegalAppend() +{ + const Collection testFolder1(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + + // adding item to non-existing collection + auto job = new ItemCreateJob(item, Collection(INT_MAX), this); + QVERIFY(!job->exec()); + + // adding item into a collection which can't handle items of this type + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bla"))); + QVERIFY(col.isValid()); + job = new ItemCreateJob(item, col, this); + QEXPECT_FAIL("", "Test not yet implemented in the server.", Continue); + QVERIFY(!job->exec()); +} + +void ItemAppendTest::testMultipartAppend() +{ + const Collection testFolder1(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(testFolder1.isValid()); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + item.attribute(Item::AddIfMissing)->data = "extra data"; + item.setFlag("TestFlag"); + auto job = new ItemCreateJob(item, testFolder1, this); + AKVERIFYEXEC(job); + Item ref = job->item(); + + auto fjob = new ItemFetchJob(ref, this); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + QCOMPARE(item.payload(), QByteArray("body data")); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, QByteArray("extra data")); + QVERIFY(item.flags().contains("TestFlag")); + + auto djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); +} + +void ItemAppendTest::testInvalidMultipartAppend() +{ + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + item.attribute(Item::AddIfMissing)->data = "extra data"; + item.setFlag("TestFlag"); + auto job = new ItemCreateJob(item, Collection(-1), this); + QVERIFY(!job->exec()); + + Item item2; + item2.setMimeType(QStringLiteral("application/octet-stream")); + item2.setPayload("more body data"); + item2.attribute(Item::AddIfMissing)->data = "even more extra data"; + item2.setFlag("TestFlag"); + auto job2 = new ItemCreateJob(item2, Collection(-1), this); + QVERIFY(!job2->exec()); +} + +void ItemAppendTest::testItemSize_data() +{ + QTest::addColumn("item"); + QTest::addColumn("size"); + + Item i(QStringLiteral("application/octet-stream")); + i.setPayload(QByteArray("ABCD")); + + QTest::newRow("auto size") << i << 56LL; + i.setSize(3); + QTest::newRow("too small") << i << 56LL; + i.setSize(100); + QTest::newRow("too large") << i << 100LL; +} + +void ItemAppendTest::testItemSize() +{ + QFETCH(Akonadi::Item, item); + QFETCH(qint64, size); + + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + auto create = new ItemCreateJob(item, col, this); + AKVERIFYEXEC(create); + Item newItem = create->item(); + + auto fetch = new ItemFetchJob(newItem, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + + QCOMPARE(fetch->items().first().size(), size); +} + +void ItemAppendTest::testItemMerge_data() +{ + QTest::addColumn("item1"); + QTest::addColumn("item2"); + QTest::addColumn("mergedItem"); + QTest::addColumn("silent"); + + { + Item i1(QStringLiteral("application/octet-stream")); + i1.setPayload(QByteArray("ABCD")); + i1.setSize(56); // take compression into account + i1.setRemoteId(QStringLiteral("XYZ")); + i1.setGid(QStringLiteral("XYZ")); + i1.setFlag("TestFlag1"); + i1.setRemoteRevision(QStringLiteral("5")); + + Item i2(QStringLiteral("application/octet-stream")); + i2.setPayload(QByteArray("DEFGH")); + i2.setSize(60); // the compression into account + i2.setRemoteId(QStringLiteral("XYZ")); + i2.setGid(QStringLiteral("XYZ")); + i2.setFlag("TestFlag2"); + i2.setRemoteRevision(QStringLiteral("6")); + + Item mergedItem(i2); + mergedItem.setFlag("TestFlag1"); + + QTest::newRow("merge") << i1 << i2 << mergedItem << false; + QTest::newRow("merge (silent)") << i1 << i2 << mergedItem << true; + } + { + Item i1(QStringLiteral("application/octet-stream")); + i1.setPayload(QByteArray("ABCD")); + i1.setSize(56); // take compression into account + i1.setRemoteId(QStringLiteral("RID2")); + i1.setGid(QStringLiteral("GID2")); + i1.setFlag("TestFlag1"); + i1.setRemoteRevision(QStringLiteral("5")); + + Item i2(QStringLiteral("application/octet-stream")); + i2.setRemoteId(QStringLiteral("RID2")); + i2.setGid(QStringLiteral("GID2")); + i2.setFlags(Item::Flags() << "TestFlag2"); + i2.setRemoteRevision(QStringLiteral("6")); + + Item mergedItem(i1); + mergedItem.setFlags(i2.flags()); + mergedItem.setRemoteRevision(i2.remoteRevision()); + + QTest::newRow("overwrite flags, and don't remove existing payload") << i1 << i2 << mergedItem << false; + QTest::newRow("overwrite flags, and don't remove existing payload (silent)") << i1 << i2 << mergedItem << true; + } +} + +void ItemAppendTest::testItemMerge() +{ + QFETCH(Akonadi::Item, item1); + QFETCH(Akonadi::Item, item2); + QFETCH(Akonadi::Item, mergedItem); + QFETCH(bool, silent); + + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + auto create = new ItemCreateJob(item1, col, this); + AKVERIFYEXEC(create); + const Item createdItem = create->item(); + + auto merge = new ItemCreateJob(item2, col, this); + ItemCreateJob::MergeOptions options = ItemCreateJob::GID | ItemCreateJob::RID; + if (silent) { + options |= ItemCreateJob::Silent; + } + merge->setMerge(options); + AKVERIFYEXEC(merge); + + QCOMPARE(merge->item().id(), createdItem.id()); + if (!silent) { + QCOMPARE(merge->item().gid(), mergedItem.gid()); + QCOMPARE(merge->item().remoteId(), mergedItem.remoteId()); + QCOMPARE(merge->item().remoteRevision(), mergedItem.remoteRevision()); + QCOMPARE(merge->item().payloadData(), mergedItem.payloadData()); + QCOMPARE(merge->item().size(), mergedItem.size()); + qDebug() << merge->item().flags() << mergedItem.flags(); + QCOMPARE(merge->item().flags(), mergedItem.flags()); + } + + if (merge->item().id() != createdItem.id()) { + auto del = new ItemDeleteJob(merge->item(), this); + AKVERIFYEXEC(del); + } + auto del = new ItemDeleteJob(createdItem, this); + AKVERIFYEXEC(del); +} + +void ItemAppendTest::testForeignPayload() +{ + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + QVERIFY(col.isValid()); + + const QString filePath = QString::fromUtf8(qgetenv("TMPDIR")) + QStringLiteral("/foreignPayloadFile.mbox"); + QFile file(filePath); + QVERIFY(file.open(QIODevice::WriteOnly)); + file.write("123456789"); + file.close(); + + Item item(QStringLiteral("application/octet-stream")); + item.setPayloadPath(filePath); + item.setRemoteId(QStringLiteral("RID3")); + item.setSize(9); + + auto create = new ItemCreateJob(item, col, this); + AKVERIFYEXEC(create); + + auto ref = create->item(); + + auto fetch = new ItemFetchJob(ref, this); + fetch->fetchScope().fetchFullPayload(true); + AKVERIFYEXEC(fetch); + const auto items = fetch->items(); + QCOMPARE(items.size(), 1); + item = items[0]; + + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("123456789")); + + auto del = new ItemDeleteJob(item, this); + AKVERIFYEXEC(del); + + // Make sure Akonadi does not delete a foreign payload + QVERIFY(file.exists()); + QVERIFY(file.remove()); +} diff --git a/autotests/libs/itemappendtest.h b/autotests/libs/itemappendtest.h new file mode 100644 index 0000000..72f49d2 --- /dev/null +++ b/autotests/libs/itemappendtest.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ItemAppendTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testItemAppend_data(); + void testItemAppend(); + void testContent_data(); + void testContent(); + void testNewMimetype(); + void testIllegalAppend(); + void testMultipartAppend(); + void testInvalidMultipartAppend(); + void testItemSize_data(); + void testItemSize(); + void testItemMerge_data(); + void testItemMerge(); + void testForeignPayload(); +}; + diff --git a/autotests/libs/itembenchmark.cpp b/autotests/libs/itembenchmark.cpp new file mode 100644 index 0000000..ac95244 --- /dev/null +++ b/autotests/libs/itembenchmark.cpp @@ -0,0 +1,179 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentmanager.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "qtest_akonadi.h" + +#include + +using namespace Akonadi; + +class ItemBenchmark : public QObject +{ + Q_OBJECT +public Q_SLOTS: + void createResult(KJob *job) + { + Q_ASSERT(job->error() == KJob::NoError); + Item createdItem = static_cast(job)->item(); + mCreatedItems[createdItem.size()].append(createdItem); + } + + void fetchResult(KJob *job) + { + Q_ASSERT(job->error() == KJob::NoError); + } + + void modifyResult(KJob *job) + { + Q_ASSERT(job->error() == KJob::NoError); + } + +private: + QMap mCreatedItems; + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + } + + void data() + { + QTest::addColumn("count"); + QTest::addColumn("size"); + + QList counts = QList() << 1 << 10 << 100 << 1000; // << 10000; + QList sizes = QList() << 0 << 256 << 1024 << 8192 << 32768 << 65536; + foreach (int count, counts) + foreach (int size, sizes) + QTest::newRow(QString::fromLatin1("%1-%2").arg(count).arg(size).toLatin1().constData()) << count << size; + } + + void itemBenchmarkCreate_data() + { + data(); + } + void itemBenchmarkCreate() /// Tests performance of creating items in the cache + { + QFETCH(int, count); + QFETCH(int, size); + + const Collection parent(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(parent.isValid()); + + Item item(QStringLiteral("application/octet-stream")); + item.setPayload(QByteArray(size, 'X')); + item.setSize(size); + + Job *lastJob = 0; + QBENCHMARK { + for (int i = 0; i < count; ++i) { + lastJob = new ItemCreateJob(item, parent, this); + connect(lastJob, SIGNAL(result(KJob *)), SLOT(createResult(KJob *))); + } + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob *))); + } + } + + void itemBenchmarkFetch_data() + { + data(); + } + void itemBenchmarkFetch() /// Tests performance of fetching cached items + { + QFETCH(int, count); + QFETCH(int, size); + + // With only one iteration itemBenchmarkCreate() should have created count + // items, otherwise iterations * count, however, at least count items should + // be there. + QVERIFY(mCreatedItems.value(size).count() >= count); + + QBENCHMARK { + Item::List items; + for (int i = 0; i < count; ++i) { + items << mCreatedItems[size].at(i); + } + + ItemFetchJob *fetchJob = new ItemFetchJob(items, this); + fetchJob->fetchScope().fetchFullPayload(); + fetchJob->fetchScope().setCacheOnly(true); + connect(fetchJob, SIGNAL(result(KJob *)), SLOT(fetchResult(KJob *))); + AkonadiTest::akWaitForSignal(fetchJob, SIGNAL(result(KJob *))); + } + } + + void itemBenchmarkModifyPayload_data() + { + data(); + } + void itemBenchmarkModifyPayload() /// Tests performance of modifying payload of cached items + { + QFETCH(int, count); + QFETCH(int, size); + + // With only one iteration itemBenchmarkCreate() should have created count + // items, otherwise iterations * count, however, at least count items should + // be there. + QVERIFY(mCreatedItems.value(size).count() >= count); + + Job *lastJob = 0; + const int newSize = qMax(size, 1); + QBENCHMARK { + for (int i = 0; i < count; ++i) { + Item item = mCreatedItems.value(size).at(i); + item.setPayload(QByteArray(newSize, 'Y')); + ItemModifyJob *job = new ItemModifyJob(item, this); + job->disableRevisionCheck(); + lastJob = job; + connect(lastJob, SIGNAL(result(KJob *)), SLOT(modifyResult(KJob *))); + } + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob *))); + } + } + + void itemBenchmarkDelete_data() + { + data(); + } + void itemBenchmarkDelete() /// Tests performance of removing items from the cache + { + QFETCH(int, count); + QFETCH(int, size); + + Job *lastJob = 0; + int emptyItemArrayIterations = 0; + QBENCHMARK { + if (mCreatedItems[size].isEmpty()) { + ++emptyItemArrayIterations; + } + + Item::List items; + for (int i = 0; i < count && !mCreatedItems[size].isEmpty(); ++i) { + items << mCreatedItems[size].takeFirst(); + } + lastJob = new ItemDeleteJob(items, this); + AkonadiTest::akWaitForSignal(lastJob, SIGNAL(result(KJob *))); + } + + if (emptyItemArrayIterations) { + qDebug() << "Delete Benchmark performed" << emptyItemArrayIterations << "times on an empty list."; + } + } +}; + +QTEST_AKONADIMAIN(ItemBenchmark) + +#include "itembenchmark.moc" diff --git a/autotests/libs/itemcopytest.cpp b/autotests/libs/itemcopytest.cpp new file mode 100644 index 0000000..e55c50a --- /dev/null +++ b/autotests/libs/itemcopytest.cpp @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" +#include "collectionstatistics.h" +#include "control.h" +#include "itemcopyjob.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include "qtest_akonadi.h" + +#include +#include + +using namespace Akonadi; + +class ItemCopyTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + // switch target resources offline to reduce interference from them + foreach (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) { // krazy:exclude=foreach + if (agent.identifier() == QLatin1String("akonadi_knut_resource_2")) { + agent.setIsOnline(false); + } + } + } + + void testCopy() + { + const Collection target(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(target.isValid()); + + auto copy = new ItemCopyJob(Item(1), target); + AKVERIFYEXEC(copy); + + Item source(1); + auto sourceFetch = new ItemFetchJob(source); + AKVERIFYEXEC(sourceFetch); + source = sourceFetch->items().first(); + + auto fetch = new ItemFetchJob(target); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().fetchAllAttributes(); + fetch->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + + Item item = fetch->items().first(); + QVERIFY(item.hasPayload()); + QVERIFY(source.size() > 0); + QVERIFY(item.size() > 0); + QCOMPARE(item.size(), source.size()); + QCOMPARE(item.attributes().count(), 1); + QVERIFY(item.remoteId().isEmpty()); + QEXPECT_FAIL("", "statistics are not properly updated after copy", Abort); + QCOMPARE(target.statistics().count(), 1LL); + } + + void testIlleagalCopy() + { + // empty item list + auto copy = new ItemCopyJob(Item::List(), Collection::root()); + QVERIFY(!copy->exec()); + + // non-existing target + copy = new ItemCopyJob(Item(1), Collection(INT_MAX)); + QVERIFY(!copy->exec()); + + // non-existing source + copy = new ItemCopyJob(Item(INT_MAX), Collection::root()); + QVERIFY(!copy->exec()); + } + + void testCopyForeign() + { + QTemporaryFile file; + QVERIFY(file.open()); + file.write("123456789"); + file.close(); + + const Collection source(AkonadiTest::collectionIdFromPath(QStringLiteral("res2"))); + + Item item(QStringLiteral("application/octet-stream")); + item.setPayloadPath(file.fileName()); + + auto create = new ItemCreateJob(item, source, this); + AKVERIFYEXEC(create); + item = create->item(); + + const Collection target(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/space folder"))); + auto copy = new ItemCopyJob(item, target, this); + AKVERIFYEXEC(copy); + + auto fetch = new ItemFetchJob(target, this); + fetch->fetchScope().fetchFullPayload(true); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().size(), 1); + auto copiedItem = fetch->items().at(0); + + // Copied payload should be completely stored inside Akonadi + QVERIFY(copiedItem.payloadPath().isEmpty()); + QVERIFY(copiedItem.hasPayload()); + QCOMPARE(copiedItem.payload(), item.payload()); + } +}; + +QTEST_AKONADIMAIN(ItemCopyTest) + +#include "itemcopytest.moc" diff --git a/autotests/libs/itemdeletetest.cpp b/autotests/libs/itemdeletetest.cpp new file mode 100644 index 0000000..d0e2b93 --- /dev/null +++ b/autotests/libs/itemdeletetest.cpp @@ -0,0 +1,220 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include "collectionpathresolver.h" +#include "control.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemmodifyjob.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" +#include "tagcreatejob.h" +#include "transactionjobs.h" + +#include + +#include + +using namespace Akonadi; + +class ItemDeleteTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + void testIllegalDelete() + { + auto djob = new ItemDeleteJob(Item(INT_MAX), this); + QVERIFY(!djob->exec()); + + // make sure a failed delete doesn't leave a transaction open (the kpilot bug) + auto tjob = new TransactionRollbackJob(this); + QVERIFY(!tjob->exec()); + } + + void testDelete() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy spy(monitor.get(), &Monitor::itemsRemoved); + + auto fjob = new ItemFetchJob(Item(1), this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + auto djob = new ItemDeleteJob(Item(1), this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(Item(1), this); + QVERIFY(!fjob->exec()); + + QTRY_COMPARE(spy.count(), 1); + auto items = spy.at(0).at(0).value(); + QCOMPARE(items.count(), 1); + QCOMPARE(items.at(0).id(), 1); + QVERIFY(items.at(0).parentCollection().isValid()); + } + + void testDeleteFromUnselectedCollection() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy spy(monitor.get(), &Monitor::itemsRemoved); + + const QString path = QLatin1String("res1") + CollectionPathResolver::pathDelimiter() + QLatin1String("foo"); + auto rjob = new CollectionPathResolver(path, this); + AKVERIFYEXEC(rjob); + + auto fjob = new ItemFetchJob(Collection(rjob->collection()), this); + AKVERIFYEXEC(fjob); + + const Item::List items = fjob->items(); + QVERIFY(!items.isEmpty()); + + fjob = new ItemFetchJob(items[0], this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + auto djob = new ItemDeleteJob(items[0], this); + AKVERIFYEXEC(djob); + + fjob = new ItemFetchJob(items[0], this); + QVERIFY(!fjob->exec()); + + QTRY_COMPARE(spy.count(), 1); + auto ntfItems = spy.at(0).at(0).value(); + QCOMPARE(ntfItems.count(), 1); + QCOMPARE(ntfItems.at(0).id(), items[0].id()); + QVERIFY(ntfItems.at(0).parentCollection().isValid()); + } + + void testRidDelete() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy spy(monitor.get(), &Monitor::itemsRemoved); + + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + + Item i; + i.setRemoteId(QStringLiteral("C")); + + auto fjob = new ItemFetchJob(i, this); + fjob->setCollection(col); + AKVERIFYEXEC(fjob); + auto items = fjob->items(); + QCOMPARE(items.count(), 1); + + auto djob = new ItemDeleteJob(i, this); + AKVERIFYEXEC(djob); + + QTRY_COMPARE(spy.count(), 1); + auto ntfItems = spy.at(0).at(0).value(); + QCOMPARE(ntfItems.count(), 1); + QCOMPARE(ntfItems.at(0).id(), items[0].id()); + QVERIFY(ntfItems.at(0).parentCollection().isValid()); + + fjob = new ItemFetchJob(i, this); + fjob->setCollection(col); + QVERIFY(!fjob->exec()); + { + auto select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } + } + + void testTagDelete() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy spy(monitor.get(), &Monitor::itemsRemoved); + + // Create tag + Tag tag; + tag.setName(QStringLiteral("Tag1")); + tag.setGid("Tag1"); + auto tjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(tjob); + tag = tjob->tag(); + + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + + Item i; + i.setRemoteId(QStringLiteral("D")); + + auto fjob = new ItemFetchJob(i, this); + fjob->setCollection(col); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + + i = fjob->items().first(); + i.setTag(tag); + auto mjob = new ItemModifyJob(i, this); + AKVERIFYEXEC(mjob); + + // Delete the tagged item + auto djob = new ItemDeleteJob(tag, this); + AKVERIFYEXEC(djob); + + QTRY_COMPARE(spy.count(), 1); + auto ntfItems = spy.at(0).at(0).value(); + QCOMPARE(ntfItems.count(), 1); + QCOMPARE(ntfItems.at(0).id(), i.id()); + QVERIFY(ntfItems.at(0).parentCollection().isValid()); + + // Try to fetch the item again, there should be none + fjob = new ItemFetchJob(i, this); + QVERIFY(!fjob->exec()); + } + + void testCollectionDelete() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy spy(monitor.get(), &Monitor::itemsRemoved); + + const Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + auto fjob = new ItemFetchJob(col, this); + AKVERIFYEXEC(fjob); + auto items = fjob->items(); + QVERIFY(items.count() > 0); + + // delete from non-empty collection + auto djob = new ItemDeleteJob(col, this); + AKVERIFYEXEC(djob); + + QTRY_COMPARE(spy.count(), 1); + auto ntfItems = spy.at(0).at(0).value(); + QCOMPARE(ntfItems.count(), items.count()); + if (ntfItems.count() > 0) { + QVERIFY(ntfItems.at(0).parentCollection().isValid()); + } + + fjob = new ItemFetchJob(col, this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 0); + + // delete from empty collection + djob = new ItemDeleteJob(col, this); + QVERIFY(!djob->exec()); // error: no items found + + fjob = new ItemFetchJob(col, this); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 0); + } +}; + +QTEST_AKONADIMAIN(ItemDeleteTest) + +#include "itemdeletetest.moc" diff --git a/autotests/libs/itemfetchtest.cpp b/autotests/libs/itemfetchtest.cpp new file mode 100644 index 0000000..bb4f8b2 --- /dev/null +++ b/autotests/libs/itemfetchtest.cpp @@ -0,0 +1,264 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemfetchtest.h" +#include "attributefactory.h" +#include "collectionpathresolver.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "resourceselectjob_p.h" +#include "testattribute.h" + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(ItemFetchTest) + +void ItemFetchTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + qRegisterMetaType(); + AttributeFactory::registerAttribute(); +} + +void ItemFetchTest::testFetch() +{ + auto resolver = new CollectionPathResolver(QStringLiteral("res1"), this); + AKVERIFYEXEC(resolver); + int colId = resolver->collection(); + + // listing of an empty folder + auto job = new ItemFetchJob(Collection(colId), this); + AKVERIFYEXEC(job); + QVERIFY(job->items().isEmpty()); + + resolver = new CollectionPathResolver(QStringLiteral("res1/foo"), this); + AKVERIFYEXEC(resolver); + int colId2 = resolver->collection(); + + // listing of a non-empty folder + job = new ItemFetchJob(Collection(colId2), this); + QSignalSpy spy(job, &ItemFetchJob::itemsReceived); + QVERIFY(spy.isValid()); + AKVERIFYEXEC(job); + Item::List items = job->items(); + QCOMPARE(items.count(), 15); + + int count = 0; + for (int i = 0; i < spy.count(); ++i) { + auto l = spy[i][0].value(); + for (int j = 0; j < l.count(); ++j) { + QVERIFY(items.count() > count + j); + QCOMPARE(items[count + j], l[j]); + } + count += l.count(); + } + QCOMPARE(count, items.count()); + + // check if the fetch response is parsed correctly (note: order is undefined) + Item item; + foreach (const Item &it, items) { + if (it.remoteId() == QLatin1Char('A')) { + item = it; + } + } + QVERIFY(item.isValid()); + + QCOMPARE(item.flags().count(), 3); + QVERIFY(item.hasFlag("\\SEEN")); + QVERIFY(item.hasFlag("\\FLAGGED")); + QVERIFY(item.hasFlag("\\DRAFT")); + + item = Item(); + foreach (const Item &it, items) { + if (it.remoteId() == QLatin1Char('B')) { + item = it; + } + } + QVERIFY(item.isValid()); + QCOMPARE(item.flags().count(), 1); + QVERIFY(item.hasFlag("\\FLAGGED")); + + item = Item(); + foreach (const Item &it, items) { + if (it.remoteId() == QLatin1Char('C')) { + item = it; + } + } + QVERIFY(item.isValid()); + QVERIFY(item.flags().isEmpty()); +} + +void ItemFetchTest::testResourceRetrieval() +{ + Item item(1); + + auto job = new ItemFetchJob(item, this); + job->fetchScope().fetchFullPayload(true); + job->fetchScope().fetchAllAttributes(true); + job->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + item = job->items().first(); + QCOMPARE(item.id(), 1LL); + QVERIFY(!item.remoteId().isEmpty()); + QVERIFY(!item.hasPayload()); // not yet in cache + QCOMPARE(item.attributes().count(), 1); + + job = new ItemFetchJob(item, this); + job->fetchScope().fetchFullPayload(true); + job->fetchScope().fetchAllAttributes(true); + job->fetchScope().setCacheOnly(false); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + item = job->items().first(); + QCOMPARE(item.id(), 1LL); + QVERIFY(!item.remoteId().isEmpty()); + QVERIFY(item.hasPayload()); + QCOMPARE(item.attributes().count(), 1); +} + +void ItemFetchTest::testIllegalFetch() +{ + // fetch non-existing folder + auto job = new ItemFetchJob(Collection(INT_MAX), this); + QVERIFY(!job->exec()); + + // listing of root + job = new ItemFetchJob(Collection::root(), this); + QVERIFY(!job->exec()); + + // fetch a non-existing message + job = new ItemFetchJob(Item(INT_MAX), this); + QVERIFY(!job->exec()); + QVERIFY(job->items().isEmpty()); + + // fetch message with empty reference + job = new ItemFetchJob(Item(), this); + QVERIFY(!job->exec()); +} + +void ItemFetchTest::testMultipartFetch_data() +{ + QTest::addColumn("fetchFullPayload"); + QTest::addColumn("fetchAllAttrs"); + QTest::addColumn("fetchSinglePayload"); + QTest::addColumn("fetchSingleAttr"); + + QTest::newRow("empty") << false << false << false << false; + QTest::newRow("full") << true << true << false << false; + QTest::newRow("full payload") << true << false << false << false; + QTest::newRow("single payload") << false << false << true << false; + QTest::newRow("single") << false << false << true << true; + QTest::newRow("attr full") << false << true << false << false; + QTest::newRow("attr single") << false << false << false << true; + QTest::newRow("mixed cross 1") << true << false << false << true; + QTest::newRow("mixed cross 2") << false << true << true << false; + QTest::newRow("all") << true << true << true << true; + QTest::newRow("all payload") << true << false << true << false; + QTest::newRow("all attr") << false << true << true << false; +} + +void ItemFetchTest::testMultipartFetch() +{ + QFETCH(bool, fetchFullPayload); + QFETCH(bool, fetchAllAttrs); + QFETCH(bool, fetchSinglePayload); + QFETCH(bool, fetchSingleAttr); + + auto resolver = new CollectionPathResolver(QStringLiteral("res1/foo"), this); + AKVERIFYEXEC(resolver); + int colId = resolver->collection(); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + item.attribute(Item::AddIfMissing)->data = "extra data"; + auto job = new ItemCreateJob(item, Collection(colId), this); + AKVERIFYEXEC(job); + Item ref = job->item(); + + auto fjob = new ItemFetchJob(ref, this); + fjob->setCollection(Collection(colId)); + if (fetchFullPayload) { + fjob->fetchScope().fetchFullPayload(); + } + if (fetchAllAttrs) { + fjob->fetchScope().fetchAttribute(); + } + if (fetchSinglePayload) { + fjob->fetchScope().fetchPayloadPart(Item::FullPayload); + } + if (fetchSingleAttr) { + fjob->fetchScope().fetchAttribute(); + } + + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items().first(); + + if (fetchFullPayload || fetchSinglePayload) { + QCOMPARE(item.loadedPayloadParts().count(), 1); + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("body data")); + } else { + QCOMPARE(item.loadedPayloadParts().count(), 0); + QVERIFY(!item.hasPayload()); + } + + if (fetchAllAttrs || fetchSingleAttr) { + QCOMPARE(item.attributes().count(), 1); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, QByteArray("extra data")); + } else { + QCOMPARE(item.attributes().count(), 0); + } + + // cleanup + auto djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); +} + +void ItemFetchTest::testRidFetch() +{ + Item item; + item.setRemoteId(QStringLiteral("A")); + Collection col; + col.setRemoteId(QStringLiteral("10")); + + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); + AKVERIFYEXEC(select); + + auto job = new ItemFetchJob(item, this); + job->setCollection(col); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + item = job->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.remoteId(), QString::fromLatin1("A")); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); +} + +void ItemFetchTest::testAncestorRetrieval() +{ + auto job = new ItemFetchJob(Item(1), this); + job->fetchScope().setAncestorRetrieval(ItemFetchScope::All); + AKVERIFYEXEC(job); + QCOMPARE(job->items().count(), 1); + const Item item = job->items().first(); + QVERIFY(item.isValid()); + QCOMPARE(item.remoteId(), QString::fromLatin1("A")); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); + const Collection c = item.parentCollection(); + QCOMPARE(c.remoteId(), QLatin1String("10")); + const Collection c2 = c.parentCollection(); + QCOMPARE(c2.remoteId(), QLatin1String("6")); + const Collection c3 = c2.parentCollection(); + QCOMPARE(c3, Collection::root()); +} diff --git a/autotests/libs/itemfetchtest.h b/autotests/libs/itemfetchtest.h new file mode 100644 index 0000000..d53a7ef --- /dev/null +++ b/autotests/libs/itemfetchtest.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ItemFetchTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testFetch(); + void testResourceRetrieval(); + void testIllegalFetch(); + void testMultipartFetch_data(); + void testMultipartFetch(); + void testRidFetch(); + void testAncestorRetrieval(); +}; + diff --git a/autotests/libs/itemhydratest.cpp b/autotests/libs/itemhydratest.cpp new file mode 100644 index 0000000..52de6cb --- /dev/null +++ b/autotests/libs/itemhydratest.cpp @@ -0,0 +1,389 @@ +/* + SPDX-FileCopyrightText: 2006 Till Adam + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemhydratest.h" + +#include "item.h" +#include +#include +#include +#include +#include + +using namespace Akonadi; + +struct Volker { + bool operator==(const Volker &f) const + { + return f.who == who; + } + virtual ~Volker() + { + } + virtual Volker *clone() const = 0; + QString who; +}; +using VolkerPtr = std::shared_ptr; +using VolkerQPtr = QSharedPointer; + +struct Rudi : public Volker { + Rudi() + { + who = QStringLiteral("Rudi"); + } + ~Rudi() override + { + } + Rudi *clone() const override + { + return new Rudi(*this); + } +}; + +using RudiPtr = std::shared_ptr; +using RudiQPtr = QSharedPointer; + +struct Gerd : public Volker { + Gerd() + { + who = QStringLiteral("Gerd"); + } + Gerd *clone() const override + { + return new Gerd(*this); + } + + using SuperClass = Volker; +}; + +using GerdPtr = std::shared_ptr; +using GerdQPtr = QSharedPointer; + +Q_DECLARE_METATYPE(Volker *) +Q_DECLARE_METATYPE(Rudi *) +Q_DECLARE_METATYPE(Gerd *) + +Q_DECLARE_METATYPE(Rudi) +Q_DECLARE_METATYPE(Gerd) + +namespace Akonadi +{ +template<> struct SuperClass : public SuperClassTrait { +}; +} + +QTEST_MAIN(ItemHydra) + +ItemHydra::ItemHydra() +{ +} + +void ItemHydra::initTestCase() +{ +} + +void ItemHydra::testItemValuePayload() +{ + Item f; + Rudi rudi; + f.setPayload(rudi); + QVERIFY(f.hasPayload()); + + Item b; + Gerd gerd; + b.setPayload(gerd); + QVERIFY(b.hasPayload()); + + QCOMPARE(f.payload(), rudi); + QVERIFY(!(f.payload() == gerd)); + QCOMPARE(b.payload(), gerd); + QVERIFY(!(b.payload() == rudi)); +} + +void ItemHydra::testItemPointerPayload() +{ + Item f; + Rudi *rudi = new Rudi; + + // the below should not compile + // f.setPayload( rudi ); + + // f.setPayload( std::unique_ptr( rudi ) ); + // QVERIFY( f.hasPayload() ); + // QCOMPARE( f.payload< std::unique_ptr >()->who, rudi->who ); + + // below doesn't compile, hopefully + // QCOMPARE( f.payload< Rudi* >()->who, rudi->who ); + + delete rudi; +} + +void ItemHydra::testItemCopy() +{ + Item f; + Rudi rudi; + f.setPayload(rudi); + + Item r = f; + QCOMPARE(r.payload(), rudi); + + Item s; + s = f; + QVERIFY(s.hasPayload()); + QCOMPARE(s.payload(), rudi); +} + +void ItemHydra::testEmptyPayload() +{ + Item i1; + Item i2; + i1 = i2; // should not crash + + QVERIFY(!i1.hasPayload()); + QVERIFY(!i2.hasPayload()); + QVERIFY(!i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + + bool caughtException = false; + bool caughtRightException = true; + try { + Rudi r = i1.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + caughtRightException = true; + } catch (const Akonadi::Exception &e) { + qDebug() << "Caught Akonadi exception of type " << typeid(e).name() << ": " << e.what() << ", expected type" + << typeid(Akonadi::PayloadException).name(); + caughtException = true; + caughtRightException = false; + } catch (const std::exception &e) { + qDebug() << "Caught exception of type " << typeid(e).name() << ": " << e.what() << ", expected type" << typeid(Akonadi::PayloadException).name(); + caughtException = true; + caughtRightException = false; + } catch (...) { + qDebug() << "Caught unknown exception"; + caughtException = true; + caughtRightException = false; + } + QVERIFY(caughtException); + QVERIFY(caughtRightException); +} + +void ItemHydra::testPointerPayload() +{ + Rudi *r = new Rudi; + RudiPtr p(r); + std::weak_ptr w(p); + QCOMPARE(p.use_count(), (long)1); + + { + Item i1; + i1.setPayload(p); + QVERIFY(i1.hasPayload()); + QCOMPARE(p.use_count(), (long)2); + { + QVERIFY(i1.hasPayload()); + auto p2 = i1.payload(); + QCOMPARE(p.use_count(), (long)3); + } + + { + QVERIFY(i1.hasPayload()); + auto p2 = i1.payload(); + QCOMPARE(p.use_count(), (long)3); + } + + QCOMPARE(p.use_count(), (long)2); + } + QCOMPARE(p.use_count(), (long)1); + QCOMPARE(w.use_count(), (long)1); + p.reset(); + QCOMPARE(w.use_count(), (long)0); +} + +void ItemHydra::testPolymorphicPayloadWithTrait() +{ + VolkerPtr p(new Rudi); + + { + Item i1; + i1.setPayload(p); + QVERIFY(i1.hasPayload()); + QVERIFY(i1.hasPayload()); + QVERIFY(i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + QCOMPARE(p.use_count(), (long)2); + { + RudiPtr p2 = std::dynamic_pointer_cast(i1.payload()); + QCOMPARE(p.use_count(), (long)3); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + { + auto p2 = i1.payload(); + QCOMPARE(p.use_count(), (long)3); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + bool caughtException = false; + try { + auto p3 = i1.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + } + QVERIFY(caughtException); + + QCOMPARE(p.use_count(), (long)2); + } +} + +void ItemHydra::testPolymorphicPayloadWithTypedef() +{ + VolkerPtr p(new Gerd); + + { + Item i1; + i1.setPayload(p); + QVERIFY(i1.hasPayload()); + QVERIFY(i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + QVERIFY(i1.hasPayload()); + QCOMPARE(p.use_count(), (long)2); + { + auto p2 = std::dynamic_pointer_cast(i1.payload()); + QCOMPARE(p.use_count(), (long)3); + QCOMPARE(p2->who, QStringLiteral("Gerd")); + } + + { + auto p2 = i1.payload(); + QCOMPARE(p.use_count(), (long)3); + QCOMPARE(p2->who, QStringLiteral("Gerd")); + } + + bool caughtException = false; + try { + auto p3 = i1.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + } + QVERIFY(caughtException); + + QCOMPARE(p.use_count(), (long)2); + } +} + +void ItemHydra::testNullPointerPayload() +{ + RudiPtr p(static_cast(nullptr)); + Item i; + i.setPayload(p); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + // Fails, because GerdQPtr is QSharedPointer, while RudiPtr is std::shared_ptr + // and we cannot do sharedptr casting for null pointers + QVERIFY(!i.hasPayload()); + QCOMPARE(i.payload().get(), (Rudi *)nullptr); + QCOMPARE(i.payload().get(), (Volker *)nullptr); +} + +void ItemHydra::testQSharedPointerPayload() +{ + RudiQPtr p(new Rudi); + Item i; + i.setPayload(p); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + QVERIFY(i.hasPayload()); + QVERIFY(!i.hasPayload()); + + { + auto p2 = i.payload(); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + { + auto p2 = i.payload(); + QCOMPARE(p2->who, QStringLiteral("Rudi")); + } + + bool caughtException = false; + try { + auto p3 = i.payload(); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + caughtException = true; + } + QVERIFY(caughtException); +} + +void ItemHydra::testHasPayload() +{ + Item i1; + QVERIFY(!i1.hasPayload()); + QVERIFY(!i1.hasPayload()); + + Rudi r; + i1.setPayload(r); + QVERIFY(i1.hasPayload()); + QVERIFY(!i1.hasPayload()); +} + +void ItemHydra::testSharedPointerConversions() +{ + Item a; + RudiQPtr rudi(new Rudi); + a.setPayload(rudi); + // only the root base classes should show up with their metatype ids: + QVERIFY(a.availablePayloadMetaTypeIds().contains(qMetaTypeId())); + QVERIFY(a.hasPayload()); + QVERIFY(a.hasPayload()); + QVERIFY(a.hasPayload()); + QVERIFY(!a.hasPayload()); + QVERIFY(a.payload().get()); + QVERIFY(a.payload().get()); + bool thrown = false; + + bool thrownCorrectly = true; + try { + QVERIFY(!a.payload()); + } catch (const Akonadi::PayloadException &e) { + thrown = thrownCorrectly = true; + } catch (...) { + thrown = true; + thrownCorrectly = false; + } + QVERIFY(thrown); + QVERIFY(thrownCorrectly); +} + +void ItemHydra::testForeignPayload() +{ + QTemporaryFile file; + QVERIFY(file.open()); + file.write("123456789"); + file.close(); + + Item a(QStringLiteral("application/octet-stream")); + a.setPayloadPath(file.fileName()); + QVERIFY(a.hasPayload()); + QCOMPARE(a.payload(), QByteArray("123456789")); + + Item b(QStringLiteral("application/octet-stream")); + b.apply(a); + QVERIFY(b.hasPayload()); + QCOMPARE(b.payload(), QByteArray("123456789")); + QCOMPARE(b.payloadPath(), file.fileName()); + + Item c = b; + QVERIFY(c.hasPayload()); + QCOMPARE(c.payload(), QByteArray("123456789")); + QCOMPARE(c.payloadPath(), file.fileName()); +} diff --git a/autotests/libs/itemhydratest.h b/autotests/libs/itemhydratest.h new file mode 100644 index 0000000..cf94c49 --- /dev/null +++ b/autotests/libs/itemhydratest.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ItemHydra : public QObject +{ + Q_OBJECT +public: + ItemHydra(); + virtual ~ItemHydra() + { + } +private Q_SLOTS: + void initTestCase(); + void testItemValuePayload(); + void testItemPointerPayload(); + void testItemCopy(); + void testEmptyPayload(); + void testPointerPayload(); + void testPolymorphicPayloadWithTrait(); + void testPolymorphicPayloadWithTypedef(); + void testNullPointerPayload(); + void testQSharedPointerPayload(); + void testHasPayload(); + void testSharedPointerConversions(); + void testForeignPayload(); +}; + diff --git a/autotests/libs/itemmovetest.cpp b/autotests/libs/itemmovetest.cpp new file mode 100644 index 0000000..b4cc29a --- /dev/null +++ b/autotests/libs/itemmovetest.cpp @@ -0,0 +1,174 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include "collectionfetchscope.h" +#include "control.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmovejob.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" +#include "session.h" + +#include + +using namespace Akonadi; + +class ItemMoveTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + // TODO: test inter and intra resource moves + void testMove_data() + { + QTest::addColumn("items"); + QTest::addColumn("destination"); + QTest::addColumn("source"); + + Collection destination(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bar"))); + QVERIFY(destination.isValid()); + + QTest::newRow("intra-res single uid") << (Item::List() << Item(5)) << destination << Collection(); + + destination = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(destination.isValid()); + + QTest::newRow("inter-res single uid") << (Item::List() << Item(1)) << destination << Collection(); + QTest::newRow("inter-res two uid") << (Item::List() << Item(2) << Item(3)) << destination << Collection(); + Item r1; + r1.setRemoteId(QStringLiteral("D")); + Collection ridDest; + ridDest.setRemoteId(QStringLiteral("3")); + Collection ridSource; + ridSource.setRemoteId(QStringLiteral("10")); + QTest::newRow("intra-res single rid") << (Item::List() << r1) << ridDest << ridSource; + } + + void testMove() + { + QFETCH(Item::List, items); + QFETCH(Collection, destination); + QFETCH(Collection, source); + + Session monitorSession; + Monitor monitor(&monitorSession); + monitor.setObjectName(QStringLiteral("itemmovetest")); + monitor.setCollectionMonitored(Collection::root()); + monitor.fetchCollection(true); + monitor.itemFetchScope().setAncestorRetrieval(ItemFetchScope::Parent); + monitor.itemFetchScope().setFetchRemoteIdentification(true); + QSignalSpy moveSpy(&monitor, &Monitor::itemsMoved); + AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady); + + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); // for rid based moves + + auto prefetchjob = new ItemFetchJob(destination, this); + AKVERIFYEXEC(prefetchjob); + int baseline = prefetchjob->items().size(); + + auto move = new ItemMoveJob(items, source, destination, this); + AKVERIFYEXEC(move); + + auto fetch = new ItemFetchJob(destination, this); + fetch->fetchScope().setAncestorRetrieval(ItemFetchScope::Parent); + fetch->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), items.count() + baseline); + foreach (const Item &movedItem, fetch->items()) { + QVERIFY(movedItem.hasPayload()); + QVERIFY(!movedItem.payload().isEmpty()); + if (destination.id() >= 0) { + QCOMPARE(movedItem.parentCollection().id(), destination.id()); + } else { + QCOMPARE(movedItem.parentCollection().remoteId(), destination.remoteId()); + } + } + + QTRY_COMPARE(moveSpy.count(), 1); + const Akonadi::Item::List &ntfItems = moveSpy.takeFirst().at(0).value(); + QCOMPARE(ntfItems.size(), items.size()); + Q_FOREACH (const Item &ntfItem, ntfItems) { + if (destination.id() >= 0) { + QCOMPARE(ntfItem.parentCollection().id(), destination.id()); + } else { + QCOMPARE(ntfItem.parentCollection().remoteId(), destination.remoteId()); + } + } + } + + void testIllegalMove() + { + Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res2"))); + QVERIFY(col.isValid()); + + auto prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + QCOMPARE(prefetchjob->items().count(), 1); + Item item = prefetchjob->items()[0]; + + // move into invalid collection + auto store = new ItemMoveJob(item, Collection(INT_MAX), this); + QVERIFY(!store->exec()); + + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy itemMovedSpy(monitor.get(), &Monitor::itemsMoved); + + // move item into folder that doesn't support its mimetype + store = new ItemMoveJob(item, col, this); + QEXPECT_FAIL("", "Check not yet implemented by the server.", Continue); + QVERIFY(!store->exec()); + + // Wait for the notification so that it does not disturb the next test + QTRY_COMPARE(itemMovedSpy.count(), 1); + } + + void testMoveNotifications() + { + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy itemMovedSpy(monitor.get(), &Monitor::itemsMoved); + QSignalSpy itemAddedSpy(monitor.get(), &Monitor::itemAdded); + + Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + Item item(QStringLiteral("application/octet-stream")); + item.setFlags({"\\SEEN", "$ENCRYPTED"}); + item.setPayload(QByteArray("This is a test payload")); + item.setSize(34); + item.setParentCollection(col); + auto create = new ItemCreateJob(item, col, this); + AKVERIFYEXEC(create); + item = create->item(); + + QTRY_COMPARE(itemAddedSpy.size(), 1); + auto ntfItem = itemAddedSpy.at(0).at(0).value(); + QCOMPARE(ntfItem.id(), item.id()); + QCOMPARE(ntfItem.flags(), item.flags()); + + Collection dest(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bar"))); + auto move = new ItemMoveJob(item, dest, this); + AKVERIFYEXEC(move); + + QTRY_COMPARE(itemMovedSpy.size(), 1); + const auto ntfItems = itemMovedSpy.at(0).at(0).value(); + QCOMPARE(ntfItems.size(), 1); + ntfItem = ntfItems.at(0); + QCOMPARE(ntfItem.id(), item.id()); + QCOMPARE(ntfItem.flags(), item.flags()); + } +}; + +QTEST_AKONADIMAIN(ItemMoveTest) + +#include "itemmovetest.moc" diff --git a/autotests/libs/itemsearchjobtest.cpp b/autotests/libs/itemsearchjobtest.cpp new file mode 100644 index 0000000..d9e37e4 --- /dev/null +++ b/autotests/libs/itemsearchjobtest.cpp @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "qtest_akonadi.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" +#include "item.h" +#include "itemsearchjob.h" +#include "searchquery.h" + +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_METATYPE(Akonadi::SearchQuery) + +using namespace Akonadi; +class ItemSearchJobTest : public QObject +{ + Q_OBJECT +private: + Akonadi::SearchQuery createQuery(const QString &key, const QSet &resultSet) + { + Akonadi::SearchQuery query; + foreach (qint64 id, resultSet) { + query.addTerm(Akonadi::SearchTerm(key, id)); + } + return query; + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testItemSearch_data() + { + QTest::addColumn("remoteSearchEnabled"); + QTest::addColumn("query"); + QTest::addColumn>("resultSet"); + + { + QSet resultSet; + resultSet << 1 << 2 << 3; + QTest::newRow("plugin search") << false << createQuery(QStringLiteral("plugin"), resultSet) << resultSet; + } + { + QSet resultSet; + resultSet << 1 << 2 << 3; + QTest::newRow("resource search") << true << createQuery(QStringLiteral("resource"), resultSet) << resultSet; + } + { + QSet resultSet; + resultSet << 1 << 2 << 3 << 4; + Akonadi::SearchQuery query; + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 1)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 2)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 3)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 4)); + QTest::newRow("mixed search: results are merged") << true << query << resultSet; + } + } + + void testItemSearch() + { + QFETCH(bool, remoteSearchEnabled); + QFETCH(SearchQuery, query); + QFETCH(QSet, resultSet); + + ItemSearchJob *itemSearchJob = new ItemSearchJob(query, this); + itemSearchJob->setRemoteSearchEnabled(remoteSearchEnabled); + itemSearchJob->setSearchCollections(Collection::List() << Collection::root()); + itemSearchJob->setRecursive(true); + AKVERIFYEXEC(itemSearchJob); + QSet actualResultSet; + foreach (const Item &item, itemSearchJob->items()) { + actualResultSet << item.id(); + } + qDebug() << actualResultSet << resultSet; + QCOMPARE(actualResultSet, resultSet); + } +}; + +QTEST_AKONADIMAIN(ItemSearchJobTest) + +#include "itemsearchjobtest.moc" diff --git a/autotests/libs/itemserializertest.cpp b/autotests/libs/itemserializertest.cpp new file mode 100644 index 0000000..f154939 --- /dev/null +++ b/autotests/libs/itemserializertest.cpp @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemserializertest.h" + +#include "attributefactory.h" +#include "item.h" +#include "itemserializer_p.h" + +#include + +using namespace Akonadi; + +QTEST_MAIN(ItemSerializerTest) + +void ItemSerializerTest::testEmptyPayload() +{ + // should not crash + QByteArray data; + Item item; + ItemSerializer::deserialize(item, Item::FullPayload, data, 0, ItemSerializer::Internal); + QVERIFY(data.isEmpty()); +} + +void ItemSerializerTest::testDefaultSerializer_data() +{ + QTest::addColumn("serialized"); + + QTest::newRow("null") << QByteArray(); + QTest::newRow("empty") << QByteArray(""); + QTest::newRow("nullbytei") << QByteArray("\0", 1); + QTest::newRow("mixed") << QByteArray("\0\r\n\0bla", 7); +} + +void ItemSerializerTest::testDefaultSerializer() +{ + QFETCH(QByteArray, serialized); + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + ItemSerializer::deserialize(item, Item::FullPayload, serialized, 0, ItemSerializer::Internal); + + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), serialized); + + QByteArray data; + int version = 0; + ItemSerializer::serialize(item, Item::FullPayload, data, version); + QCOMPARE(data, serialized); + QEXPECT_FAIL("null", "Serializer cannot distinguish null vs. empty", Continue); + QCOMPARE(data.isNull(), serialized.isNull()); +} diff --git a/autotests/libs/itemserializertest.h b/autotests/libs/itemserializertest.h new file mode 100644 index 0000000..42416e6 --- /dev/null +++ b/autotests/libs/itemserializertest.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ItemSerializerTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testEmptyPayload(); + void testDefaultSerializer_data(); + void testDefaultSerializer(); +}; + diff --git a/autotests/libs/itemstoretest.cpp b/autotests/libs/itemstoretest.cpp new file mode 100644 index 0000000..a2e41b4 --- /dev/null +++ b/autotests/libs/itemstoretest.cpp @@ -0,0 +1,500 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + SPDX-FileCopyrightText: 2007 Robert Zwerus + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemstoretest.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "attributefactory.h" +#include "collectionfetchjob.h" +#include "control.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "itemmodifyjob_p.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" +#include "testattribute.h" + +using namespace Akonadi; + +QTEST_AKONADIMAIN(ItemStoreTest) + +static Collection res1_foo; +static Collection res2; +static Collection res3; + +void ItemStoreTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AttributeFactory::registerAttribute(); + + // get the collections we run the tests on + res1_foo = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(res1_foo.isValid()); + res2 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2"))); + QVERIFY(res2.isValid()); + res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + QVERIFY(res3.isValid()); + + AkonadiTest::setAllResourcesOffline(); +} + +void ItemStoreTest::testFlagChange() +{ + auto fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + Item item = fjob->items()[0]; + + // add a flag + Item::Flags origFlags = item.flags(); + Item::Flags expectedFlags = origFlags; + expectedFlags.insert("added_test_flag_1"); + item.setFlag("added_test_flag_1"); + auto sjob = new ItemModifyJob(item, this); + AKVERIFYEXEC(sjob); + + fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.flags().count(), expectedFlags.count()); + Item::Flags diff = expectedFlags - item.flags(); + QVERIFY(diff.isEmpty()); + + // set flags + expectedFlags.insert("added_test_flag_2"); + item.setFlags(expectedFlags); + sjob = new ItemModifyJob(item, this); + AKVERIFYEXEC(sjob); + + fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.flags().count(), expectedFlags.count()); + diff = expectedFlags - item.flags(); + QVERIFY(diff.isEmpty()); + + // remove a flag + item.clearFlag("added_test_flag_1"); + item.clearFlag("added_test_flag_2"); + sjob = new ItemModifyJob(item, this); + AKVERIFYEXEC(sjob); + + fjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.flags().count(), origFlags.count()); + diff = origFlags - item.flags(); + QVERIFY(diff.isEmpty()); +} + +void ItemStoreTest::testDataChange_data() +{ + QTest::addColumn("data"); + QTest::addColumn("expectedSize"); + + QTest::newRow("simple") << QByteArray("testbody") << 60LL; + QTest::newRow("null") << QByteArray() << 0LL; + QTest::newRow("empty") << QByteArray("") << 0LL; + QTest::newRow("nullbyte") << QByteArray("\0", 1) << 56LL; + QTest::newRow("nullbyte2") << QByteArray("\0X", 2) << 56LL; + QTest::newRow("linebreaks") << QByteArray("line1\nline2\n\rline3\rline4\r\n") << 80LL; + QTest::newRow("linebreaks2") << QByteArray("line1\r\nline2\r\n\r\n") << 68LL; + QTest::newRow("linebreaks3") << QByteArray("line1\nline2") << 64LL; + QByteArray b; + QTest::newRow("big") << b.fill('a', 1 << 20) << 280LL; + QTest::newRow("bignull") << b.fill('\0', 1 << 20) << 280LL; + QTest::newRow("bigcr") << b.fill('\r', 1 << 20) << 280LL; + QTest::newRow("biglf") << b.fill('\n', 1 << 20) << 280LL; +} + +void ItemStoreTest::testDataChange() +{ + QFETCH(QByteArray, data); + QFETCH(qint64, expectedSize); + + Item item; + auto prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + item = prefetchjob->items()[0]; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload(data); + QCOMPARE(item.payload(), data); + + // modify data + auto sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + auto fjob = new ItemFetchJob(Item(1)); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), data); + QEXPECT_FAIL("null", "STORE will not update item size on 0 sizes", Continue); + QEXPECT_FAIL("empty", "STORE will not update item size on 0 sizes", Continue); + // Cannot compare with data.size() due to payload compression + QCOMPARE(item.size(), expectedSize); +} + +void ItemStoreTest::testRemoteId_data() +{ + QTest::addColumn("rid"); + QTest::addColumn("exprid"); + + QTest::newRow("set") << QStringLiteral("A") << QStringLiteral("A"); + QTest::newRow("no-change") << QString() << QStringLiteral("A"); + QTest::newRow("clear") << QStringLiteral("") << QStringLiteral(""); + QTest::newRow("reset") << QStringLiteral("A") << QStringLiteral("A"); + QTest::newRow("utf8") << QStringLiteral("ä ö ü @") << QStringLiteral("ä ö ü @"); +} + +void ItemStoreTest::testRemoteId() +{ + QFETCH(QString, rid); + QFETCH(QString, exprid); + + // pretend to be a resource, we cannot change remote identifiers otherwise + auto rsel = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0"), this); + AKVERIFYEXEC(rsel); + + auto prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + Item item = prefetchjob->items()[0]; + + item.setRemoteId(rid); + auto store = new ItemModifyJob(item, this); + store->disableRevisionCheck(); + store->setIgnorePayload(true); // we only want to update the remote id + AKVERIFYEXEC(store); + + auto fetch = new ItemFetchJob(item, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 1); + item = fetch->items().at(0); + QEXPECT_FAIL("clear", "Clearing RID by clients is currently forbidden to avoid conflicts.", Continue); + QCOMPARE(item.remoteId().toUtf8(), exprid.toUtf8()); + + // no longer pretend to be a resource + rsel = new ResourceSelectJob(QString(), this); + AKVERIFYEXEC(rsel); +} + +void ItemStoreTest::testMultiPart() +{ + auto prefetchjob = new ItemFetchJob(Item(1)); + AKVERIFYEXEC(prefetchjob); + QCOMPARE(prefetchjob->items().count(), 1); + Item item = prefetchjob->items()[0]; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("testmailbody"); + item.attribute(Item::AddIfMissing)->data = "extra"; + + // store item + auto sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + auto fjob = new ItemFetchJob(Item(1)); + fjob->fetchScope().fetchAttribute(); + fjob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("testmailbody")); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, QByteArray("extra")); + + // clean up + item.removeAttribute("EXTRA"); + sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); +} + +void ItemStoreTest::testPartRemove() +{ + auto prefetchjob = new ItemFetchJob(Item(2)); + AKVERIFYEXEC(prefetchjob); + Item item = prefetchjob->items()[0]; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.attribute(Item::AddIfMissing)->data = "extra"; + + // store item + auto sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + // fetch item and its parts (should be RFC822, HEAD and EXTRA) + auto fjob = new ItemFetchJob(Item(2)); + fjob->fetchScope().fetchFullPayload(); + fjob->fetchScope().fetchAllAttributes(); + fjob->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fjob); + QCOMPARE(fjob->items().count(), 1); + item = fjob->items()[0]; + QCOMPARE(item.attributes().count(), 2); + QVERIFY(item.hasAttribute()); + + // remove a part + item.removeAttribute(); + sjob = new ItemModifyJob(item); + AKVERIFYEXEC(sjob); + + // fetch item again (should only have RFC822 and HEAD left) + auto fjob2 = new ItemFetchJob(Item(2)); + fjob2->fetchScope().fetchFullPayload(); + fjob2->fetchScope().fetchAllAttributes(); + fjob2->fetchScope().setCacheOnly(true); + AKVERIFYEXEC(fjob2); + QCOMPARE(fjob2->items().count(), 1); + item = fjob2->items()[0]; + QCOMPARE(item.attributes().count(), 1); + QVERIFY(!item.hasAttribute()); +} + +void ItemStoreTest::testRevisionCheck() +{ + // fetch same item twice + Item ref(2); + auto prefetchjob = new ItemFetchJob(ref); + AKVERIFYEXEC(prefetchjob); + QCOMPARE(prefetchjob->items().count(), 1); + Item item1 = prefetchjob->items()[0]; + Item item2 = prefetchjob->items()[0]; + + // store first item unmodified + auto sjob = new ItemModifyJob(item1); + AKVERIFYEXEC(sjob); + + // store the first item with modifications (should work) + item1.attribute(Item::AddIfMissing)->data = "random stuff 1"; + sjob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(sjob); + + // try to store second item with modifications (should be detected as a conflict) + item2.attribute(Item::AddIfMissing)->data = "random stuff 2"; + auto sjob2 = new ItemModifyJob(item2); + sjob2->disableAutomaticConflictHandling(); + QVERIFY(!sjob2->exec()); + + // fetch same again + prefetchjob = new ItemFetchJob(ref); + AKVERIFYEXEC(prefetchjob); + item1 = prefetchjob->items()[0]; + + // delete item + auto djob = new ItemDeleteJob(ref, this); + AKVERIFYEXEC(djob); + + // try to store it + sjob = new ItemModifyJob(item1); + QVERIFY(!sjob->exec()); +} + +void ItemStoreTest::testModificationTime() +{ + Item item; + item.setMimeType(QStringLiteral("text/directory")); + QVERIFY(item.modificationTime().isNull()); + + auto job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + + // The item should have a datetime set now. + item = job->item(); + QVERIFY(!item.modificationTime().isNull()); + QDateTime initialDateTime = item.modificationTime(); + + // Fetch the same item again. + Item item2(item.id()); + auto fjob = new ItemFetchJob(item2, this); + AKVERIFYEXEC(fjob); + item2 = fjob->items().first(); + QCOMPARE(initialDateTime, item2.modificationTime()); + + // Lets wait at least a second, which is the resolution of mtime + QTest::qWait(1000); + + // Modify the item + item.attribute(Item::AddIfMissing)->data = "extra"; + auto mjob = new ItemModifyJob(item); + AKVERIFYEXEC(mjob); + + // The item should still have a datetime set and that date should be somewhere + // after the initialDateTime. + item = mjob->item(); + QVERIFY(!item.modificationTime().isNull()); + QVERIFY(initialDateTime < item.modificationTime()); + + // Fetch the item after modification. + Item item3(item.id()); + auto fjob2 = new ItemFetchJob(item3, this); + AKVERIFYEXEC(fjob2); + + // item3 should have the same modification time as item. + item3 = fjob2->items().first(); + QCOMPARE(item3.modificationTime(), item.modificationTime()); + + // Clean up + auto idjob = new ItemDeleteJob(item, this); + AKVERIFYEXEC(idjob); +} + +void ItemStoreTest::testRemoteIdRace() +{ + // Create an item and store it + Item item; + item.setMimeType(QStringLiteral("text/directory")); + auto job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + + // Fetch the same item again. It should not have a remote Id yet, as the resource + // is offline. + // The remote id should be null, not only empty, so that item modify jobs with this + // item don't overwrite the remote id. + Item item2(job->item().id()); + auto fetchJob = new ItemFetchJob(item2); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + QVERIFY(fetchJob->items().first().remoteId().isEmpty()); +} + +void ItemStoreTest::itemModifyJobShouldOnlySendModifiedAttributes() +{ + // Given an item with an attribute (created on the server) + Item item; + item.setMimeType(QStringLiteral("text/directory")); + item.attribute(Item::AddIfMissing)->data = "initial"; + auto job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + item = job->item(); + QCOMPARE(item.attributes().count(), 1); + + // When one job modifies this attribute, and another one does an unrelated change + Item item1(item.id()); + item1.attribute(Item::AddIfMissing)->data = "modified"; + auto mjob = new ItemModifyJob(item1); + mjob->disableRevisionCheck(); + AKVERIFYEXEC(mjob); + + item.setFlag("added_test_flag_1"); + // this job shouldn't send the old attribute again + auto mjob2 = new ItemModifyJob(item); + mjob2->disableRevisionCheck(); + AKVERIFYEXEC(mjob2); + + // Then the item has the new value for the attribute (the other one didn't send the old attribute value) + { + auto fetchJob = new ItemFetchJob(Item(item.id())); + ItemFetchScope fetchScope; + fetchScope.fetchAllAttributes(true); + fetchJob->setFetchScope(fetchScope); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + const Item fetchedItem = fetchJob->items().first(); + QCOMPARE(fetchedItem.flags().count(), 1); + QCOMPARE(fetchedItem.attributes().count(), 1); + QCOMPARE(fetchedItem.attribute()->data, "modified"); + } +} + +class ParallelJobsRunner +{ +public: + explicit ParallelJobsRunner(int count) + : numSessions(count) + { + sessions.reserve(numSessions); + modifyJobs.reserve(numSessions); + for (int i = 0; i < numSessions; ++i) { + auto session = new Session(QByteArray::number(i)); + sessions.push_back(session); + } + } + + ~ParallelJobsRunner() + { + qDeleteAll(sessions); + } + + void addJob(ItemModifyJob *mjob) + { + modifyJobs.push_back(mjob); + QObject::connect(mjob, &KJob::result, [mjob, this]() { + if (mjob->error()) { + errors.append(mjob->errorString()); + } + doneJobs.push_back(mjob); + }); + } + + void waitForAllJobs() + { + for (int i = 0; i < modifyJobs.count(); ++i) { + ItemModifyJob *mjob = modifyJobs.at(i); + if (!doneJobs.contains(mjob)) { + QSignalSpy spy(mjob, &ItemModifyJob::result); + QVERIFY(spy.wait()); + if (mjob->error()) { + qWarning() << mjob->errorString(); + } + QCOMPARE(mjob->error(), KJob::NoError); + } + } + QVERIFY2(errors.isEmpty(), qPrintable(errors.join(QLatin1String("; ")))); + } + + const int numSessions; + std::vector sessions; + QVector modifyJobs, doneJobs; + QStringList errors; +}; + +void ItemStoreTest::testParallelJobsAddingAttributes() +{ + // Given an item (created on the server) + Item::Id itemId; + { + Item item; + item.setMimeType(QStringLiteral("text/directory")); + auto job = new ItemCreateJob(item, res1_foo); + AKVERIFYEXEC(job); + itemId = job->item().id(); + QVERIFY(itemId >= 0); + } + + // When adding N attributes from N different sessions (e.g. threads or processes) + ParallelJobsRunner runner(10); + for (int i = 0; i < runner.numSessions; ++i) { + Item item(itemId); + Attribute *attr = AttributeFactory::createAttribute("type" + QByteArray::number(i)); + QVERIFY(attr); + attr->deserialize("attr" + QByteArray::number(i)); + item.addAttribute(attr); + auto mjob = new ItemModifyJob(item, runner.sessions.at(i)); + runner.addJob(mjob); + } + runner.waitForAllJobs(); + + // Then the item should have all attributes + auto fetchJob = new ItemFetchJob(Item(itemId)); + ItemFetchScope fetchScope; + fetchScope.fetchAllAttributes(true); + fetchJob->setFetchScope(fetchScope); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + const Item fetchedItem = fetchJob->items().first(); + QCOMPARE(fetchedItem.attributes().count(), runner.numSessions); +} diff --git a/autotests/libs/itemstoretest.h b/autotests/libs/itemstoretest.h new file mode 100644 index 0000000..cb16043 --- /dev/null +++ b/autotests/libs/itemstoretest.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + SPDX-FileCopyrightText: 2007 Robert Zwerus + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ItemStoreTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testFlagChange(); + void testDataChange_data(); + void testDataChange(); + void testRemoteId_data(); + void testRemoteId(); + void testMultiPart(); + void testPartRemove(); + void testRevisionCheck(); + void testModificationTime(); + void testRemoteIdRace(); + void itemModifyJobShouldOnlySendModifiedAttributes(); + void testParallelJobsAddingAttributes(); +}; + diff --git a/autotests/libs/itemsynctest.cpp b/autotests/libs/itemsynctest.cpp new file mode 100644 index 0000000..f513767 --- /dev/null +++ b/autotests/libs/itemsynctest.cpp @@ -0,0 +1,696 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemsync.h" +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" +#include "control.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" + +#include + +#include +#include + +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(KJob *) +Q_DECLARE_METATYPE(ItemSync::TransactionMode) + +class ItemsyncTest : public QObject +{ + Q_OBJECT +private: + Item::List fetchItems(const Collection &col) + { + qDebug() << "fetching items from collection" << col.remoteId() << col.name(); + auto fetch = new ItemFetchJob(col, this); + fetch->fetchScope().fetchFullPayload(); + fetch->fetchScope().fetchAllAttributes(); + fetch->fetchScope().setCacheOnly(true); // resources are switched off anyway + if (!fetch->exec()) { + []() { + QFAIL("Failed to fetch items!"); + }(); + } + return fetch->items(); + } + + static void createItems(const Collection &col, int itemCount) + { + for (int i = 0; i < itemCount; ++i) { + Item item(QStringLiteral("application/octet-stream")); + item.setRemoteId(QStringLiteral("rid") + QString::number(i)); + item.setGid(QStringLiteral("gid") + QString::number(i)); + item.setPayload("payload1"); + auto job = new ItemCreateJob(item, col); + AKVERIFYEXEC(job); + } + } + + static Item duplicateItem(const Item &item, const Collection &col) + { + Item duplicate = item; + duplicate.setId(-1); + auto job = new ItemCreateJob(duplicate, col); + [job]() { + AKVERIFYEXEC(job); + }(); + return job->item(); + } + + static Item modifyItem(Item item) + { + static int counter = 0; + item.setFlag(QByteArray("\\READ") + QByteArray::number(counter)); + counter++; + return item; + } + + std::unique_ptr createCollectionMonitor(const Collection &col) + { + auto monitor = std::make_unique(); + monitor->setCollectionMonitored(col); + if (!AkonadiTest::akWaitForSignal(monitor.get(), &Monitor::monitorReady)) { + QTest::qFail("Failed to wait for monitor", __FILE__, __LINE__); + return nullptr; + } + + return monitor; + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + qRegisterMetaType(); + qRegisterMetaType(); + } + + void testFullSync() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + // Since the item sync affects the knut resource we ensure we actually managed to load all items + // This needs to be adjusted should the testdataset change + QCOMPARE(origItems.size(), 15); + + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(col); + QSignalSpy deletedSpy(&monitor, &Monitor::itemRemoved); + QVERIFY(deletedSpy.isValid()); + QSignalSpy addedSpy(&monitor, &Monitor::itemAdded); + QVERIFY(addedSpy.isValid()); + QSignalSpy changedSpy(&monitor, &Monitor::itemChanged); + QVERIFY(changedSpy.isValid()); + + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setFullSyncItems(origItems); + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QCOMPARE(changedSpy.count(), 0); + } + + void testFullStreamingSync_data() + { + QTest::addColumn("transactionMode"); + QTest::addColumn("goToEventLoopAfterAddingItems"); + + QTest::newRow("single transaction, no eventloop") << ItemSync::SingleTransaction << false; + QTest::newRow("multi transaction, no eventloop") << ItemSync::MultipleTransactions << false; + QTest::newRow("single transaction, with eventloop") << ItemSync::SingleTransaction << true; + QTest::newRow("multi transaction, with eventloop") << ItemSync::MultipleTransactions << true; + } + + void testFullStreamingSync() + { + QFETCH(ItemSync::TransactionMode, transactionMode); + QFETCH(bool, goToEventLoopAfterAddingItems); + + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + QCOMPARE(origItems.size(), 15); + + auto monitor = createCollectionMonitor(col); + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + auto syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setTransactionMode(transactionMode); + syncer->setBatchSize(1); + syncer->setAutoDelete(false); + syncer->setStreamingEnabled(true); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setTotalItems(origItems.count()); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origItems.count(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setFullSyncItems(l); + if (goToEventLoopAfterAddingItems) { + QTest::qWait(0); + } + if (i < origItems.count() - 1) { + QCOMPARE(spy.count(), 0); + } + } + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->error(), 0); + if (transactionMode == ItemSync::SingleTransaction) { + QCOMPARE(transactionSpy.count(), 1); + } + if (transactionMode == ItemSync::MultipleTransactions) { + QCOMPARE(transactionSpy.count(), origItems.count()); + } + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + delete syncer; + QTest::qWait(100); + QTRY_COMPARE(deletedSpy.count(), 0); + QTRY_COMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), origItems.count()); + } + + void testIncrementalSync() + { + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + QCOMPARE(origItems.size(), 15); + + auto monitor = createCollectionMonitor(col); + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + { + auto syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setTransactionMode(ItemSync::SingleTransaction); + syncer->setIncrementalSyncItems(origItems, Item::List()); + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + } + + QTest::qWait(100); + QTRY_COMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), 0); + deletedSpy.clear(); + addedSpy.clear(); + changedSpy.clear(); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + Item::List delItems; + delItems << resultItems.takeFirst(); + + Item itemWithOnlyRemoteId; + itemWithOnlyRemoteId.setRemoteId(resultItems.front().remoteId()); + delItems << itemWithOnlyRemoteId; + resultItems.takeFirst(); + + // This item will not be removed since it isn't existing locally + Item itemWithRandomRemoteId; + itemWithRandomRemoteId.setRemoteId(KRandom::randomString(100)); + delItems << itemWithRandomRemoteId; + + { + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setIncrementalSyncItems(resultItems, delItems); + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + } + + Item::List resultItems2 = fetchItems(col); + QCOMPARE(resultItems2.count(), resultItems.count()); + + QTest::qWait(100); + QTRY_COMPARE(deletedSpy.count(), 2); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), 0); + + { + auto select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } + } + + void testIncrementalStreamingSync() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + auto monitor = createCollectionMonitor(col); + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setAutoDelete(false); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < origItems.count(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + KJob *job = spy.at(0).at(0).value(); + QCOMPARE(job, syncer); + QCOMPARE(job->error(), 0); + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + delete syncer; + + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), origItems.size()); + } + + void testEmptyIncrementalSync() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + auto monitor = createCollectionMonitor(col); + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setIncrementalSyncItems(Item::List(), Item::List()); + AKVERIFYEXEC(syncer); + // It would be better if we didn't have a transaction at all, but so far the transaction is still created + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QCOMPARE(changedSpy.count(), 0); + } + + void testIncrementalStreamingSyncBatchProcessing() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + auto monitor = createCollectionMonitor(col); + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + auto syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < syncer->batchSize(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < (syncer->batchSize() - 1)) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + QTest::qWait(100); + // this should process one batch of batchSize() items + QTRY_COMPARE(changedSpy.count(), syncer->batchSize()); + QCOMPARE(transactionSpy.count(), 1); // one per batch + + for (int i = syncer->batchSize(); i < origItems.count(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + QCOMPARE(transactionSpy.count(), 2); // one per batch + QTest::qWait(100); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QTRY_COMPARE(changedSpy.count(), resultItems.count()); + } + + void testGidMerge() + { + Collection col(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + { + Item item(QStringLiteral("application/octet-stream")); + item.setRemoteId(QStringLiteral("rid1")); + item.setGid(QStringLiteral("gid1")); + item.setPayload("payload1"); + auto job = new ItemCreateJob(item, col); + AKVERIFYEXEC(job); + } + { + Item item(QStringLiteral("application/octet-stream")); + item.setRemoteId(QStringLiteral("rid2")); + item.setGid(QStringLiteral("gid2")); + item.setPayload("payload1"); + auto job = new ItemCreateJob(item, col); + AKVERIFYEXEC(job); + } + Item modifiedItem(QStringLiteral("application/octet-stream")); + modifiedItem.setRemoteId(QStringLiteral("rid3")); + modifiedItem.setGid(QStringLiteral("gid2")); + modifiedItem.setPayload("payload2"); + + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + syncer->setIncrementalSyncItems(Item::List() << modifiedItem, Item::List()); + AKVERIFYEXEC(syncer); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), 3); + + Item item; + item.setGid(QStringLiteral("gid2")); + auto fetchJob = new ItemFetchJob(item); + fetchJob->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 2); + QCOMPARE(fetchJob->items().first().payload(), QByteArray("payload2")); + QCOMPARE(fetchJob->items().first().remoteId(), QString::fromLatin1("rid3")); + QCOMPARE(fetchJob->items().at(1).payload(), QByteArray("payload1")); + QCOMPARE(fetchJob->items().at(1).remoteId(), QStringLiteral("rid2")); + } + + /* + * This test verifies that ItemSync doesn't prematurely emit its result if a job inside a transaction fails. + * ItemSync is supposed to continue the sync but simply ignoring all delivered data. + */ + void testFailingJob() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + auto syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < syncer->batchSize(); ++i) { + Item::List l; + // Modify to trigger a changed signal + Item item = modifyItem(origItems[i]); + // item.setRemoteId(QByteArray("foo")); + item.setRemoteId(QString()); + item.setId(-1); + l << item; + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < (syncer->batchSize() - 1)) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + QTest::qWait(100); + QTRY_COMPARE(spy.count(), 0); + + for (int i = syncer->batchSize(); i < origItems.count(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + } + + /* + * This test verifies that ItemSync doesn't prematurely emit its result if a job inside a transaction fails, due to a duplicate. + * This case used to break the TransactionSequence. + * ItemSync is supposed to continue the sync but simply ignoring all delivered data. + */ + void testFailingDueToDuplicateItem() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + + // Create a duplicate that will trigger an error during the first batch + Item dupe = duplicateItem(origItems.at(0), col); + origItems = fetchItems(col); + + auto syncer = new ItemSync(col); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + QSignalSpy spy(syncer, &KJob::result); + QVERIFY(spy.isValid()); + syncer->setStreamingEnabled(true); + syncer->setTransactionMode(ItemSync::MultipleTransactions); + QTest::qWait(0); + QCOMPARE(spy.count(), 0); + + for (int i = 0; i < syncer->batchSize(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < (syncer->batchSize() - 1)) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + QTest::qWait(100); + // Ensure the job hasn't finished yet due to the errors + QTRY_COMPARE(spy.count(), 0); + + for (int i = syncer->batchSize(); i < origItems.count(); ++i) { + Item::List l; + // Modify to trigger a changed signal + l << modifyItem(origItems[i]); + syncer->setIncrementalSyncItems(l, Item::List()); + if (i < origItems.count() - 1) { + QTest::qWait(0); // enter the event loop so itemsync actually can do something + } + QCOMPARE(spy.count(), 0); + } + + syncer->deliveryDone(); + QTRY_COMPARE(spy.count(), 1); + + // cleanup + auto del = new ItemDeleteJob(dupe, this); + AKVERIFYEXEC(del); + } + + void testFullSyncFailingDueToDuplicateItem() + { + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + QVERIFY(col.isValid()); + Item::List origItems = fetchItems(col); + // Create a duplicate that will trigger an error during the first batch + Item dupe = duplicateItem(origItems.at(0), col); + origItems = fetchItems(col); + + auto monitor = createCollectionMonitor(col); + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setFullSyncItems(origItems); + QVERIFY(!syncer->exec()); + QCOMPARE(transactionSpy.count(), 1); + + Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + QTest::qWait(100); + // QCOMPARE(deletedSpy.count(), 1); // ## is this correct? + // QCOMPARE(addedSpy.count(), 1); // ## is this correct? + QCOMPARE(changedSpy.count(), 0); + + // cleanup + auto del = new ItemDeleteJob(dupe, this); + AKVERIFYEXEC(del); + } + + void testFullSyncManyItems() + { + // Given a collection with 1000 items + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/foo2"))); + QVERIFY(col.isValid()); + + auto monitor = createCollectionMonitor(col); + QSignalSpy addedSpy(monitor.get(), &Monitor::itemAdded); + + const int itemCount = 1000; + createItems(col, itemCount); + QTRY_COMPARE(addedSpy.count(), itemCount); + addedSpy.clear(); + + const Item::List origItems = fetchItems(col); + QCOMPARE(origItems.size(), itemCount); + + QSignalSpy deletedSpy(monitor.get(), &Monitor::itemRemoved); + QSignalSpy changedSpy(monitor.get(), &Monitor::itemChanged); + + QBENCHMARK { + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + QSignalSpy transactionSpy(syncer, &ItemSync::transactionCommitted); + QVERIFY(transactionSpy.isValid()); + syncer->setFullSyncItems(origItems); + + AKVERIFYEXEC(syncer); + QCOMPARE(transactionSpy.count(), 1); + } + + const Item::List resultItems = fetchItems(col); + QCOMPARE(resultItems.count(), origItems.count()); + QTest::qWait(100); + QCOMPARE(deletedSpy.count(), 0); + QCOMPARE(addedSpy.count(), 0); + QCOMPARE(changedSpy.count(), 0); + + // delete all items; QBENCHMARK leads to the whole method being called more than once + auto job = new ItemDeleteJob(resultItems); + AKVERIFYEXEC(job); + } + + void testUserCancel() + { + // Given a collection with 100 items + const Collection col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/foo2"))); + QVERIFY(col.isValid()); + + const Item::List itemsToDelete = fetchItems(col); + if (!itemsToDelete.isEmpty()) { + auto deleteJob = new ItemDeleteJob(itemsToDelete); + AKVERIFYEXEC(deleteJob); + } + + const int itemCount = 100; + createItems(col, itemCount); + const Item::List origItems = fetchItems(col); + QCOMPARE(origItems.size(), itemCount); + + // and an ItemSync running + auto syncer = new ItemSync(col); + syncer->setTransactionMode(ItemSync::SingleTransaction); + syncer->setFullSyncItems(origItems); + + // When the user cancels the ItemSync + QTimer::singleShot(10, syncer, &ItemSync::rollback); + + // Then the itemsync should finish at some point, and not crash + QVERIFY(!syncer->exec()); + QCOMPARE(syncer->errorString(), QStringLiteral("User canceled operation.")); + + // Cleanup + auto job = new ItemDeleteJob(origItems); + AKVERIFYEXEC(job); + } +}; + +QTEST_AKONADIMAIN(ItemsyncTest) + +#include "itemsynctest.moc" diff --git a/autotests/libs/itemtest.cpp b/autotests/libs/itemtest.cpp new file mode 100644 index 0000000..34bcfc9 --- /dev/null +++ b/autotests/libs/itemtest.cpp @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemtest.h" +#include "collection.h" +#include "item.h" +#include "testattribute.h" + +#include + +#include + +QTEST_MAIN(ItemTest) + +using namespace Akonadi; + +void ItemTest::testMultipart() +{ + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + + QSet parts; + QCOMPARE(item.loadedPayloadParts(), parts); + + QByteArray bodyData = "bodydata"; + item.setPayload(bodyData); + parts << Item::FullPayload; + QCOMPARE(item.loadedPayloadParts(), parts); + QCOMPARE(item.payload(), bodyData); + + QByteArray myData = "mypartdata"; + item.attribute(Item::AddIfMissing)->data = myData; + + QCOMPARE(item.loadedPayloadParts(), parts); + QCOMPARE(item.attributes().count(), 1); + QVERIFY(item.hasAttribute()); + QCOMPARE(item.attribute()->data, myData); +} + +void ItemTest::testInheritance() +{ + Item a; + + a.setRemoteId(QStringLiteral("Hello World")); + a.setSize(10); + + Item b(a); + b.setFlag("\\send"); + QCOMPARE(b.remoteId(), QStringLiteral("Hello World")); + QCOMPARE(b.size(), (qint64)10); +} + +void ItemTest::testParentCollection() +{ + Item a; + QVERIFY(!a.parentCollection().isValid()); + + a.setParentCollection(Collection::root()); + QCOMPARE(a.parentCollection(), Collection::root()); + Item b = a; + QCOMPARE(b.parentCollection(), Collection::root()); + + Item c; + c.parentCollection().setRemoteId(QStringLiteral("foo")); + QCOMPARE(c.parentCollection().remoteId(), QStringLiteral("foo")); + const Item d = c; + QCOMPARE(d.parentCollection().remoteId(), QStringLiteral("foo")); + + const Item e; + QVERIFY(!e.parentCollection().isValid()); + + Collection col(5); + Item f; + f.setParentCollection(col); + QCOMPARE(f.parentCollection(), col); + Item g = f; + QCOMPARE(g.parentCollection(), col); + b = g; + QCOMPARE(b.parentCollection(), col); +} + +void ItemTest::testComparison_data() +{ + QTest::addColumn("itemA"); + QTest::addColumn("itemB"); + QTest::addColumn("match"); + + QTest::newRow("both invalid, same invalid IDs") << Item(-10) << Item(-10) << true; + QTest::newRow("both invalid, different invalid IDs") << Item(-11) << Item(-12) << true; + QTest::newRow("one valid") << Item(1) << Item() << false; + QTest::newRow("both valid, same IDs") << Item(2) << Item(2) << true; + QTest::newRow("both valid, different IDs") << Item(3) << Item(4) << false; +} + +void ItemTest::testComparison() +{ + QFETCH(Akonadi::Item, itemA); + QFETCH(Akonadi::Item, itemB); + QFETCH(bool, match); + + if (match) { + QVERIFY(itemA == itemB); + QVERIFY(!(itemA != itemB)); + } else { + QVERIFY(itemA != itemB); + QVERIFY(!(itemA == itemB)); + } +} diff --git a/autotests/libs/itemtest.h b/autotests/libs/itemtest.h new file mode 100644 index 0000000..dc1f824 --- /dev/null +++ b/autotests/libs/itemtest.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ItemTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testMultipart(); + void testInheritance(); + void testParentCollection(); + + void testComparison_data(); + void testComparison(); +}; + diff --git a/autotests/libs/jobtest.cpp b/autotests/libs/jobtest.cpp new file mode 100644 index 0000000..ff9bb0f --- /dev/null +++ b/autotests/libs/jobtest.cpp @@ -0,0 +1,236 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "fakesession.h" +#include "job.h" + +Q_DECLARE_METATYPE(KJob *) +Q_DECLARE_METATYPE(Akonadi::Job *) + +using namespace Akonadi; + +class FakeJob : public Job +{ + Q_OBJECT +public: + explicit FakeJob(QObject *parent = nullptr) + : Job(parent) + { + } + void done() + { + emitResult(); + } + +protected: + void doStart() override + { + emitWriteFinished(); + } +}; + +class JobTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + qRegisterMetaType(); + qRegisterMetaType(); + } + + void testTopLevelJobExecution() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, &FakeSession::jobAdded); + QVERIFY(sessionQueueSpy.isValid()); + + auto job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, &KJob::result); + QVERIFY(job1DoneSpy.isValid()); + + auto job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, &KJob::result); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QCOMPARE(job1DoneSpy.size(), 0); + + QSignalSpy job1AboutToStartSpy(job1, &Job::aboutToStart); + QVERIFY(job1AboutToStartSpy.wait()); + + QCOMPARE(job1DoneSpy.size(), 0); + + job1->done(); + QCOMPARE(job1DoneSpy.size(), 1); + + QSignalSpy job2AboutToStartSpy(job2, &Job::aboutToStart); + QVERIFY(job2AboutToStartSpy.wait()); + QCOMPARE(job2DoneSpy.size(), 0); + job2->done(); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + } + + void testKillSession() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, &FakeSession::jobAdded); + QVERIFY(sessionQueueSpy.isValid()); + QSignalSpy sessionReconnectSpy(&session, &Session::reconnected); + QVERIFY(sessionReconnectSpy.isValid()); + + auto job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, &KJob::result); + QVERIFY(job1DoneSpy.isValid()); + + auto job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, &KJob::result); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QSignalSpy job1AboutToStartSpy(job1, &Job::aboutToStart); + QVERIFY(job1AboutToStartSpy.wait()); + + // one job running, one queued, now kill the session + session.clear(); + QVERIFY(sessionReconnectSpy.wait()); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + QCOMPARE(sessionReconnectSpy.size(), 2); + } + + void testKillQueuedJob() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, &FakeSession::jobAdded); + QVERIFY(sessionQueueSpy.isValid()); + QSignalSpy sessionReconnectSpy(&session, &Session::reconnected); + QVERIFY(sessionReconnectSpy.isValid()); + + auto job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, &KJob::result); + QVERIFY(job1DoneSpy.isValid()); + + auto job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, &KJob::result); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QSignalSpy job1AboutToStartSpy(job1, &Job::aboutToStart); + QVERIFY(job1AboutToStartSpy.wait()); + + // one job running, one queued, now kill the waiting job + QVERIFY(job2->kill(KJob::EmitResult)); + + QCOMPARE(job1DoneSpy.size(), 0); + QCOMPARE(job2DoneSpy.size(), 1); + + job1->done(); + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + QCOMPARE(sessionReconnectSpy.size(), 1); + } + + void testKillRunningJob() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, &FakeSession::jobAdded); + QVERIFY(sessionQueueSpy.isValid()); + QSignalSpy sessionReconnectSpy(&session, &Session::reconnected); + QVERIFY(sessionReconnectSpy.isValid()); + + auto job1 = new FakeJob(&session); + QSignalSpy job1DoneSpy(job1, &KJob::result); + QVERIFY(job1DoneSpy.isValid()); + + auto job2 = new FakeJob(&session); + QSignalSpy job2DoneSpy(job2, &KJob::result); + QVERIFY(job2DoneSpy.isValid()); + + QCOMPARE(sessionQueueSpy.size(), 2); + QSignalSpy job1AboutToStartSpy(job1, &Job::aboutToStart); + QVERIFY(job1AboutToStartSpy.wait()); + + // one job running, one queued, now kill the running one + QVERIFY(job1->kill(KJob::EmitResult)); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 0); + + // session needs to reconnect, then execute the next job + QSignalSpy job2AboutToStartSpy(job2, &Job::aboutToStart); + QVERIFY(job2AboutToStartSpy.wait()); + QCOMPARE(sessionReconnectSpy.size(), 2); + job2->done(); + + QCOMPARE(job1DoneSpy.size(), 1); + QCOMPARE(job2DoneSpy.size(), 1); + QCOMPARE(sessionReconnectSpy.size(), 2); + } + + void testKillRunningSubjob() + { + FakeSession session("fakeSession", FakeSession::EndJobsManually); + + QSignalSpy sessionQueueSpy(&session, &FakeSession::jobAdded); + QSignalSpy sessionReconnectSpy(&session, &Session::reconnected); + + auto parentJob = new FakeJob(&session); + parentJob->setObjectName(QStringLiteral("parentJob")); + QSignalSpy parentJobDoneSpy(parentJob, &KJob::result); + + auto subjob = new FakeJob(parentJob); + subjob->setObjectName(QStringLiteral("subjob")); + QSignalSpy subjobDoneSpy(subjob, &KJob::result); + + auto subjob2 = new FakeJob(parentJob); + subjob2->setObjectName(QStringLiteral("subjob2")); + QSignalSpy subjob2DoneSpy(subjob2, &KJob::result); + + auto nextJob = new FakeJob(&session); + nextJob->setObjectName(QStringLiteral("nextJob")); + QSignalSpy nextJobDoneSpy(nextJob, &KJob::result); + + QCOMPARE(sessionQueueSpy.size(), 2); + QSignalSpy parentJobAboutToStart(parentJob, &Job::aboutToStart); + QVERIFY(parentJobAboutToStart.wait()); + + QSignalSpy subjobAboutToStart(subjob, &Job::aboutToStart); + QVERIFY(subjobAboutToStart.wait()); + + // one parent job, one subjob running (another one waiting), now kill the running subjob + QVERIFY(subjob->kill(KJob::EmitResult)); + + QCOMPARE(subjobDoneSpy.size(), 1); + QCOMPARE(subjob2DoneSpy.size(), 0); + + // Note that killing a subjob aborts the whole parent job + // Since the session only knows about the parent + QCOMPARE(parentJobDoneSpy.size(), 1); + + // session needs to reconnect, then execute the next job + QSignalSpy nextJobAboutToStartSpy(nextJob, &Job::aboutToStart); + QVERIFY(nextJobAboutToStartSpy.wait()); + QCOMPARE(sessionReconnectSpy.size(), 2); + nextJob->done(); + + QCOMPARE(subjob2DoneSpy.size(), 0); + QCOMPARE(nextJobDoneSpy.size(), 1); + } +}; + +QTEST_AKONADIMAIN(JobTest) + +#include "jobtest.moc" diff --git a/autotests/libs/lazypopulationtest.cpp b/autotests/libs/lazypopulationtest.cpp new file mode 100644 index 0000000..ecae5fc --- /dev/null +++ b/autotests/libs/lazypopulationtest.cpp @@ -0,0 +1,358 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "changerecorder_p.h" +#include "collectioncreatejob.h" +#include "control.h" +#include "entitytreemodel.h" +#include "entitytreemodel_p.h" +#include "itemcreatejob.h" +#include "monitor_p.h" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class InspectableETM : public EntityTreeModel +{ +public: + explicit InspectableETM(ChangeRecorder *monitor, QObject *parent = nullptr) + : EntityTreeModel(monitor, parent) + { + } + EntityTreeModelPrivate *etmPrivate() + { + return d_ptr; + } +}; + +/** + * This is a test for the LazyPopulation of the ETM and the associated refcounting in the Monitor. + */ +class LazyPopulationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + /** + * Test a complete scenario that checks: + * * loading on referencing + * * buffering after referencing + * * purging after the collection leaves the buffer + * * not-fetching when a collection is not buffered and not referenced + * * reloading after a collection becomes referenced again + */ + void testItemAdded(); + /* + * Test what happens if we + * * Create an item + * * Reference before item added signal arrives + * * Try fetching rest of items + */ + void testItemAddedBeforeFetch(); + + /* + * We purge an empty collection and make sure it can be fetched later on. + * * reference collection to remember empty status + * * purge collection + * * add item (it should not be added since not monitored) + * * reference collection and make sure items are added + */ + void testPurgeEmptyCollection(); + +private: + Collection res3; + static const int numberOfRootCollections = 4; + static const int bufferSize; +}; + +const int LazyPopulationTest::bufferSize = MonitorPrivate::PurgeBuffer::buffersize(); + +void LazyPopulationTest::initTestCase() +{ + qRegisterMetaType("Akonadi::Collection::Id"); + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + + res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + + // Set up a bunch of collections that we can select to purge a collection from the buffer + + // Number of buffered collections in the monitor + const int bufferSize = MonitorPrivate::PurgeBuffer::buffersize(); + for (int i = 0; i < bufferSize; i++) { + Collection col; + col.setParentCollection(res3); + col.setName(QStringLiteral("col%1").arg(i)); + auto create = new CollectionCreateJob(col, this); + AKVERIFYEXEC(create); + } +} + +QModelIndex getIndex(const QString &string, EntityTreeModel *model) +{ + QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, string, 1, Qt::MatchRecursive); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); +} + +/** + * Since we have no sensible way to figure out if the model is fully populated, + * we use the brute force approach. + */ +bool waitForPopulation(const QModelIndex &idx, EntityTreeModel *model, int count) +{ + for (int i = 0; i < 500; i++) { + if (model->rowCount(idx) >= count) { + return true; + } + QTest::qWait(10); + } + return false; +} + +void referenceCollection(EntityTreeModel *model, int index) +{ + QModelIndex idx = getIndex(QStringLiteral("col%1").arg(index), model); + QVERIFY(idx.isValid()); + model->setData(idx, QVariant(), EntityTreeModel::CollectionRefRole); + model->setData(idx, QVariant(), EntityTreeModel::CollectionDerefRole); +} + +void referenceCollections(EntityTreeModel *model, int count) +{ + for (int i = 0; i < count; i++) { + referenceCollection(model, i); + } +} + +void LazyPopulationTest::testItemAdded() +{ + const int bufferSize = MonitorPrivate::PurgeBuffer::buffersize(); + const QString mainCollectionName(QStringLiteral("main")); + Collection monitorCol; + { + monitorCol.setParentCollection(res3); + monitorCol.setName(mainCollectionName); + auto create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, monitorCol, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + QVERIFY(AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady)); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + // Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(waitForPopulation(res3Index, model, bufferSize + 1)); + + QModelIndex monitorIndex = getIndex(mainCollectionName, model); + QVERIFY(monitorIndex.isValid()); + + // Start + + //---Check that the item is present after the collection was referenced + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + model->fetchMore(monitorIndex); + // Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 1)); + + //---ensure we cannot fetchMore again + QVERIFY(!model->etmPrivate()->canFetchMore(monitorIndex)); + + // The item should now be present + QCOMPARE(model->index(0, 0, monitorIndex).data(Akonadi::EntityTreeModel::ItemIdRole).value(), item1.id()); + + //---ensure item1 is still available after no longer being referenced due to buffering + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + QCOMPARE(model->index(0, 0, monitorIndex).data(Akonadi::EntityTreeModel::ItemIdRole).value(), item1.id()); + + //---ensure item1 gets purged after the collection is no longer buffered + // Get the monitorCol out of the buffer + referenceCollections(model, bufferSize - 1); + // The collection is still in the buffer... + QCOMPARE(model->rowCount(monitorIndex), 1); + referenceCollection(model, bufferSize - 1); + //...and now purged + QCOMPARE(model->rowCount(monitorIndex), 0); + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + + //---ensure item2 added to unbuffered and unreferenced collection is not added to the model + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item2, monitorCol, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + QCOMPARE(model->rowCount(monitorIndex), 0); + + //---ensure all items are loaded after re-referencing the collection + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + model->fetchMore(monitorIndex); + // Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 2)); + QCOMPARE(model->rowCount(monitorIndex), 2); + + QVERIFY(!model->etmPrivate()->canFetchMore(monitorIndex)); + + // purge collection again + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + referenceCollections(model, bufferSize); + QCOMPARE(model->rowCount(monitorIndex), 0); + // fetch when not monitored + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + model->fetchMore(monitorIndex); + QVERIFY(waitForPopulation(monitorIndex, model, 2)); + // ensure we cannot refetch + QVERIFY(!model->etmPrivate()->canFetchMore(monitorIndex)); +} + +void LazyPopulationTest::testItemAddedBeforeFetch() +{ + const QString mainCollectionName(QStringLiteral("main2")); + Collection monitorCol; + { + monitorCol.setParentCollection(res3); + monitorCol.setName(mainCollectionName); + auto create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + } + + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + QVERIFY(AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady)); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + // Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(waitForPopulation(res3Index, model, bufferSize + 1)); + + QModelIndex monitorIndex = getIndex(mainCollectionName, model); + QVERIFY(monitorIndex.isValid()); + + // Create a first item before referencing, it should not show up in the ETM + { + Item item1; + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, monitorCol, this); + AKVERIFYEXEC(append); + } + + // Before referenced or fetchMore is called, the collection should be empty + QTest::qWait(500); + QCOMPARE(model->rowCount(monitorIndex), 0); + + // Reference the collection + QVERIFY(!model->etmPrivate()->isMonitored(monitorCol.id())); + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + QVERIFY(model->etmPrivate()->isMonitored(monitorCol.id())); + + // Create another item, it should not be added to the ETM although the signal is emitted from the monitor, but we should be able to fetchMore + { + QSignalSpy addedSpy(changeRecorder, &Monitor::itemAdded); + QVERIFY(addedSpy.isValid()); + Item item2; + item2.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item2, monitorCol, this); + AKVERIFYEXEC(append); + QTRY_VERIFY(addedSpy.count() >= 1); + } + + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + + model->fetchMore(monitorIndex); + // Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 2)); +} + +void LazyPopulationTest::testPurgeEmptyCollection() +{ + const QString mainCollectionName(QStringLiteral("main3")); + Collection monitorCol; + { + monitorCol.setParentCollection(res3); + monitorCol.setName(mainCollectionName); + auto create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + } + // Monitor without referencing so we get all signals + auto monitor = new Monitor(this); + monitor->setCollectionMonitored(Collection::root()); + QVERIFY(AkonadiTest::akWaitForSignal(monitor, &Monitor::monitorReady)); + + auto changeRecorder = new ChangeRecorder(this); + changeRecorder->setCollectionMonitored(Collection::root()); + QVERIFY(AkonadiTest::akWaitForSignal(changeRecorder, &Monitor::monitorReady)); + auto model = new InspectableETM(changeRecorder, this); + model->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + // Wait for initial listing to complete + QVERIFY(waitForPopulation(QModelIndex(), model, numberOfRootCollections)); + + const QModelIndex res3Index = getIndex(QStringLiteral("res3"), model); + QVERIFY(waitForPopulation(res3Index, model, bufferSize + 1)); + + QModelIndex monitorIndex = getIndex(mainCollectionName, model); + QVERIFY(monitorIndex.isValid()); + + // fetch the collection so we remember the empty status + QSignalSpy populatedSpy(model, &EntityTreeModel::collectionPopulated); + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionRefRole); + model->fetchMore(monitorIndex); + // Wait for collection to be fetched + QTRY_VERIFY(populatedSpy.count() >= 1); + + // get the collection purged + model->setData(monitorIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + referenceCollections(model, bufferSize); + + // create an item + { + QSignalSpy addedSpy(monitor, &Monitor::itemAdded); + QVERIFY(addedSpy.isValid()); + Item item1; + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, monitorCol, this); + AKVERIFYEXEC(append); + QTRY_VERIFY(addedSpy.count() >= 1); + } + + // ensure it's not in the model + // fetch the collection + QVERIFY(model->etmPrivate()->canFetchMore(monitorIndex)); + + model->fetchMore(monitorIndex); + // Wait for collection to be fetched + QVERIFY(waitForPopulation(monitorIndex, model, 1)); +} + +#include "lazypopulationtest.moc" + +QTEST_AKONADIMAIN(LazyPopulationTest) diff --git a/autotests/libs/linktest.cpp b/autotests/libs/linktest.cpp new file mode 100644 index 0000000..495e797 --- /dev/null +++ b/autotests/libs/linktest.cpp @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include "collectionfetchjob.h" +#include "control.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "linkjob.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "searchcreatejob.h" +#include "searchquery.h" +#include "unlinkjob.h" + +#include + +using namespace Akonadi; + +class LinkTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + void testLink() + { + auto create = new SearchCreateJob(QStringLiteral("linkTestFolder"), SearchQuery(), this); + AKVERIFYEXEC(create); + + auto list = new CollectionFetchJob(Collection(1), CollectionFetchJob::Recursive, this); + AKVERIFYEXEC(list); + Collection col; + foreach (const Collection &c, list->collections()) { + if (c.name() == QLatin1String("linkTestFolder")) { + col = c; + } + } + QVERIFY(col.isValid()); + + Item::List items; + items << Item(3) << Item(4) << Item(6); + + // Force-retrieve payload from resource + auto f = new ItemFetchJob(items, this); + f->fetchScope().fetchFullPayload(); + AKVERIFYEXEC(f); + const auto itemsLst = f->items(); + for (const Item &item : itemsLst) { + QVERIFY(item.hasPayload()); + } + + Monitor monitor; + monitor.setCollectionMonitored(col); + monitor.itemFetchScope().fetchFullPayload(); + AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady); + + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy lspy(&monitor, &Monitor::itemLinked); + QSignalSpy uspy(&monitor, &Monitor::itemUnlinked); + QVERIFY(lspy.isValid()); + QVERIFY(uspy.isValid()); + + auto link = new LinkJob(col, items, this); + AKVERIFYEXEC(link); + + QTRY_COMPARE(lspy.count(), 3); + QTest::qWait(100); + QVERIFY(uspy.isEmpty()); + + QList arg = lspy.takeFirst(); + Item item = arg.at(0).value(); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); + QVERIFY(item.hasPayload()); + + lspy.clear(); + + auto fetch = new ItemFetchJob(col); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 3); + foreach (const Item &item, fetch->items()) { + QVERIFY(items.contains(item)); + } + + auto unlink = new UnlinkJob(col, items, this); + AKVERIFYEXEC(unlink); + + QTRY_COMPARE(uspy.count(), 3); + QTest::qWait(100); + QVERIFY(lspy.isEmpty()); + + fetch = new ItemFetchJob(col); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->items().count(), 0); + } +}; + +QTEST_AKONADIMAIN(LinkTest) + +#include "linktest.moc" diff --git a/autotests/libs/mimetypecheckertest.cpp b/autotests/libs/mimetypecheckertest.cpp new file mode 100644 index 0000000..b167a6d --- /dev/null +++ b/autotests/libs/mimetypecheckertest.cpp @@ -0,0 +1,291 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "mimetypecheckertest.h" +#include "testattribute.h" + +#include "collection.h" +#include "item.h" + +#include + +#include + +#include + +QTEST_MAIN(MimeTypeCheckerTest) + +using namespace Akonadi; + +MimeTypeCheckerTest::MimeTypeCheckerTest(QObject *parent) + : QObject(parent) +{ + mCalendarSubTypes << QStringLiteral("application/x-vnd.akonadi.calendar.event") << QStringLiteral("application/x-vnd.akonadi.calendar.todo"); +} + +void MimeTypeCheckerTest::initTestCase() +{ + QVERIFY(QMimeDatabase().mimeTypeForName(QLatin1String("application/x-vnd.akonadi.calendar.event")).isValid()); + + MimeTypeChecker emptyChecker; + MimeTypeChecker calendarChecker; + MimeTypeChecker subTypeChecker; + MimeTypeChecker aliasChecker; + + // for testing reset through assignments + const QLatin1String textPlain = QLatin1String("text/plain"); + mEmptyChecker.addWantedMimeType(textPlain); + QVERIFY(!mEmptyChecker.wantedMimeTypes().isEmpty()); + QVERIFY(mEmptyChecker.hasWantedMimeTypes()); + + const QLatin1String textCalendar = QLatin1String("text/calendar"); + calendarChecker.addWantedMimeType(textCalendar); + QCOMPARE(calendarChecker.wantedMimeTypes().count(), 1); + + subTypeChecker.setWantedMimeTypes(mCalendarSubTypes); + QCOMPARE(subTypeChecker.wantedMimeTypes().count(), 2); + + const QLatin1String textVCard = QLatin1String("text/directory"); + aliasChecker.addWantedMimeType(textVCard); + QCOMPARE(aliasChecker.wantedMimeTypes().count(), 1); + + // test assignment works correctly + mEmptyChecker = emptyChecker; + mCalendarChecker = calendarChecker; + mSubTypeChecker = subTypeChecker; + mAliasChecker = aliasChecker; + + QVERIFY(mEmptyChecker.wantedMimeTypes().isEmpty()); + QVERIFY(!mEmptyChecker.hasWantedMimeTypes()); + + QCOMPARE(mCalendarChecker.wantedMimeTypes().count(), 1); + QCOMPARE(mCalendarChecker.wantedMimeTypes(), QStringList() << textCalendar); + + QCOMPARE(mSubTypeChecker.wantedMimeTypes().count(), 2); + const auto calendarSubTypes = QSet(mCalendarSubTypes.begin(), mCalendarSubTypes.end()); + const auto wantedMimeTypes = mSubTypeChecker.wantedMimeTypes(); + const auto wantedSubTypes = QSet(wantedMimeTypes.begin(), wantedMimeTypes.end()); + QCOMPARE(wantedSubTypes, calendarSubTypes); + + QCOMPARE(mAliasChecker.wantedMimeTypes().count(), 1); + QCOMPARE(mAliasChecker.wantedMimeTypes(), QStringList() << textVCard); +} + +void MimeTypeCheckerTest::testCollectionCheck() +{ + Collection invalidCollection; + Collection emptyCollection(1); + Collection calendarCollection(2); + Collection eventCollection(3); + Collection journalCollection(4); + Collection vcardCollection(5); + Collection aliasCollection(6); + + const QLatin1String textCalendar = QLatin1String("text/calendar"); + calendarCollection.setContentMimeTypes(QStringList() << textCalendar); + const QLatin1String akonadiEvent = QLatin1String("application/x-vnd.akonadi.calendar.event"); + eventCollection.setContentMimeTypes(QStringList() << akonadiEvent); + journalCollection.setContentMimeTypes(QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.journal")); + const QLatin1String textDirectory = QLatin1String("text/directory"); + vcardCollection.setContentMimeTypes(QStringList() << textDirectory); + aliasCollection.setContentMimeTypes(QStringList() << QStringLiteral("text/x-vcard")); + + Collection::List voidCollections; + voidCollections << invalidCollection << emptyCollection; + + Collection::List subTypeCollections; + subTypeCollections << eventCollection << journalCollection; + + Collection::List calendarCollections = subTypeCollections; + calendarCollections << calendarCollection; + + Collection::List contactCollections; + contactCollections << vcardCollection << aliasCollection; + + //// empty checker fails for all + Collection::List collections = voidCollections + calendarCollections + contactCollections; + foreach (const Collection &collection, collections) { + QVERIFY(!mEmptyChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, QString())); + } + + //// calendar checker fails for void and contact collections + collections = voidCollections + contactCollections; + foreach (const Collection &collection, collections) { + QVERIFY(!mCalendarChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, textCalendar)); + } + + // but accepts all calendar collections + collections = calendarCollections; + foreach (const Collection &collection, collections) { + QVERIFY(mCalendarChecker.isWantedCollection(collection)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection, textCalendar)); + } + + //// sub type checker fails for all but the event collection + collections = voidCollections + calendarCollections + contactCollections; + collections.removeAll(eventCollection); + foreach (const Collection &collection, collections) { + QVERIFY(!mSubTypeChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, akonadiEvent)); + } + + // but accepts the event collection + collections = Collection::List() << eventCollection; + foreach (const Collection &collection, collections) { + QVERIFY(mSubTypeChecker.isWantedCollection(collection)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection, akonadiEvent)); + } + + //// alias checker fails for void and calendar collections + collections = voidCollections + calendarCollections; + foreach (const Collection &collection, collections) { + QVERIFY(!mAliasChecker.isWantedCollection(collection)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection, textDirectory)); + } + + // but accepts all contact collections + collections = contactCollections; + foreach (const Collection &collection, collections) { + QVERIFY(mAliasChecker.isWantedCollection(collection)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection, textDirectory)); + } +} + +void MimeTypeCheckerTest::testItemCheck() +{ + Item invalidItem; + Item emptyItem(1); + Item calendarItem(2); + Item eventItem(3); + Item journalItem(4); + Item vcardItem(5); + Item aliasItem(6); + + const QLatin1String textCalendar = QLatin1String("text/calendar"); + calendarItem.setMimeType(textCalendar); + const QLatin1String akonadiEvent = QLatin1String("application/x-vnd.akonadi.calendar.event"); + eventItem.setMimeType(akonadiEvent); + journalItem.setMimeType(QStringLiteral("application/x-vnd.akonadi.calendar.journal")); + const QLatin1String textDirectory = QLatin1String("text/directory"); + vcardItem.setMimeType(textDirectory); + aliasItem.setMimeType(QStringLiteral("text/x-vcard")); + + Item::List voidItems; + voidItems << invalidItem << emptyItem; + + Item::List subTypeItems; + subTypeItems << eventItem << journalItem; + + Item::List calendarItems = subTypeItems; + calendarItems << calendarItem; + + Item::List contactItems; + contactItems << vcardItem << aliasItem; + + //// empty checker fails for all + Item::List items = voidItems + calendarItems + contactItems; + foreach (const Item &item, items) { + QVERIFY(!mEmptyChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, QString())); + } + + //// calendar checker fails for void and contact items + items = voidItems + contactItems; + foreach (const Item &item, items) { + QVERIFY(!mCalendarChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, textCalendar)); + } + + // but accepts all calendar items + items = calendarItems; + foreach (const Item &item, items) { + QVERIFY(mCalendarChecker.isWantedItem(item)); + QVERIFY(MimeTypeChecker::isWantedItem(item, textCalendar)); + } + + //// sub type checker fails for all but the event item + items = voidItems + calendarItems + contactItems; + items.removeAll(eventItem); + foreach (const Item &item, items) { + QVERIFY(!mSubTypeChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, akonadiEvent)); + } + + // but accepts the event item + items = Item::List() << eventItem; + foreach (const Item &item, items) { + QVERIFY(mSubTypeChecker.isWantedItem(item)); + QVERIFY(MimeTypeChecker::isWantedItem(item, akonadiEvent)); + } + + //// alias checker fails for void and calendar items + items = voidItems + calendarItems; + foreach (const Item &item, items) { + QVERIFY(!mAliasChecker.isWantedItem(item)); + QVERIFY(!MimeTypeChecker::isWantedItem(item, textDirectory)); + } + + // but accepts all contact items + items = contactItems; + foreach (const Item &item, items) { + QVERIFY(mAliasChecker.isWantedItem(item)); + QVERIFY(MimeTypeChecker::isWantedItem(item, textDirectory)); + } +} + +void MimeTypeCheckerTest::testStringMatchEquivalent() +{ + // check that a random and thus not installed MIME type + // can still be checked just like with direct string comparison + + const QLatin1String installedMimeType("text/plain"); + const QString randomMimeType = QLatin1String("application/x-vnd.test.") + KRandom::randomString(10); + + MimeTypeChecker installedTypeChecker; + installedTypeChecker.addWantedMimeType(installedMimeType); + + MimeTypeChecker randomTypeChecker; + randomTypeChecker.addWantedMimeType(randomMimeType); + + Item item1(1); + item1.setMimeType(installedMimeType); + Item item2(2); + item2.setMimeType(randomMimeType); + + Collection collection1(1); + collection1.setContentMimeTypes(QStringList() << installedMimeType); + Collection collection2(2); + collection2.setContentMimeTypes(QStringList() << randomMimeType); + Collection collection3(3); + collection3.setContentMimeTypes(QStringList() << installedMimeType << randomMimeType); + + QVERIFY(installedTypeChecker.isWantedItem(item1)); + QVERIFY(!randomTypeChecker.isWantedItem(item1)); + QVERIFY(MimeTypeChecker::isWantedItem(item1, installedMimeType)); + QVERIFY(!MimeTypeChecker::isWantedItem(item1, randomMimeType)); + + QVERIFY(!installedTypeChecker.isWantedItem(item2)); + QVERIFY(randomTypeChecker.isWantedItem(item2)); + QVERIFY(!MimeTypeChecker::isWantedItem(item2, installedMimeType)); + QVERIFY(MimeTypeChecker::isWantedItem(item2, randomMimeType)); + + QVERIFY(installedTypeChecker.isWantedCollection(collection1)); + QVERIFY(!randomTypeChecker.isWantedCollection(collection1)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection1, installedMimeType)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection1, randomMimeType)); + + QVERIFY(!installedTypeChecker.isWantedCollection(collection2)); + QVERIFY(randomTypeChecker.isWantedCollection(collection2)); + QVERIFY(!MimeTypeChecker::isWantedCollection(collection2, installedMimeType)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection2, randomMimeType)); + + QVERIFY(installedTypeChecker.isWantedCollection(collection3)); + QVERIFY(randomTypeChecker.isWantedCollection(collection3)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection3, installedMimeType)); + QVERIFY(MimeTypeChecker::isWantedCollection(collection3, randomMimeType)); +} diff --git a/autotests/libs/mimetypecheckertest.h b/autotests/libs/mimetypecheckertest.h new file mode 100644 index 0000000..2904ea2 --- /dev/null +++ b/autotests/libs/mimetypecheckertest.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "mimetypechecker.h" + +#include +#include + +class MimeTypeCheckerTest : public QObject +{ + Q_OBJECT +public: + explicit MimeTypeCheckerTest(QObject *parent = nullptr); + +private: + QStringList mCalendarSubTypes; + + Akonadi::MimeTypeChecker mEmptyChecker; + Akonadi::MimeTypeChecker mCalendarChecker; + Akonadi::MimeTypeChecker mSubTypeChecker; + Akonadi::MimeTypeChecker mAliasChecker; + +private Q_SLOTS: + void initTestCase(); + void testCollectionCheck(); + void testItemCheck(); + void testStringMatchEquivalent(); +}; + diff --git a/autotests/libs/modelspy.cpp b/autotests/libs/modelspy.cpp new file mode 100644 index 0000000..d1c15a9 --- /dev/null +++ b/autotests/libs/modelspy.cpp @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "modelspy.h" + +#include + +QVariantList extractModelColumn(const QAbstractItemModel &model, const QModelIndex &parent, const int firstRow, const int lastRow) +{ + QVariantList result; + for (auto row = firstRow; row <= lastRow; ++row) { + result.append(model.index(row, 0, parent).data()); + } + return result; +} + +ModelSpy::ModelSpy(QAbstractItemModel *model, QObject *parent) + : QObject{parent} + , m_model{model} + , m_isSpying{false} +{ + qRegisterMetaType("QModelIndex"); +} + +bool ModelSpy::isEmpty() const +{ + return QList::isEmpty() && m_expectedSignals.isEmpty(); +} + +void ModelSpy::setExpectedSignals(const QList &expectedSignals) +{ + m_expectedSignals = expectedSignals; +} + +QList ModelSpy::expectedSignals() const +{ + return m_expectedSignals; +} + +void ModelSpy::verifySignal(SignalType type, const QModelIndex &parent, int start, int end) +{ + const auto expectedSignal = m_expectedSignals.takeFirst(); + QCOMPARE(int(type), int(expectedSignal.signalType)); + QCOMPARE(parent.data(), expectedSignal.parentData); + QCOMPARE(start, expectedSignal.startRow); + QCOMPARE(end, expectedSignal.endRow); + if (!expectedSignal.newData.isEmpty()) { + // TODO + } +} + +void ModelSpy::verifySignal(SignalType type, const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destStart) +{ + const auto expectedSignal = m_expectedSignals.takeFirst(); + QCOMPARE(int(type), int(expectedSignal.signalType)); + QCOMPARE(start, expectedSignal.startRow); + QCOMPARE(end, expectedSignal.endRow); + QCOMPARE(parent.data(), expectedSignal.sourceParentData); + QCOMPARE(destParent.data(), expectedSignal.parentData); + QCOMPARE(destStart, expectedSignal.destRow); + if (type == RowsAboutToBeMoved) { + QCOMPARE(extractModelColumn(*m_model, parent, start, end), expectedSignal.newData); + } else { + const auto destEnd = destStart + (end - start); + QCOMPARE(extractModelColumn(*m_model, destParent, destStart, destEnd), expectedSignal.newData); + } +} + +void ModelSpy::verifySignal(SignalType type, const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QCOMPARE(type, DataChanged); + const auto expectedSignal = m_expectedSignals.takeFirst(); + QCOMPARE(int(type), int(expectedSignal.signalType)); + QModelIndex parent = topLeft.parent(); + // This check won't work for toplevel indexes + if (parent.isValid()) { + if (expectedSignal.parentData.isValid()) { + QCOMPARE(parent.data(), expectedSignal.parentData); + } + } + QCOMPARE(topLeft.row(), expectedSignal.startRow); + QCOMPARE(bottomRight.row(), expectedSignal.endRow); + QCOMPARE(extractModelColumn(*m_model, parent, topLeft.row(), bottomRight.row()), expectedSignal.newData); +} + +void ModelSpy::startSpying() +{ + m_isSpying = true; + + // If a signal is connected to a slot multiple times, the slot gets called multiple times. + // As we're doing start and stop spying all the time, we disconnect here first to make sure. + + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &ModelSpy::rowsAboutToBeInserted); + disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &ModelSpy::rowsInserted); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ModelSpy::rowsAboutToBeRemoved); + disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &ModelSpy::rowsRemoved); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeMoved, this, &ModelSpy::rowsAboutToBeMoved); + disconnect(m_model, &QAbstractItemModel::rowsMoved, this, &ModelSpy::rowsMoved); + + disconnect(m_model, &QAbstractItemModel::dataChanged, this, &ModelSpy::dataChanged); + + connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &ModelSpy::rowsAboutToBeInserted); + connect(m_model, &QAbstractItemModel::rowsInserted, this, &ModelSpy::rowsInserted); + connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ModelSpy::rowsAboutToBeRemoved); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ModelSpy::rowsRemoved); + connect(m_model, &QAbstractItemModel::rowsAboutToBeMoved, this, &ModelSpy::rowsAboutToBeMoved); + connect(m_model, &QAbstractItemModel::rowsMoved, this, &ModelSpy::rowsMoved); + + connect(m_model, &QAbstractItemModel::dataChanged, this, &ModelSpy::dataChanged); +} + +void ModelSpy::stopSpying() +{ + m_isSpying = false; + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &ModelSpy::rowsAboutToBeInserted); + disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &ModelSpy::rowsInserted); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ModelSpy::rowsAboutToBeRemoved); + disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &ModelSpy::rowsRemoved); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeMoved, this, &ModelSpy::rowsAboutToBeMoved); + disconnect(m_model, &QAbstractItemModel::rowsMoved, this, &ModelSpy::rowsMoved); + + disconnect(m_model, &QAbstractItemModel::dataChanged, this, &ModelSpy::dataChanged); +} + +void ModelSpy::rowsAboutToBeInserted(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsAboutToBeInserted, parent, start, end); + } else { + append(QVariantList{RowsAboutToBeInserted, QVariant::fromValue(parent), start, end}); + } +} + +void ModelSpy::rowsInserted(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsInserted, parent, start, end); + } else { + append(QVariantList{RowsInserted, QVariant::fromValue(parent), start, end}); + } +} + +void ModelSpy::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsAboutToBeRemoved, parent, start, end); + } else { + append(QVariantList{RowsAboutToBeRemoved, QVariant::fromValue(parent), start, end}); + } +} + +void ModelSpy::rowsRemoved(const QModelIndex &parent, int start, int end) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsRemoved, parent, start, end); + } else { + append(QVariantList{RowsRemoved, QVariant::fromValue(parent), start, end}); + } +} + +void ModelSpy::rowsAboutToBeMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsAboutToBeMoved, srcParent, start, end, destParent, destStart); + } else { + append(QVariantList{RowsAboutToBeMoved, QVariant::fromValue(srcParent), start, end, QVariant::fromValue(destParent), destStart}); + } +} + +void ModelSpy::rowsMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(RowsMoved, srcParent, start, end, destParent, destStart); + } else { + append(QVariantList{RowsMoved, QVariant::fromValue(srcParent), start, end, QVariant::fromValue(destParent), destStart}); + } +} + +void ModelSpy::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!m_expectedSignals.isEmpty()) { + verifySignal(DataChanged, topLeft, bottomRight); + } else { + append(QVariantList{DataChanged, QVariant::fromValue(topLeft), QVariant::fromValue(bottomRight)}); + } +} diff --git a/autotests/libs/modelspy.h b/autotests/libs/modelspy.h new file mode 100644 index 0000000..0f5465a --- /dev/null +++ b/autotests/libs/modelspy.h @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditestfake_export.h" +#include +#include +#include + +enum SignalType { + NoSignal, + RowsAboutToBeInserted, + RowsInserted, + RowsAboutToBeRemoved, + RowsRemoved, + RowsAboutToBeMoved, + RowsMoved, + DataChanged, +}; + +struct ExpectedSignal { + ExpectedSignal(SignalType type, int start, int end, const QVariantList &newData) + : ExpectedSignal{type, start, end, {}, newData} + { + } + + ExpectedSignal(SignalType type, int start, int end, const QVariant &parentData = {}, const QVariantList &newData = {}) + : signalType(type) + , startRow(start) + , endRow(end) + , parentData(parentData) + , newData(newData) + { + } + + ExpectedSignal(SignalType type, + int start, + int end, + const QVariant &sourceParentData, + int destRow, + const QVariant &destParentData, + const QVariantList &newData) + : signalType(type) + , startRow(start) + , endRow(end) + , parentData(destParentData) + , sourceParentData(sourceParentData) + , destRow(destRow) + , newData(newData) + { + } + + SignalType signalType; + int startRow; + int endRow; + QVariant parentData; + QVariant sourceParentData; + int destRow = 0; + QVariantList newData; +}; + +Q_DECLARE_METATYPE(QModelIndex) + +class AKONADITESTFAKE_EXPORT ModelSpy : public QObject, public QList +{ + Q_OBJECT +public: + explicit ModelSpy(QAbstractItemModel *model, QObject *parent = nullptr); + + bool isEmpty() const; + + void setExpectedSignals(const QList &expectedSignals); + QList expectedSignals() const; + + void verifySignal(SignalType type, const QModelIndex &parent, int start, int end); + void verifySignal(SignalType type, const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destStart); + void verifySignal(SignalType type, const QModelIndex &topLeft, const QModelIndex &bottomRight); + + void startSpying(); + void stopSpying(); + bool isSpying() + { + return m_isSpying; + } + +protected Q_SLOTS: + void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end); + void rowsInserted(const QModelIndex &parent, int start, int end); + void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + void rowsRemoved(const QModelIndex &parent, int start, int end); + void rowsAboutToBeMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart); + void rowsMoved(const QModelIndex &srcParent, int start, int end, const QModelIndex &destParent, int destStart); + + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + +private: + QAbstractItemModel *const m_model = nullptr; + bool m_isSpying; + QList m_expectedSignals; +}; + +#ifdef _MSC_VER +// FIXME: This is a very lousy implementation to make MSVC happy. Apparently +// MSVC insists on instantiating QSet QList::toSet() and complains about +// not having qHash() overload for QVariant(List) in QSet +// +// FIXME: This is good enough for ModelSpy, but should never ever be used in +// regular code. +inline uint qHash(const QVariant &v) +{ + return v.userType() + v.toUInt(); +} + +inline uint qHash(const QVariantList &list) +{ + return qHashRange(list.cbegin(), list.cend()); +} +#endif + diff --git a/autotests/libs/monitorfiltertest.cpp b/autotests/libs/monitorfiltertest.cpp new file mode 100644 index 0000000..644f41a --- /dev/null +++ b/autotests/libs/monitorfiltertest.cpp @@ -0,0 +1,369 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "monitor_p.h" +#include +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotification::Operation) +Q_DECLARE_METATYPE(QSet) + +class MonitorFilterTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + } + + void filterConnected_data() + { + QTest::addColumn("op"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << QByteArray(SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << QByteArray(SIGNAL(itemChanged(Akonadi::Item, QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags + << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List, QSet, QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move + << QByteArray(SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << QByteArray(SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection))); + } + + void filterConnected() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + + Protocol::ChangeNotification msg; + msg.addEntity(1); + msg.setOperation(op); + msg.setType(Akonadi::Protocol::ChangeNotification::Items); + + QVERIFY(!m.acceptNotification(msg)); + m.monitorAll = true; + QVERIFY(m.acceptNotification(msg)); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + QVERIFY(m.acceptNotification(msg)); + m.monitorAll = false; + QVERIFY(!m.acceptNotification(msg)); + } + + void filterSession() + { + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + m.monitorAll = true; + QSignalSpy spy(&dummyMonitor, &Monitor::itemAdded); + + Protocol::ChangeNotification msg; + msg.addEntity(1); + msg.setOperation(Protocol::ChangeNotification::Add); + msg.setType(Akonadi::Protocol::ChangeNotification::Items); + msg.setSessionId("foo"); + + QVERIFY(m.acceptNotification(msg)); + m.sessions.append("bar"); + QVERIFY(m.acceptNotification(msg)); + m.sessions.append("foo"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterResource_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemChanged(Akonadi::Item, QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List, QSet, QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection))); + + QTest::newRow("colAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionChanged(Akonadi::Collection, QSet))); + QTest::newRow("colRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionRemoved(Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Subscribe << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionSubscribed(Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Unsubscribe << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + } + + void filterResource() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1); + msg.setOperation(op); + msg.setParentCollection(2); + msg.setType(type); + msg.setResource("foo"); + msg.setSessionId("mysession"); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("bar"); + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("foo"); + QVERIFY(m.acceptNotification(msg)); + + // filtering out the session overwrites the resource + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterDestinationResource_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection))); + } + + void filterDestinationResource() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.setOperation(op); + msg.setType(type); + msg.setResource("foo"); + msg.setDestinationResource("bar"); + msg.setSessionId("mysession"); + msg.addEntity(1); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("bla"); + QVERIFY(!m.acceptNotification(msg)); + m.resources.insert("bar"); + QVERIFY(m.acceptNotification(msg)); + + // filtering out the mimetype does not overwrite resources + msg.addEntity(1, QString(), QString(), QStringLiteral("my/type")); + QVERIFY(m.acceptNotification(msg)); + + // filtering out the session overwrites the resource + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterMimeType_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemChanged(Akonadi::Item, QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List, QSet, QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection))); + + QTest::newRow("colAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionChanged(Akonadi::Collection, QSet))); + QTest::newRow("colRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionRemoved(Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Subscribe << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionSubscribed(Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Unsubscribe << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + } + + void filterMimeType() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1, QString(), QString(), QStringLiteral("my/type")); + msg.setOperation(op); + msg.setParentCollection(2); + msg.setType(type); + msg.setResource("foo"); + msg.setSessionId("mysession"); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.mimetypes.insert(QStringLiteral("your/type")); + QVERIFY(!m.acceptNotification(msg)); + m.mimetypes.insert(QStringLiteral("my/type")); + QCOMPARE(m.acceptNotification(msg), type == Protocol::ChangeNotification::Items); + + // filter out the resource does not overwrite mimetype + m.resources.insert("bar"); + QCOMPARE(m.acceptNotification(msg), type == Protocol::ChangeNotification::Items); + + // filtering out the session overwrites the mimetype + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + } + + void filterCollection_data() + { + QTest::addColumn("op"); + QTest::addColumn("type"); + QTest::addColumn("signalName"); + + QTest::newRow("itemAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemChanged(Akonadi::Item, QSet))); + QTest::newRow("itemsFlagsChanged") << Protocol::ChangeNotification::ModifyFlags << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemsFlagsChanged(Akonadi::Item::List, QSet, QSet))); + QTest::newRow("itemRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemRemoved(Akonadi::Item))); + QTest::newRow("itemMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("itemLinked") << Protocol::ChangeNotification::Link << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection))); + QTest::newRow("itemUnlinked") << Protocol::ChangeNotification::Unlink << Protocol::ChangeNotification::Items + << QByteArray(SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection))); + + QTest::newRow("colAdded") << Protocol::ChangeNotification::Add << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colChanged") << Protocol::ChangeNotification::Modify << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionChanged(Akonadi::Collection, QSet))); + QTest::newRow("colRemoved") << Protocol::ChangeNotification::Remove << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionRemoved(Akonadi::Collection))); + QTest::newRow("colMoved") << Protocol::ChangeNotification::Move << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Subscribe << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionSubscribed(Akonadi::Collection, Akonadi::Collection))); + QTest::newRow("colSubscribed") << Protocol::ChangeNotification::Unsubscribe << Protocol::ChangeNotification::Collections + << QByteArray(SIGNAL(collectionUnsubscribed(Akonadi::Collection))); + } + + void filterCollection() + { + QFETCH(Protocol::ChangeNotification::Operation, op); + QFETCH(Protocol::ChangeNotification::Type, type); + QFETCH(QByteArray, signalName); + + Monitor dummyMonitor; + MonitorPrivate m(0, &dummyMonitor); + QSignalSpy spy(&dummyMonitor, signalName.constData()); + QVERIFY(spy.isValid()); + + Protocol::ChangeNotification msg; + msg.addEntity(1, QString(), QString(), QStringLiteral("my/type")); + msg.setOperation(op); + msg.setParentCollection(2); + msg.setType(type); + msg.setResource("foo"); + msg.setSessionId("mysession"); + + // using the right resource makes it pass + QVERIFY(!m.acceptNotification(msg)); + m.collections.append(Collection(3)); + QVERIFY(!m.acceptNotification(msg)); + + for (int colId = 0; colId < 3; ++colId) { // 0 == root, 1 == this, 2 == parent + if (colId == 1 && type == Protocol::ChangeNotification::Items) { + continue; + } + + m.collections.clear(); + m.collections.append(Collection(colId)); + + QVERIFY(m.acceptNotification(msg)); + + // filter out the resource does overwrite collection + m.resources.insert("bar"); + QVERIFY(!m.acceptNotification(msg)); + m.resources.clear(); + + // filter out the mimetype does overwrite collection, for item operations (mimetype filter has no effect on collections) + m.mimetypes.insert(QStringLiteral("your/type")); + QCOMPARE(!m.acceptNotification(msg), type == Protocol::ChangeNotification::Items); + m.mimetypes.clear(); + + // filtering out the session overwrites the mimetype + m.sessions.append("mysession"); + QVERIFY(!m.acceptNotification(msg)); + m.sessions.clear(); + + // filter non-matching resource and matching mimetype make it pass + m.resources.insert("bar"); + m.mimetypes.insert(QStringLiteral("my/type")); + QVERIFY(m.acceptNotification(msg)); + m.resources.clear(); + m.mimetypes.clear(); + } + } +}; + +QTEST_MAIN(MonitorFilterTest) + +#include "monitorfiltertest.moc" diff --git a/autotests/libs/monitornotificationtest.cpp b/autotests/libs/monitornotificationtest.cpp new file mode 100644 index 0000000..3c20b4e --- /dev/null +++ b/autotests/libs/monitornotificationtest.cpp @@ -0,0 +1,303 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "monitor.h" + +#include "akonaditestfake_export.h" +#include "fakeserverdata.h" +#include "fakesession.h" +#include "inspectablechangerecorder.h" +#include "inspectablemonitor.h" +#include +#include + +using namespace Akonadi; + +class MonitorNotificationTest : public QObject +{ + Q_OBJECT +public: + explicit MonitorNotificationTest(QObject *parent = nullptr) + : QObject(parent) + { + m_sessionName = "MonitorNotificationTest fake session"; + m_fakeSession = new FakeSession(m_sessionName, FakeSession::EndJobsImmediately); + m_fakeSession->setAsDefaultSession(); + } + + ~MonitorNotificationTest() + { + delete m_fakeSession; + } + +private Q_SLOTS: + void testSingleMessage(); + void testFillPipeline(); + void testMonitor(); + + void testSingleMessage_data(); + void testFillPipeline_data(); + void testMonitor_data(); + +private: + template void testSingleMessage_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache); + template void testFillPipeline_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache); + template void testMonitor_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache); + +private: + FakeSession *m_fakeSession = nullptr; + QByteArray m_sessionName; +}; + +void MonitorNotificationTest::testSingleMessage_data() +{ + QTest::addColumn("useChangeRecorder"); + + QTest::newRow("useChangeRecorder") << true; + QTest::newRow("useMonitor") << false; +} + +void MonitorNotificationTest::testSingleMessage() +{ + QFETCH(bool, useChangeRecorder); + + auto collectionCache = new FakeCollectionCache(m_fakeSession); + FakeItemCache itemCache(m_fakeSession); + auto depsFactory = new FakeMonitorDependenciesFactory(&itemCache, collectionCache); + + if (!useChangeRecorder) { + testSingleMessage_impl(new InspectableMonitor(depsFactory, this), collectionCache, &itemCache); + } else { + auto changeRecorder = new InspectableChangeRecorder(depsFactory, this); + changeRecorder->setChangeRecordingEnabled(false); + testSingleMessage_impl(changeRecorder, collectionCache, &itemCache); + } +} + +template +void MonitorNotificationTest::testSingleMessage_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache) +{ + Q_UNUSED(itemCache) + + // Workaround for the QTimer::singleShot() in fake monitors to happen + QTest::qWait(10); + + monitor->setSession(m_fakeSession); + monitor->fetchCollection(true); + + Protocol::ChangeNotificationList list; + + Collection parent(1); + Collection added(2); + + auto msg = Protocol::CollectionChangeNotificationPtr::create(); + msg->setParentCollection(parent.id()); + msg->setOperation(Protocol::CollectionChangeNotification::Add); + msg->setCollection(Protocol::FetchCollectionsResponse(added.id())); + // With notification payloads most requests by-pass the pipeline as the + // notification already contains everything. To force pipelineing we set + // the internal metadata (normally set by ChangeRecorder) + msg->addMetadata("FETCH_COLLECTION"); + + QHash data; + data.insert(parent.id(), parent); + data.insert(added.id(), added); + + // Pending notifications remains empty because we don't fill the pipeline with one message. + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + monitor->notificationConnection()->emitNotify(msg); + + QTRY_COMPARE(monitor->pipeline().size(), 1); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + collectionCache->setData(data); + collectionCache->emitDataAvailable(); + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); +} + +void MonitorNotificationTest::testFillPipeline_data() +{ + QTest::addColumn("useChangeRecorder"); + + QTest::newRow("useChangeRecorder") << true; + QTest::newRow("useMonitor") << false; +} + +void MonitorNotificationTest::testFillPipeline() +{ + QFETCH(bool, useChangeRecorder); + + auto collectionCache = new FakeCollectionCache(m_fakeSession); + FakeItemCache itemCache(m_fakeSession); + auto depsFactory = new FakeMonitorDependenciesFactory(&itemCache, collectionCache); + + if (!useChangeRecorder) { + testFillPipeline_impl(new InspectableMonitor(depsFactory, this), collectionCache, &itemCache); + } else { + auto changeRecorder = new InspectableChangeRecorder(depsFactory, this); + changeRecorder->setChangeRecordingEnabled(false); + testFillPipeline_impl(changeRecorder, collectionCache, &itemCache); + } +} + +template +void MonitorNotificationTest::testFillPipeline_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache) +{ + Q_UNUSED(itemCache) + + monitor->setSession(m_fakeSession); + monitor->fetchCollection(true); + + Protocol::ChangeNotificationList list; + QHash data; + + int i = 1; + while (i < 40) { + Collection parent(i++); + Collection added(i++); + + auto msg = Protocol::CollectionChangeNotificationPtr::create(); + msg->setParentCollection(parent.id()); + msg->setOperation(Protocol::CollectionChangeNotification::Add); + msg->setCollection(Protocol::FetchCollectionsResponse(added.id())); + msg->addMetadata("FETCH_COLLECTION"); + + data.insert(parent.id(), parent); + data.insert(added.id(), added); + + list << msg; + } + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + Q_FOREACH (const Protocol::ChangeNotificationPtr &ntf, list) { + monitor->notificationConnection()->emitNotify(ntf); + } + + QTRY_COMPARE(monitor->pipeline().size(), 5); + QCOMPARE(monitor->pendingNotifications().size(), 15); + + collectionCache->setData(data); + collectionCache->emitDataAvailable(); + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); +} + +void MonitorNotificationTest::testMonitor_data() +{ + QTest::addColumn("useChangeRecorder"); + + QTest::newRow("useChangeRecorder") << true; + QTest::newRow("useMonitor") << false; +} + +void MonitorNotificationTest::testMonitor() +{ + QFETCH(bool, useChangeRecorder); + + auto collectionCache = new FakeCollectionCache(m_fakeSession); + FakeItemCache itemCache(m_fakeSession); + auto depsFactory = new FakeMonitorDependenciesFactory(&itemCache, collectionCache); + + if (!useChangeRecorder) { + testMonitor_impl(new InspectableMonitor(depsFactory, this), collectionCache, &itemCache); + } else { + auto changeRecorder = new InspectableChangeRecorder(depsFactory, this); + changeRecorder->setChangeRecordingEnabled(false); + testMonitor_impl(changeRecorder, collectionCache, &itemCache); + } +} + +template +void MonitorNotificationTest::testMonitor_impl(MonitorImpl *monitor, FakeCollectionCache *collectionCache, FakeItemCache *itemCache) +{ + Q_UNUSED(itemCache) + + monitor->setSession(m_fakeSession); + monitor->fetchCollection(true); + + Protocol::ChangeNotificationList list; + + Collection col2(2); + col2.setParentCollection(Collection::root()); + + collectionCache->insert(col2); + + int i = 4; + + while (i < 8) { + Collection added(i++); + + auto msg = Protocol::CollectionChangeNotificationPtr::create(); + msg->setParentCollection(i % 2 ? 2 : added.id() - 1); + msg->setOperation(Protocol::CollectionChangeNotification::Add); + msg->setCollection(Protocol::FetchCollectionsResponse(added.id())); + msg->addMetadata("FETCH_COLLECTION"); + + list << msg; + } + + QVERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + Collection col4(4); + col4.setParentCollection(col2); + Collection col6(6); + col6.setParentCollection(col2); + + collectionCache->insert(col4); + collectionCache->insert(col6); + + qRegisterMetaType(); + QSignalSpy collectionAddedSpy(monitor, SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection))); + + collectionCache->emitDataAvailable(); + + QTRY_VERIFY(monitor->pipeline().isEmpty()); + QVERIFY(monitor->pendingNotifications().isEmpty()); + + Q_FOREACH (const Protocol::ChangeNotificationPtr &ntf, list) { + monitor->notificationConnection()->emitNotify(ntf); + } + + // Collection 6 is not notified, because Collection 5 has held up the pipeline + QTRY_COMPARE(collectionAddedSpy.size(), 1); + QCOMPARE((int)collectionAddedSpy.takeFirst().first().value().id(), 4); + QCOMPARE(monitor->pipeline().size(), 3); + QCOMPARE(monitor->pendingNotifications().size(), 0); + + Collection col7(7); + col7.setParentCollection(col6); + + collectionCache->insert(col7); + collectionCache->emitDataAvailable(); + + // Collection 5 is still holding the pipeline + QCOMPARE(collectionAddedSpy.size(), 0); + QCOMPARE(monitor->pipeline().size(), 3); + QCOMPARE(monitor->pendingNotifications().size(), 0); + + Collection col5(5); + col5.setParentCollection(col4); + + collectionCache->insert(col5); + collectionCache->emitDataAvailable(); + + // Collection 5 is in cache, pipeline is flushed + QCOMPARE(collectionAddedSpy.size(), 3); + QCOMPARE(monitor->pipeline().size(), 0); + QCOMPARE(monitor->pendingNotifications().size(), 0); +} + +QTEST_MAIN(MonitorNotificationTest) +#include "monitornotificationtest.moc" diff --git a/autotests/libs/monitortest.cpp b/autotests/libs/monitortest.cpp new file mode 100644 index 0000000..4a4123f --- /dev/null +++ b/autotests/libs/monitortest.cpp @@ -0,0 +1,399 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "monitortest.h" +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "collectionmovejob.h" +#include "collectionstatistics.h" +#include "control.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "searchcreatejob.h" +#include "searchquery.h" +#include "subscriptionjob_p.h" + +#include +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(MonitorTest) + +static Collection res3; + +Q_DECLARE_METATYPE(Akonadi::Collection::Id) +Q_DECLARE_METATYPE(QSet) + +void MonitorTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); + + res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + + AkonadiTest::setAllResourcesOffline(); +} + +void MonitorTest::testMonitor_data() +{ + QTest::addColumn("fetchCol"); + QTest::newRow("with collection fetching") << true; + QTest::newRow("without collection fetching") << false; +} + +void MonitorTest::testMonitor() +{ + QFETCH(bool, fetchCol); + + Monitor monitor; + monitor.setCollectionMonitored(Collection::root()); + monitor.fetchCollection(fetchCol); + monitor.itemFetchScope().fetchFullPayload(); + monitor.itemFetchScope().setCacheOnly(true); + QVERIFY(AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady)); + + // monitor signals + qRegisterMetaType(); + /* + qRegisterMetaType() registers the type with a + name of "qlonglong". Doing + qRegisterMetaType( "Akonadi::Collection::Id" ) + doesn't help. (works now , see QTBUG-937 and QTBUG-6833, -- dvratil) + + The problem here is that Akonadi::Collection::Id is a typedef to qlonglong, + and qlonglong is already a registered meta type. So the signal spy will + give us a QVariant of type Akonadi::Collection::Id, but calling + .value() on that variant will in fact end up + calling qvariant_cast. From the point of view of QMetaType, + Akonadi::Collection::Id and qlonglong are different types, so QVariant + can't convert, and returns a default-constructed qlonglong, zero. + + When connecting to a real slot (without QSignalSpy), this problem is + avoided, because the casting is done differently (via a lot of void + pointers). + + The docs say nothing about qRegisterMetaType -ing a typedef, so I'm not + sure if this is a bug or not. (cberzan) + */ + qRegisterMetaType("Akonadi::Collection::Id"); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); + QSignalSpy caddspy(&monitor, &Monitor::collectionAdded); + QSignalSpy cmodspy(&monitor, SIGNAL(collectionChanged(Akonadi::Collection, QSet))); + QSignalSpy cmvspy(&monitor, &Monitor::collectionMoved); + QSignalSpy crmspy(&monitor, &Monitor::collectionRemoved); + QSignalSpy cstatspy(&monitor, &Monitor::collectionStatisticsChanged); + QSignalSpy cSubscribedSpy(&monitor, &Monitor::collectionSubscribed); + QSignalSpy cUnsubscribedSpy(&monitor, &Monitor::collectionUnsubscribed); + QSignalSpy iaddspy(&monitor, &Monitor::itemAdded); + QSignalSpy imodspy(&monitor, &Monitor::itemChanged); + QSignalSpy imvspy(&monitor, &Monitor::itemMoved); + QSignalSpy irmspy(&monitor, &Monitor::itemRemoved); + + QVERIFY(caddspy.isValid()); + QVERIFY(cmodspy.isValid()); + QVERIFY(cmvspy.isValid()); + QVERIFY(crmspy.isValid()); + QVERIFY(cstatspy.isValid()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isValid()); + QVERIFY(imodspy.isValid()); + QVERIFY(imvspy.isValid()); + QVERIFY(irmspy.isValid()); + + // create a collection + Collection monitorCol; + monitorCol.setParentCollection(res3); + monitorCol.setName(QStringLiteral("monitor")); + auto create = new CollectionCreateJob(monitorCol, this); + AKVERIFYEXEC(create); + monitorCol = create->collection(); + QVERIFY(monitorCol.isValid()); + + QTRY_COMPARE(caddspy.count(), 1); + QList arg = caddspy.takeFirst(); + auto col = arg.at(0).value(); + QCOMPARE(col, monitorCol); + if (fetchCol) { + QCOMPARE(col.name(), QStringLiteral("monitor")); + } + auto parent = arg.at(1).value(); + QCOMPARE(parent, res3); + + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // add an item + Item newItem; + newItem.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(newItem, monitorCol, this); + AKVERIFYEXEC(append); + Item monitorRef = append->item(); + QVERIFY(monitorRef.isValid()); + + QTRY_COMPARE(cstatspy.count(), 1); + arg = cstatspy.takeFirst(); + QCOMPARE(arg.at(0).value(), monitorCol.id()); + + QCOMPARE(iaddspy.count(), 1); + arg = iaddspy.takeFirst(); + Item item = arg.at(0).value(); + QCOMPARE(item, monitorRef); + QCOMPARE(item.mimeType(), QString::fromLatin1("application/octet-stream")); + auto collection = arg.at(1).value(); + QCOMPARE(collection.id(), monitorCol.id()); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // modify an item + item.setPayload("some new content"); + auto store = new ItemModifyJob(item, this); + AKVERIFYEXEC(store); + + QTRY_COMPARE(cstatspy.count(), 1); + arg = cstatspy.takeFirst(); + QCOMPARE(arg.at(0).value(), monitorCol.id()); + + QCOMPARE(imodspy.count(), 1); + arg = imodspy.takeFirst(); + item = arg.at(0).value(); + QCOMPARE(monitorRef, item); + QVERIFY(item.hasPayload()); + QCOMPARE(item.payload(), QByteArray("some new content")); + auto parts = arg.at(1).value>(); + QCOMPARE(parts, QSet() << "PLD:RFC822"); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // move an item + auto move = new ItemMoveJob(item, res3); + AKVERIFYEXEC(move); + QTRY_COMPARE(cstatspy.count(), 2); + // NOTE: We don't make any assumptions about the order of the collectionStatisticsChanged + // signals, they seem to arrive in random order + QList notifiedCols; + notifiedCols << cstatspy.takeFirst().at(0).value() << cstatspy.takeFirst().at(0).value(); + QVERIFY(notifiedCols.contains(res3.id())); // destination + QVERIFY(notifiedCols.contains(monitorCol.id())); // source + + QCOMPARE(imvspy.count(), 1); + arg = imvspy.takeFirst(); + item = arg.at(0).value(); // the item + QCOMPARE(monitorRef, item); + col = arg.at(1).value(); // the source collection + QCOMPARE(col.id(), monitorCol.id()); + col = arg.at(2).value(); // the destination collection + QCOMPARE(col.id(), res3.id()); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // delete an item + auto del = new ItemDeleteJob(monitorRef, this); + AKVERIFYEXEC(del); + + QTRY_COMPARE(cstatspy.count(), 1); + arg = cstatspy.takeFirst(); + QCOMPARE(arg.at(0).value(), res3.id()); + cmodspy.clear(); + + QCOMPARE(irmspy.count(), 1); + arg = irmspy.takeFirst(); + Item ref = qvariant_cast(arg.at(0)); + QCOMPARE(monitorRef, ref); + QCOMPARE(ref.parentCollection(), res3); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + imvspy.clear(); + + // Unsubscribe and re-subscribed a collection that existed before the monitor was created. + Collection subCollection = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/foo2"))); + subCollection.setName(QStringLiteral("foo2")); + QVERIFY(subCollection.isValid()); + + auto subscribeJob = new SubscriptionJob(this); + subscribeJob->unsubscribe(Collection::List() << subCollection); + AKVERIFYEXEC(subscribeJob); + // Wait for unsubscribed signal, it goes after changed, so we can check for both + QTRY_COMPARE(cmodspy.size(), 1); + arg = cmodspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + + QVERIFY(cSubscribedSpy.isEmpty()); + QTRY_COMPARE(cUnsubscribedSpy.size(), 1); + arg = cUnsubscribedSpy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + + subscribeJob = new SubscriptionJob(this); + subscribeJob->subscribe(Collection::List() << subCollection); + AKVERIFYEXEC(subscribeJob); + // Wait for subscribed signal, it goes after changed, so we can check for both + QTRY_COMPARE(cmodspy.size(), 1); + arg = cmodspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + + QVERIFY(cUnsubscribedSpy.isEmpty()); + QTRY_COMPARE(cSubscribedSpy.size(), 1); + arg = cSubscribedSpy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), subCollection.id()); + if (fetchCol) { + QVERIFY(!col.name().isEmpty()); + QCOMPARE(col.name(), subCollection.name()); + } + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // modify a collection + monitorCol.setName(QStringLiteral("changed name")); + auto mod = new CollectionModifyJob(monitorCol, this); + AKVERIFYEXEC(mod); + + QTRY_COMPARE(cmodspy.count(), 1); + arg = cmodspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col, monitorCol); + if (fetchCol) { + QCOMPARE(col.name(), QStringLiteral("changed name")); + } + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // move a collection + Collection dest = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo"))); + auto cmove = new CollectionMoveJob(monitorCol, dest, this); + AKVERIFYEXEC(cmove); + + QTRY_COMPARE(cmvspy.count(), 1); + arg = cmvspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col, monitorCol); + QCOMPARE(col.parentCollection(), dest); + if (fetchCol) { + QCOMPARE(col.name(), monitorCol.name()); + } + col = arg.at(1).value(); + QCOMPARE(col, res3); + col = arg.at(2).value(); + QCOMPARE(col, dest); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(crmspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); + + // delete a collection + auto cdel = new CollectionDeleteJob(monitorCol, this); + AKVERIFYEXEC(cdel); + + QTRY_COMPARE(crmspy.count(), 1); + arg = crmspy.takeFirst(); + col = arg.at(0).value(); + QCOMPARE(col.id(), monitorCol.id()); + QCOMPARE(col.parentCollection(), dest); + + QVERIFY(caddspy.isEmpty()); + QVERIFY(cmodspy.isEmpty()); + QVERIFY(cmvspy.isEmpty()); + QVERIFY(cstatspy.isEmpty()); + QVERIFY(cSubscribedSpy.isEmpty()); + QVERIFY(cUnsubscribedSpy.isEmpty()); + QVERIFY(iaddspy.isEmpty()); + QVERIFY(imodspy.isEmpty()); + QVERIFY(imvspy.isEmpty()); + QVERIFY(irmspy.isEmpty()); +} + +void MonitorTest::testVirtualCollectionsMonitoring() +{ + Monitor monitor; + monitor.setCollectionMonitored(Collection(1)); // top-level 'Search' collection + QVERIFY(AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady)); + + QSignalSpy caddspy(&monitor, &Monitor::collectionAdded); + + auto job = new SearchCreateJob(QStringLiteral("Test search collection"), Akonadi::SearchQuery(), this); + AKVERIFYEXEC(job); + QTRY_COMPARE(caddspy.count(), 1); +} diff --git a/autotests/libs/monitortest.h b/autotests/libs/monitortest.h new file mode 100644 index 0000000..368ce78 --- /dev/null +++ b/autotests/libs/monitortest.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class MonitorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testMonitor_data(); + void testMonitor(); + void testVirtualCollectionsMonitoring(); +}; + diff --git a/autotests/libs/protocolhelpertest.cpp b/autotests/libs/protocolhelpertest.cpp new file mode 100644 index 0000000..e1745eb --- /dev/null +++ b/autotests/libs/protocolhelpertest.cpp @@ -0,0 +1,324 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "protocolhelper.cpp" +#include "attributestorage.cpp" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +Q_DECLARE_METATYPE(Scope) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(Protocol::ItemFetchScope) + +class ProtocolHelperTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testItemSetToByteArray_data() + { + QTest::addColumn("items"); + QTest::addColumn("result"); + QTest::addColumn("shouldThrow"); + + Item u1; + u1.setId(1); + Item u2; + u2.setId(2); + Item u3; + u3.setId(3); + Item r1; + r1.setRemoteId(QStringLiteral("A")); + Item r2; + r2.setRemoteId(QStringLiteral("B")); + Item h1; + h1.setRemoteId(QStringLiteral("H1")); + h1.setParentCollection(Collection::root()); + Item h2; + h2.setRemoteId(QStringLiteral("H2a")); + h2.parentCollection().setRemoteId(QStringLiteral("H2b")); + h2.parentCollection().setParentCollection(Collection::root()); + Item h3; + h3.setRemoteId(QStringLiteral("H3a")); + h3.parentCollection().setRemoteId(QStringLiteral("H3b")); + + QTest::newRow("empty") << Item::List() << Scope() << true; + QTest::newRow("single uid") << (Item::List() << u1) << Scope(1) << false; + QTest::newRow("multi uid") << (Item::List() << u1 << u3) << Scope(QVector{1, 3}) << false; + QTest::newRow("block uid") << (Item::List() << u1 << u2 << u3) << Scope(ImapInterval(1, 3)) << false; + QTest::newRow("single rid") << (Item::List() << r1) << Scope(Scope::Rid, {QStringLiteral("A")}) << false; + QTest::newRow("multi rid") << (Item::List() << r1 << r2) << Scope(Scope::Rid, {QStringLiteral("A"), QStringLiteral("B")}) << false; + QTest::newRow("invalid") << (Item::List() << Item()) << Scope() << true; + QTest::newRow("mixed") << (Item::List() << u1 << r1) << Scope() << true; + QTest::newRow("single hrid") << (Item::List() << h1) << Scope({Scope::HRID(-1, QStringLiteral("H1")), Scope::HRID(0)}) << false; + QTest::newRow("single hrid 2") << (Item::List() << h2) + << Scope({Scope::HRID(-1, QStringLiteral("H2a")), Scope::HRID(-2, QStringLiteral("H2b")), Scope::HRID(0)}) << false; + QTest::newRow("mixed hrid/rid") << (Item::List() << h1 << r1) << Scope(Scope::Rid, {QStringLiteral("H1"), QStringLiteral("A")}) << false; + QTest::newRow("unterminated hrid") << (Item::List() << h3) << Scope(Scope::Rid, {QStringLiteral("H3a")}) << false; + } + + void testItemSetToByteArray() + { + QFETCH(Item::List, items); + QFETCH(Scope, result); + QFETCH(bool, shouldThrow); + + bool didThrow = false; + try { + const Scope scope = ProtocolHelper::entitySetToScope(items); + QCOMPARE(scope, result); + } catch (const std::exception &e) { + qDebug() << e.what(); + didThrow = true; + } + QCOMPARE(didThrow, shouldThrow); + } + + void testAncestorParsing_data() + { + QTest::addColumn>("input"); + QTest::addColumn("parent"); + + QTest::newRow("top-level") << QVector{Protocol::Ancestor(0)} << Collection::root(); + + Protocol::Ancestor a1(42); + a1.setRemoteId(QStringLiteral("net")); + + Collection c1; + c1.setRemoteId(QStringLiteral("net")); + c1.setId(42); + c1.setParentCollection(Collection::root()); + QTest::newRow("till's obscure folder") << QVector{a1, Protocol::Ancestor(0)} << c1; + } + + void testAncestorParsing() + { + QFETCH(QVector, input); + QFETCH(Collection, parent); + + Item i; + ProtocolHelper::parseAncestors(input, &i); + QCOMPARE(i.parentCollection().id(), parent.id()); + QCOMPARE(i.parentCollection().remoteId(), parent.remoteId()); + } + + void testCollectionParsing_data() + { + QTest::addColumn("input"); + QTest::addColumn("collection"); + + Collection c1; + c1.setId(2); + c1.setRemoteId(QStringLiteral("r2")); + c1.parentCollection().setId(1); + c1.setName(QStringLiteral("n2")); + + { + Protocol::FetchCollectionsResponse resp(2); + resp.setParentId(1); + resp.setRemoteId(QStringLiteral("r2")); + resp.setName(QStringLiteral("n2")); + QTest::newRow("no ancestors") << resp << c1; + } + + { + Protocol::FetchCollectionsResponse resp(3); + resp.setParentId(2); + resp.setRemoteId(QStringLiteral("r3")); + resp.setAncestors({Protocol::Ancestor(2, QStringLiteral("r2")), Protocol::Ancestor(1, QStringLiteral("r1")), Protocol::Ancestor(0)}); + + Collection c2; + c2.setId(3); + c2.setRemoteId(QStringLiteral("r3")); + c2.parentCollection().setId(2); + c2.parentCollection().setRemoteId(QStringLiteral("r2")); + c2.parentCollection().parentCollection().setId(1); + c2.parentCollection().parentCollection().setRemoteId(QStringLiteral("r1")); + c2.parentCollection().parentCollection().setParentCollection(Collection::root()); + QTest::newRow("ancestors") << resp << c2; + } + } + + void testCollectionParsing() + { + QFETCH(Protocol::FetchCollectionsResponse, input); + QFETCH(Collection, collection); + + Collection parsedCollection = ProtocolHelper::parseCollection(input); + + QCOMPARE(parsedCollection.name(), collection.name()); + + while (collection.isValid() || parsedCollection.isValid()) { + QCOMPARE(parsedCollection.id(), collection.id()); + QCOMPARE(parsedCollection.remoteId(), collection.remoteId()); + const Collection p1(parsedCollection.parentCollection()); + const Collection p2(collection.parentCollection()); + parsedCollection = p1; + collection = p2; + qDebug() << p1.isValid() << p2.isValid(); + } + } + + void testParentCollectionAfterCollectionParsing() + { + Protocol::FetchCollectionsResponse resp(111); + resp.setParentId(222); + resp.setRemoteId(QStringLiteral("A")); + resp.setAncestors({Protocol::Ancestor(222), Protocol::Ancestor(333), Protocol::Ancestor(0)}); + + Collection parsedCollection = ProtocolHelper::parseCollection(resp); + + QList ids; + ids << 111 << 222 << 333 << 0; + int i = 0; + + Collection col = parsedCollection; + while (col.isValid()) { + QCOMPARE(col.id(), ids[i++]); + col = col.parentCollection(); + } + QCOMPARE(i, 4); + } + + void testHRidToScope_data() + { + QTest::addColumn("collection"); + QTest::addColumn("result"); + + QTest::newRow("empty") << Collection() << Scope(); + + { + Scope scope; + scope.setHRidChain({Scope::HRID(0)}); + QTest::newRow("root") << Collection::root() << scope; + } + + Collection c; + c.setId(1); + c.setParentCollection(Collection::root()); + c.setRemoteId(QStringLiteral("r1")); + { + Scope scope; + scope.setHRidChain({Scope::HRID(1, QStringLiteral("r1")), Scope::HRID(0)}); + QTest::newRow("one level") << c << scope; + } + + { + Collection c2; + c2.setId(2); + c2.setParentCollection(c); + c2.setRemoteId(QStringLiteral("r2")); + + Scope scope; + scope.setHRidChain({Scope::HRID(2, QStringLiteral("r2")), Scope::HRID(1, QStringLiteral("r1")), Scope::HRID(0)}); + QTest::newRow("two level ok") << c2 << scope; + } + } + + void testHRidToScope() + { + QFETCH(Collection, collection); + QFETCH(Scope, result); + QCOMPARE(ProtocolHelper::hierarchicalRidToScope(collection), result); + } + + void testItemFetchScopeToProtocol_data() + { + QTest::addColumn("scope"); + QTest::addColumn("result"); + + { + Protocol::ItemFetchScope fs; + fs.setFetch(Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::RemoteID + | Protocol::ItemFetchScope::RemoteRevision | Protocol::ItemFetchScope::MTime); + QTest::newRow("empty") << ItemFetchScope() << fs; + } + + { + ItemFetchScope scope; + scope.fetchAllAttributes(); + scope.fetchFullPayload(); + scope.setAncestorRetrieval(Akonadi::ItemFetchScope::All); + scope.setIgnoreRetrievalErrors(true); + + Protocol::ItemFetchScope fs; + fs.setFetch(Protocol::ItemFetchScope::FullPayload | Protocol::ItemFetchScope::AllAttributes | Protocol::ItemFetchScope::Flags + | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::RemoteID | Protocol::ItemFetchScope::RemoteRevision + | Protocol::ItemFetchScope::MTime | Protocol::ItemFetchScope::IgnoreErrors); + fs.setAncestorDepth(Protocol::ItemFetchScope::AllAncestors); + QTest::newRow("full") << scope << fs; + } + + { + ItemFetchScope scope; + scope.setFetchModificationTime(false); + scope.setFetchRemoteIdentification(false); + + Protocol::ItemFetchScope fs; + fs.setFetch(Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::Size); + QTest::newRow("minimal") << scope << fs; + } + } + + void testItemFetchScopeToProtocol() + { + QFETCH(ItemFetchScope, scope); + QFETCH(Protocol::ItemFetchScope, result); + QCOMPARE(ProtocolHelper::itemFetchScopeToProtocol(scope), result); + } + + void testTagParsing_data() + { + QTest::addColumn("input"); + QTest::addColumn("expected"); + + QTest::newRow("invalid") << Protocol::FetchTagsResponse(-1) << Tag(); + + Protocol::FetchTagsResponse response(15); + response.setGid("TAG13GID"); + response.setRemoteId("TAG13RID"); + response.setParentId(-1); + response.setType("PLAIN"); + response.setAttributes({{"TAGAttribute", "MyAttribute"}}); + + Tag tag(15); + tag.setGid("TAG13GID"); + tag.setRemoteId("TAG13RID"); + tag.setType("PLAIN"); + auto attr = AttributeFactory::createAttribute("TAGAttribute"); + attr->deserialize("MyAttribute"); + tag.addAttribute(attr); + QTest::newRow("valid with invalid parent") << response << tag; + + response.setParentId(15); + tag.setParent(Tag(15)); + QTest::newRow("valid with valid parent") << response << tag; + } + + void testTagParsing() + { + QFETCH(Protocol::FetchTagsResponse, input); + QFETCH(Tag, expected); + + const Tag tag = ProtocolHelper::parseTagFetchResult(input); + QCOMPARE(tag.id(), expected.id()); + QCOMPARE(tag.gid(), expected.gid()); + QCOMPARE(tag.remoteId(), expected.remoteId()); + QCOMPARE(tag.type(), expected.type()); + QCOMPARE(tag.parent(), expected.parent()); + QCOMPARE(tag.attributes().size(), expected.attributes().size()); + for (int i = 0; i < tag.attributes().size(); ++i) { + Attribute *attr = tag.attributes().at(i); + Attribute *expectedAttr = expected.attributes().at(i); + QCOMPARE(attr->type(), expectedAttr->type()); + QCOMPARE(attr->serialized(), expectedAttr->serialized()); + } + } +}; + +QTEST_MAIN(ProtocolHelperTest) + +#include "protocolhelpertest.moc" diff --git a/autotests/libs/proxymodelstest.cpp b/autotests/libs/proxymodelstest.cpp new file mode 100644 index 0000000..c6a80f5 --- /dev/null +++ b/autotests/libs/proxymodelstest.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2010 Till Adam + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include +#include + +class KRFPTestModel : public QSortFilterProxyModel +{ +public: + explicit KRFPTestModel(QObject *parent) + : QSortFilterProxyModel(parent) + { + } + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override + { + const QModelIndex modelIndex = sourceModel()->index(sourceRow, 0, sourceParent); + return !modelIndex.data().toString().contains(QLatin1String("three")); + } +}; + +class ProxyModelsTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void init(); + + void testMatch(); + +private: + QStandardItemModel m_model; + QSortFilterProxyModel *m_krfp = nullptr; + KRFPTestModel *m_krfptest = nullptr; +}; + +void ProxyModelsTest::initTestCase() +{ +} + +void ProxyModelsTest::init() +{ + m_model.setRowCount(5); + m_model.setColumnCount(1); + m_model.setData(m_model.index(0, 0, QModelIndex()), QStringLiteral("one")); + QModelIndex idx = m_model.index(1, 0, QModelIndex()); + m_model.setData(idx, QStringLiteral("two")); + m_model.insertRows(0, 1, idx); + m_model.insertColumns(0, 1, idx); + m_model.setData(m_model.index(0, 0, idx), QStringLiteral("three")); + m_model.setData(m_model.index(2, 0, QModelIndex()), QStringLiteral("three")); + m_model.setData(m_model.index(3, 0, QModelIndex()), QStringLiteral("four")); + m_model.setData(m_model.index(4, 0, QModelIndex()), QStringLiteral("five")); + + m_model.setData(m_model.index(4, 0, QModelIndex()), QStringLiteral("mystuff"), Qt::UserRole + 42); + + m_krfp = new QSortFilterProxyModel(this); + m_krfp->setSourceModel(&m_model); + m_krfp->setRecursiveFilteringEnabled(true); + m_krfptest = new KRFPTestModel(this); + m_krfptest->setSourceModel(m_krfp); + + // some sanity checks that setup worked + QCOMPARE(m_model.rowCount(QModelIndex()), 5); + QCOMPARE(m_model.data(m_model.index(0, 0)).toString(), QStringLiteral("one")); + QCOMPARE(m_krfp->rowCount(QModelIndex()), 5); + QCOMPARE(m_krfp->data(m_krfp->index(0, 0)).toString(), QStringLiteral("one")); + QCOMPARE(m_krfptest->rowCount(QModelIndex()), 4); + QCOMPARE(m_krfptest->data(m_krfptest->index(0, 0)).toString(), QStringLiteral("one")); + + QCOMPARE(m_krfp->rowCount(m_krfp->index(1, 0)), 1); + QCOMPARE(m_krfptest->rowCount(m_krfptest->index(1, 0)), 0); +} + +void ProxyModelsTest::testMatch() +{ + QModelIndexList results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 1); + results = m_model.match(m_model.index(0, 0), Qt::DisplayRole, QStringLiteral("fourtytwo")); + QCOMPARE(results.size(), 0); + results = m_model.match(m_model.index(0, 0), Qt::UserRole + 42, QStringLiteral("mystuff")); + QCOMPARE(results.size(), 1); + + results = m_krfp->match(m_krfp->index(0, 0), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 1); + results = m_krfp->match(m_krfp->index(0, 0), Qt::UserRole + 42, QStringLiteral("mystuff")); + QCOMPARE(results.size(), 1); + + results = m_krfptest->match(m_krfptest->index(0, 0), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + results = m_krfptest->match(m_krfptest->index(0, 0), Qt::UserRole + 42, QStringLiteral("mystuff")); + QCOMPARE(results.size(), 1); + + results = m_model.match(QModelIndex(), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + results = m_krfp->match(QModelIndex(), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + results = m_krfptest->match(QModelIndex(), Qt::DisplayRole, QStringLiteral("three")); + QCOMPARE(results.size(), 0); + + const QModelIndex index = m_model.index(0, 0, QModelIndex()); + results = m_model.match(index, Qt::DisplayRole, QStringLiteral("three"), -1, Qt::MatchRecursive | Qt::MatchStartsWith | Qt::MatchWrap); + QCOMPARE(results.size(), 2); +} + +#include "proxymodelstest.moc" + +QTEST_MAIN(ProxyModelsTest) diff --git a/autotests/libs/relationtest.cpp b/autotests/libs/relationtest.cpp new file mode 100644 index 0000000..d3e6e0f --- /dev/null +++ b/autotests/libs/relationtest.cpp @@ -0,0 +1,164 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "attributefactory.h" +#include "control.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "relationcreatejob.h" +#include "relationdeletejob.h" +#include "relationfetchjob.h" +#include "resourceselectjob_p.h" +#include "tagmodifyjob.h" + +using namespace Akonadi; + +class RelationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testCreateFetch(); + void testMonitor(); + void testEqualRelation(); +}; + +void RelationTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); +} + +void RelationTest::testCreateFetch() +{ + const Collection res3{AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))}; + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item2, res3, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + + Relation rel(Relation::GENERIC, item1, item2); + auto createjob = new RelationCreateJob(rel, this); + AKVERIFYEXEC(createjob); + + // Test fetch & create + { + auto fetchJob = new RelationFetchJob(QVector(), this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->relations().size(), 1); + QCOMPARE(fetchJob->relations().first().type(), QByteArray(Relation::GENERIC)); + } + + // Test item fetch + { + auto fetchJob = new ItemFetchJob(item1); + fetchJob->fetchScope().setFetchRelations(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().relations().size(), 1); + } + + { + auto fetchJob = new ItemFetchJob(item2); + fetchJob->fetchScope().setFetchRelations(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().relations().size(), 1); + } + + // Test delete + { + auto deleteJob = new RelationDeleteJob(rel, this); + AKVERIFYEXEC(deleteJob); + + auto fetchJob = new RelationFetchJob(QVector(), this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->relations().size(), 0); + } +} + +void RelationTest::testMonitor() +{ + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Relations); + QVERIFY(AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady)); + + const Collection res3{AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))}; + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item2, res3, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + + Relation rel(Relation::GENERIC, item1, item2); + + { + QSignalSpy addedSpy(&monitor, &Monitor::relationAdded); + + auto createjob = new RelationCreateJob(rel, this); + AKVERIFYEXEC(createjob); + + // We usually pick up signals from the previous tests as well (due to server-side notification caching) + QTRY_VERIFY(addedSpy.count() >= 1); + QTRY_COMPARE(addedSpy.last().first().value(), rel); + } + + { + QSignalSpy removedSpy(&monitor, &Monitor::relationRemoved); + QVERIFY(removedSpy.isValid()); + auto deleteJob = new RelationDeleteJob(rel, this); + AKVERIFYEXEC(deleteJob); + QTRY_VERIFY(removedSpy.count() >= 1); + QTRY_COMPARE(removedSpy.last().first().value(), rel); + } +} + +void RelationTest::testEqualRelation() +{ + Relation r1; + Item it1(45); + Item it2(46); + r1.setLeft(it1); + r1.setRight(it2); + r1.setRemoteId(QByteArrayLiteral("foo")); + r1.setType(QByteArrayLiteral("foo1")); + + Relation r2 = r1; + QCOMPARE(r1, r2); +} + +QTEST_AKONADIMAIN(RelationTest) + +#include "relationtest.moc" diff --git a/autotests/libs/resourceschedulertest.cpp b/autotests/libs/resourceschedulertest.cpp new file mode 100644 index 0000000..4ae4a85 --- /dev/null +++ b/autotests/libs/resourceschedulertest.cpp @@ -0,0 +1,359 @@ +/* + SPDX-FileCopyrightText: 2009 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "resourceschedulertest.h" + +#include "../src/agentbase/resourcescheduler_p.h" + +#include +#include + +using namespace Akonadi; + +QTEST_MAIN(ResourceSchedulerTest) + +Q_DECLARE_METATYPE(QSet) + +ResourceSchedulerTest::ResourceSchedulerTest(QObject *parent) + : QObject(parent) +{ + qRegisterMetaType>(); +} + +void ResourceSchedulerTest::testTaskComparison() +{ + ResourceScheduler::Task t1; + t1.type = ResourceScheduler::ChangeReplay; + ResourceScheduler::Task t2; + t2.type = ResourceScheduler::ChangeReplay; + QCOMPARE(t1, t2); + QList taskList; + taskList.append(t1); + QVERIFY(taskList.contains(t2)); + + ResourceScheduler::Task t3; + t3.type = ResourceScheduler::DeleteResourceCollection; + QVERIFY(!(t2 == t3)); + QVERIFY(!taskList.contains(t3)); + + ResourceScheduler::Task t4; + t4.type = ResourceScheduler::Custom; + t4.receiver = this; + t4.methodName = "customTask"; + t4.argument = QStringLiteral("call1"); + + ResourceScheduler::Task t5(t4); + QVERIFY(t4 == t5); + + t5.argument = QStringLiteral("call2"); + QVERIFY(!(t4 == t5)); +} + +void ResourceSchedulerTest::testChangeReplaySchedule() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + qRegisterMetaType("Akonadi::Collection"); + QSignalSpy changeReplaySpy(&scheduler, SIGNAL(executeChangeReplay())); + QSignalSpy collectionTreeSyncSpy(&scheduler, SIGNAL(executeCollectionTreeSync())); + QSignalSpy syncSpy(&scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection))); + QVERIFY(changeReplaySpy.isValid()); + QVERIFY(collectionTreeSyncSpy.isValid()); + QVERIFY(syncSpy.isValid()); + + // Schedule a change replay, it should be executed first thing when we enter the + // event loop, but not before + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleChangeReplay(); + QVERIFY(!scheduler.isEmpty()); + QVERIFY(changeReplaySpy.isEmpty()); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + + // Schedule two change replays. The duplicate one should not be executed. + changeReplaySpy.clear(); + scheduler.scheduleChangeReplay(); + scheduler.scheduleChangeReplay(); + QVERIFY(changeReplaySpy.isEmpty()); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + + // Schedule a second change replay while one is in progress, should give as two signal emissions + changeReplaySpy.clear(); + scheduler.scheduleChangeReplay(); + QVERIFY(changeReplaySpy.isEmpty()); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 1); + scheduler.scheduleChangeReplay(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(changeReplaySpy.count(), 2); + scheduler.taskDone(); + + // + // Schedule various stuff. + // + Collection collection(42); + changeReplaySpy.clear(); + scheduler.scheduleCollectionTreeSync(); + scheduler.scheduleChangeReplay(); + scheduler.scheduleSync(collection); + scheduler.scheduleChangeReplay(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + + // Omit a taskDone() here, there shouldn't be a new signal + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); + + // At this point, we're done, check that nothing else is emitted + scheduler.taskDone(); + QVERIFY(scheduler.isEmpty()); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); +} + +void ResourceSchedulerTest::customTaskNoArg() +{ + ++mCustomCallCount; +} + +void ResourceSchedulerTest::customTask(const QVariant &argument) +{ + ++mCustomCallCount; + mLastArgument = argument; +} + +void ResourceSchedulerTest::testCustomTask() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + mCustomCallCount = 0; + + scheduler.scheduleCustomTask(this, "customTask", QStringLiteral("call1")); + scheduler.scheduleCustomTask(this, "customTask", QStringLiteral("call1")); + scheduler.scheduleCustomTask(this, "customTask", QStringLiteral("call2")); + scheduler.scheduleCustomTask(this, "customTaskNoArg", QVariant()); + + QCOMPARE(mCustomCallCount, 0); + + QTest::qWait(1); + QCOMPARE(mCustomCallCount, 1); + QCOMPARE(mLastArgument.toString(), QStringLiteral("call1")); + + scheduler.taskDone(); + QVERIFY(!scheduler.isEmpty()); + QTest::qWait(1); + QCOMPARE(mCustomCallCount, 2); + QCOMPARE(mLastArgument.toString(), QStringLiteral("call2")); + + scheduler.taskDone(); + QVERIFY(!scheduler.isEmpty()); + QTest::qWait(1); + QCOMPARE(mCustomCallCount, 3); + + scheduler.taskDone(); + QVERIFY(scheduler.isEmpty()); +} + +void ResourceSchedulerTest::testCompression() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + qRegisterMetaType("Akonadi::Collection"); + qRegisterMetaType("Akonadi::Item"); + QSignalSpy fullSyncSpy(&scheduler, SIGNAL(executeFullSync())); + QSignalSpy collectionTreeSyncSpy(&scheduler, SIGNAL(executeCollectionTreeSync())); + QSignalSpy syncSpy(&scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection))); + QSignalSpy fetchSpy(&scheduler, SIGNAL(executeItemFetch(Akonadi::Item, QSet))); + QVERIFY(fullSyncSpy.isValid()); + QVERIFY(collectionTreeSyncSpy.isValid()); + QVERIFY(syncSpy.isValid()); + QVERIFY(fetchSpy.isValid()); + + // full sync + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleFullSync(); + scheduler.scheduleFullSync(); + QTest::qWait(1); // start execution + QCOMPARE(fullSyncSpy.count(), 1); + scheduler.scheduleCollectionTreeSync(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(fullSyncSpy.count(), 1); + QVERIFY(scheduler.isEmpty()); + + // collection tree sync + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleCollectionTreeSync(); + scheduler.scheduleCollectionTreeSync(); + QTest::qWait(1); // start execution + QCOMPARE(collectionTreeSyncSpy.count(), 1); + scheduler.scheduleCollectionTreeSync(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QVERIFY(scheduler.isEmpty()); + + // sync collection + scheduler.scheduleSync(Akonadi::Collection(42)); + scheduler.scheduleSync(Akonadi::Collection(42)); + QTest::qWait(1); // start execution + QCOMPARE(syncSpy.count(), 1); + scheduler.scheduleSync(Akonadi::Collection(43)); + scheduler.scheduleSync(Akonadi::Collection(42)); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(syncSpy.count(), 2); + scheduler.taskDone(); + QTest::qWait(2); + QVERIFY(scheduler.isEmpty()); + + // sync collection + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + QTest::qWait(1); // start execution + QCOMPARE(fetchSpy.count(), 1); + scheduler.scheduleItemFetch(Akonadi::Item(43), QSet(), QDBusMessage()); + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(fetchSpy.count(), 2); + scheduler.taskDone(); + QTest::qWait(2); + QVERIFY(scheduler.isEmpty()); +} + +void ResourceSchedulerTest::testSyncCompletion() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + QSignalSpy completionSpy(&scheduler, SIGNAL(fullSyncComplete())); + QVERIFY(completionSpy.isValid()); + + // sync completion does not do compression + QVERIFY(scheduler.isEmpty()); + scheduler.scheduleFullSyncCompletion(); + scheduler.scheduleFullSyncCompletion(); + QTest::qWait(1); // start execution + QCOMPARE(completionSpy.count(), 1); + scheduler.scheduleFullSyncCompletion(); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(completionSpy.count(), 2); + scheduler.taskDone(); + QTest::qWait(1); + QCOMPARE(completionSpy.count(), 3); + scheduler.taskDone(); + QVERIFY(scheduler.isEmpty()); +} + +void ResourceSchedulerTest::testPriorities() +{ + ResourceScheduler scheduler; + scheduler.setOnline(true); + qRegisterMetaType("Akonadi::Collection"); + qRegisterMetaType("Akonadi::Item"); + QSignalSpy changeReplaySpy(&scheduler, SIGNAL(executeChangeReplay())); + QSignalSpy fullSyncSpy(&scheduler, SIGNAL(executeFullSync())); + QSignalSpy collectionTreeSyncSpy(&scheduler, SIGNAL(executeCollectionTreeSync())); + QSignalSpy syncSpy(&scheduler, SIGNAL(executeCollectionSync(Akonadi::Collection))); + QSignalSpy fetchSpy(&scheduler, SIGNAL(executeItemFetch(Akonadi::Item, QSet))); + QSignalSpy attributesSyncSpy(&scheduler, SIGNAL(executeCollectionAttributesSync(Akonadi::Collection))); + QVERIFY(changeReplaySpy.isValid()); + QVERIFY(fullSyncSpy.isValid()); + QVERIFY(collectionTreeSyncSpy.isValid()); + QVERIFY(syncSpy.isValid()); + QVERIFY(fetchSpy.isValid()); + QVERIFY(attributesSyncSpy.isValid()); + + scheduler.scheduleCollectionTreeSync(); + scheduler.scheduleChangeReplay(); + scheduler.scheduleSync(Akonadi::Collection(42)); + scheduler.scheduleItemFetch(Akonadi::Item(42), QSet(), QDBusMessage()); + scheduler.scheduleAttributesSync(Akonadi::Collection(42)); + scheduler.scheduleFullSync(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 0); + QCOMPARE(attributesSyncSpy.count(), 0); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 0); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 0); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 0); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); + QCOMPARE(fullSyncSpy.count(), 0); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QTest::qWait(1); + QCOMPARE(collectionTreeSyncSpy.count(), 1); + QCOMPARE(changeReplaySpy.count(), 1); + QCOMPARE(syncSpy.count(), 1); + QCOMPARE(fullSyncSpy.count(), 1); + QCOMPARE(fetchSpy.count(), 1); + QCOMPARE(attributesSyncSpy.count(), 1); + scheduler.taskDone(); + + QVERIFY(scheduler.isEmpty()); +} diff --git a/autotests/libs/resourceschedulertest.h b/autotests/libs/resourceschedulertest.h new file mode 100644 index 0000000..dc953b7 --- /dev/null +++ b/autotests/libs/resourceschedulertest.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2009 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include +#include + +class ResourceSchedulerTest : public QObject +{ + Q_OBJECT +public: + explicit ResourceSchedulerTest(QObject *parent = nullptr); + +public Q_SLOTS: + void customTask(const QVariant &argument); + void customTaskNoArg(); + +private Q_SLOTS: + + void testTaskComparison(); + void testChangeReplaySchedule(); + void testCustomTask(); + void testCompression(); + void testSyncCompletion(); + void testPriorities(); + +private: + int mCustomCallCount; + QVariant mLastArgument; +}; + diff --git a/autotests/libs/resourcetest.cpp b/autotests/libs/resourcetest.cpp new file mode 100644 index 0000000..636238e --- /dev/null +++ b/autotests/libs/resourcetest.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "qtest_akonadi.h" + +using namespace Akonadi; + +class ResourceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + void testResourceManagement() + { + qRegisterMetaType(); + QSignalSpy spyAddInstance(AgentManager::self(), &AgentManager::instanceAdded); + QVERIFY(spyAddInstance.isValid()); + QSignalSpy spyRemoveInstance(AgentManager::self(), &AgentManager::instanceRemoved); + QVERIFY(spyRemoveInstance.isValid()); + + AgentType type = AgentManager::self()->type(QStringLiteral("akonadi_knut_resource")); + QVERIFY(type.isValid()); + + QStringList lst; + lst << QStringLiteral("Resource"); + QCOMPARE(type.capabilities(), lst); + + auto job = new AgentInstanceCreateJob(type); + AKVERIFYEXEC(job); + + AgentInstance instance = job->instance(); + QVERIFY(instance.isValid()); + + QCOMPARE(spyAddInstance.count(), 1); + QCOMPARE(spyAddInstance.first().at(0).value(), instance); + QVERIFY(AgentManager::self()->instance(instance.identifier()).isValid()); + + job = new AgentInstanceCreateJob(type); + AKVERIFYEXEC(job); + AgentInstance instance2 = job->instance(); + QVERIFY(!(instance == instance2)); + QCOMPARE(spyAddInstance.count(), 2); + + AgentManager::self()->removeInstance(instance); + AgentManager::self()->removeInstance(instance2); + QTRY_COMPARE(spyRemoveInstance.count(), 2); + QVERIFY(!AgentManager::self()->instances().contains(instance)); + QVERIFY(!AgentManager::self()->instances().contains(instance2)); + } + + void testIllegalResourceManagement() + { + auto job = new AgentInstanceCreateJob(AgentManager::self()->type(QStringLiteral("non_existing_resource"))); + QVERIFY(!job->exec()); + + // unique agent + // According to vkrause the mailthreader agent is no longer started by + // default so this won't work. + /* + const AgentType type = AgentManager::self()->type( "akonadi_mailthreader_agent" ); + QVERIFY( type.isValid() ); + job = new AgentInstanceCreateJob( type ); + AKVERIFYEXEC( job ); + + job = new AgentInstanceCreateJob( type ); + QVERIFY( !job->exec() ); + */ + } +}; + +QTEST_AKONADIMAIN(ResourceTest) + +#include "resourcetest.moc" diff --git a/autotests/libs/searchjobtest.cpp b/autotests/libs/searchjobtest.cpp new file mode 100644 index 0000000..660503c --- /dev/null +++ b/autotests/libs/searchjobtest.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "searchjobtest.h" +#include "qtest_akonadi.h" + +#include "collection.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionmodifyjob.h" +#include "collectionutils.h" +#include "itemfetchjob.h" +#include "persistentsearchattribute.h" +#include "searchcreatejob.h" +#include "searchquery.h" + +QTEST_AKONADIMAIN(SearchJobTest) + +using namespace Akonadi; + +void SearchJobTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); +} + +void SearchJobTest::testCreateDeleteSearch() +{ + Akonadi::SearchQuery query; + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 1)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 2)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 3)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 4)); + + // Create collection + SearchCreateJob *create = new SearchCreateJob(QStringLiteral("search123456"), query, this); + create->setRemoteSearchEnabled(false); + AKVERIFYEXEC(create); + const Collection created = create->createdCollection(); + QVERIFY(created.isValid()); + + // Fetch "Search" collection, check the search collection has been created + CollectionFetchJob *list = new CollectionFetchJob(Collection(1), CollectionFetchJob::Recursive, this); + AKVERIFYEXEC(list); + const Collection::List cols = list->collections(); + Collection col; + for (const auto &c : cols) { + if (c.name() == QLatin1String("search123456")) { + col = c; + break; + } + } + QVERIFY(col == created); + QCOMPARE(col.parentCollection().id(), 1LL); + QVERIFY(col.isVirtual()); + + // Fetch items in the search collection, check whether they are there + ItemFetchJob *fetch = new ItemFetchJob(created, this); + AKVERIFYEXEC(fetch); + const Item::List items = fetch->items(); + QCOMPARE(items.count(), 2); + + CollectionDeleteJob *delJob = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(delJob); +} + +void SearchJobTest::testModifySearch() +{ + Akonadi::SearchQuery query; + query.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 1)); + query.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 2)); + + // make sure there is a virtual collection + SearchCreateJob *create = new SearchCreateJob(QStringLiteral("search123456"), query, this); + AKVERIFYEXEC(create); + Collection created = create->createdCollection(); + QVERIFY(created.isValid()); + QVERIFY(created.hasAttribute()); + + auto attr = created.attribute(); + QVERIFY(!attr->isRecursive()); + QVERIFY(!attr->isRemoteSearchEnabled()); + QCOMPARE(attr->queryCollections(), QVector{0}); + const QString oldQueryString = attr->queryString(); + + // Change the attributes + attr->setRecursive(true); + attr->setRemoteSearchEnabled(true); + attr->setQueryCollections(QVector{1}); + Akonadi::SearchQuery newQuery; + newQuery.addTerm(Akonadi::SearchTerm(QStringLiteral("plugin"), 3)); + newQuery.addTerm(Akonadi::SearchTerm(QStringLiteral("resource"), 4)); + attr->setQueryString(QString::fromUtf8(newQuery.toJSON())); + + auto modify = new CollectionModifyJob(created, this); + AKVERIFYEXEC(modify); + + auto fetch = new CollectionFetchJob(created, CollectionFetchJob::Base, this); + AKVERIFYEXEC(fetch); + QCOMPARE(fetch->collections().size(), 1); + + const auto col = fetch->collections().first(); + QVERIFY(col.hasAttribute()); + attr = col.attribute(); + + QVERIFY(attr->isRecursive()); + QVERIFY(attr->isRemoteSearchEnabled()); + QCOMPARE(attr->queryCollections(), QVector{1}); + QVERIFY(attr->queryString() != oldQueryString); + + auto delJob = new CollectionDeleteJob(col, this); + AKVERIFYEXEC(delJob); +} diff --git a/autotests/libs/searchjobtest.h b/autotests/libs/searchjobtest.h new file mode 100644 index 0000000..8fa56e8 --- /dev/null +++ b/autotests/libs/searchjobtest.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class SearchJobTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testCreateDeleteSearch(); + void testModifySearch(); +}; + diff --git a/autotests/libs/searchquerytest.cpp b/autotests/libs/searchquerytest.cpp new file mode 100644 index 0000000..f3bed63 --- /dev/null +++ b/autotests/libs/searchquerytest.cpp @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "searchquery.h" +#include +#include + +using namespace Akonadi; + +class SearchQueryTest : public QObject +{ + Q_OBJECT +private: + void verifySimpleTerm(const QVariantMap &json, const SearchTerm &term, bool *ok) + { + *ok = false; + QCOMPARE(term.subTerms().count(), 0); + QVERIFY(json.contains(QLatin1String("key"))); + QCOMPARE(json[QStringLiteral("key")].toString(), term.key()); + QVERIFY(json.contains(QLatin1String("value"))); + QCOMPARE(json[QStringLiteral("value")], term.value()); + QVERIFY(json.contains(QLatin1String("cond"))); + QCOMPARE(static_cast(json[QStringLiteral("cond")].toInt()), term.condition()); + QVERIFY(json.contains(QLatin1String("negated"))); + QCOMPARE(json[QStringLiteral("negated")].toBool(), term.isNegated()); + *ok = true; + } + +private Q_SLOTS: + void testSerializer() + { + QJson::Parser parser; + bool ok = false; + + { + SearchQuery query; + query.addTerm(QStringLiteral("body"), QStringLiteral("test string"), SearchTerm::CondContains); + + ok = false; + QVariantMap map = parser.parse(query.toJSON(), &ok).toMap(); + QVERIFY(ok); + + QCOMPARE(static_cast(map[QStringLiteral("rel")].toInt()), SearchTerm::RelAnd); + const QVariantList subTerms = map[QStringLiteral("subTerms")].toList(); + QCOMPARE(subTerms.size(), 1); + + ok = false; + verifySimpleTerm(subTerms.first().toMap(), query.term().subTerms()[0], &ok); + QVERIFY(ok); + } + + { + SearchQuery query(SearchTerm::RelOr); + query.addTerm(SearchTerm(QStringLiteral("to"), QStringLiteral("test@test.user"), SearchTerm::CondEqual)); + SearchTerm term2(QStringLiteral("subject"), QStringLiteral("Hello"), SearchTerm::CondContains); + term2.setIsNegated(true); + query.addTerm(term2); + + ok = false; + QVariantMap map = parser.parse(query.toJSON(), &ok).toMap(); + QVERIFY(ok); + + QCOMPARE(static_cast(map[QStringLiteral("rel")].toInt()), query.term().relation()); + const QVariantList subTerms = map[QStringLiteral("subTerms")].toList(); + QCOMPARE(subTerms.size(), query.term().subTerms().count()); + + for (int i = 0; i < subTerms.size(); ++i) { + ok = false; + verifySimpleTerm(subTerms[i].toMap(), query.term().subTerms()[i], &ok); + QVERIFY(ok); + } + } + } + + void testParser() + { + QJson::Serializer serializer; + bool ok = false; + + { + QVariantList subTerms; + QVariantMap termJSON; + termJSON[QStringLiteral("key")] = QStringLiteral("created"); + termJSON[QStringLiteral("value")] = QDateTime(QDate(2014, 01, 24), QTime(17, 49, 00)); + termJSON[QStringLiteral("cond")] = static_cast(SearchTerm::CondGreaterOrEqual); + termJSON[QStringLiteral("negated")] = true; + subTerms << termJSON; + + termJSON[QStringLiteral("key")] = QStringLiteral("subject"); + termJSON[QStringLiteral("value")] = QStringLiteral("Hello"); + termJSON[QStringLiteral("cond")] = static_cast(SearchTerm::CondEqual); + termJSON[QStringLiteral("negated")] = false; + subTerms << termJSON; + + QVariantMap map; + map[QStringLiteral("rel")] = static_cast(SearchTerm::RelAnd); + map[QStringLiteral("subTerms")] = subTerms; + +#if !defined(USE_QJSON_0_8) + const QByteArray json = serializer.serialize(map); + QVERIFY(!json.isNull()); +#else + ok = false; + const QByteArray json = serializer.serialize(map, &ok); + QVERIFY(ok); +#endif + + const SearchQuery query = SearchQuery::fromJSON(json); + QVERIFY(!query.isNull()); + const SearchTerm term = query.term(); + + QCOMPARE(static_cast(map[QStringLiteral("rel")].toInt()), term.relation()); + QCOMPARE(subTerms.count(), term.subTerms().count()); + + for (int i = 0; i < subTerms.count(); ++i) { + ok = false; + verifySimpleTerm(subTerms.at(i).toMap(), term.subTerms()[i], &ok); + QVERIFY(ok); + } + } + } + + void testFullQuery() + { + { + SearchQuery query; + query.addTerm("key", "value"); + const QByteArray serialized = query.toJSON(); + QCOMPARE(SearchQuery::fromJSON(serialized), query); + } + { + SearchQuery query; + query.setLimit(10); + query.addTerm("key", "value"); + const QByteArray serialized = query.toJSON(); + QCOMPARE(SearchQuery::fromJSON(serialized), query); + } + } +}; + +QTEST_AKONADIMAIN(SearchQueryTest, NoGUI) + +#include "searchquerytest.moc" diff --git a/autotests/libs/servermanagertest.cpp b/autotests/libs/servermanagertest.cpp new file mode 100644 index 0000000..7e857e2 --- /dev/null +++ b/autotests/libs/servermanagertest.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "servermanager.h" +#include "control.h" +#include "qtest_akonadi.h" + +#include + +using namespace Akonadi; + +class ServerManagerTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + QVERIFY(Control::start()); + trackAkonadiProcess(false); + } + + void cleanupTestCase() + { + trackAkonadiProcess(true); + } + + void testStartStop() + { + QSignalSpy startSpy(ServerManager::self(), SIGNAL(started())); + QVERIFY(startSpy.isValid()); + QSignalSpy stopSpy(ServerManager::self(), SIGNAL(stopped())); + QVERIFY(stopSpy.isValid()); + + QVERIFY(ServerManager::isRunning()); + QVERIFY(Control::start()); + + QVERIFY(startSpy.isEmpty()); + QVERIFY(stopSpy.isEmpty()); + + { + QSignalSpy spy(ServerManager::self(), SIGNAL(stopped())); + QVERIFY(ServerManager::stop()); + QTRY_VERIFY(spy.count() >= 1); + } + QVERIFY(!ServerManager::isRunning()); + QVERIFY(startSpy.isEmpty()); + QCOMPARE(stopSpy.count(), 1); + + QVERIFY(!ServerManager::stop()); + { + QSignalSpy spy(ServerManager::self(), SIGNAL(started())); + QVERIFY(ServerManager::start()); + QTRY_VERIFY(spy.count() >= 1); + } + QVERIFY(ServerManager::isRunning()); + QCOMPARE(startSpy.count(), 1); + QCOMPARE(stopSpy.count(), 1); + } + + void testRestart() + { + QVERIFY(ServerManager::isRunning()); + QSignalSpy startSpy(ServerManager::self(), SIGNAL(started())); + QVERIFY(startSpy.isValid()); + + QVERIFY(Control::restart()); + + QVERIFY(ServerManager::isRunning()); + QCOMPARE(startSpy.count(), 1); + } +}; + +QTEST_AKONADIMAIN(ServerManagerTest) + +#include "servermanagertest.moc" diff --git a/autotests/libs/sharedvaluepooltest.cpp b/autotests/libs/sharedvaluepooltest.cpp new file mode 100644 index 0000000..c6e08bf --- /dev/null +++ b/autotests/libs/sharedvaluepooltest.cpp @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "../sharedvaluepool_p.h" +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +class SharedValuePoolTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testQVector_data() + { + QTest::addColumn("size"); + QTest::newRow("10") << 10; + QTest::newRow("100") << 100; + } + + void testQVector() + { + QFETCH(int, size); + QVector data; + Internal::SharedValuePool pool; + + for (int i = 0; i < size; ++i) { + QByteArray b(10, static_cast(i)); + data.push_back(b); + QCOMPARE(pool.sharedValue(b), b); + QCOMPARE(pool.sharedValue(b), b); + } + + QBENCHMARK { + foreach (const QByteArray &b, data) { + pool.sharedValue(b); + } + } + } + + /*void testQSet_data() + { + QTest::addColumn( "size" ); + QTest::newRow( "10" ) << 10; + QTest::newRow( "100" ) << 100; + } + + void testQSet() + { + QFETCH( int, size ); + QVector data; + Internal::SharedValuePool pool; + + for ( int i = 0; i < size; ++i ) { + QByteArray b( 10, (char)i ); + data.push_back( b ); + QCOMPARE( pool.sharedValue( b ), b ); + QCOMPARE( pool.sharedValue( b ), b ); + } + + QBENCHMARK { + foreach ( const QByteArray &b, data ) + pool.sharedValue( b ); + } + }*/ +}; + +QTEST_MAIN(SharedValuePoolTest) + +#include "sharedvaluepooltest.moc" diff --git a/autotests/libs/statisticsproxymodeltest.cpp b/autotests/libs/statisticsproxymodeltest.cpp new file mode 100644 index 0000000..4b7ee0a --- /dev/null +++ b/autotests/libs/statisticsproxymodeltest.cpp @@ -0,0 +1,237 @@ +/* + SPDX-FileCopyrightText: 2016 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "statisticsproxymodel.h" +#include "collection.h" +#include "collectionstatistics.h" +#include "entitytreemodel.h" +#include "test_model_helpers.h" + +#include +#include + +using namespace TestModelHelpers; + +using Akonadi::StatisticsProxyModel; + +#ifndef Q_OS_WIN +void initLocale() +{ + setenv("LC_ALL", "en_US.utf-8", 1); +} +Q_CONSTRUCTOR_FUNCTION(initLocale) +#endif + +class StatisticsProxyModelTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void init(); + void shouldDoNothingIfNoExtraColumns(); + void shouldShowExtraColumns(); + void shouldShowToolTip(); + void shouldHandleDataChanged(); + void shouldHandleDataChangedInExtraColumn(); + +private: + QStandardItemModel m_model; +}; + +// Helper functions + +static QString indexToText(const QModelIndex &index) +{ + if (!index.isValid()) { + return QStringLiteral("invalid"); + } + return QString::number(index.row()) + QLatin1Char(',') + QString::number(index.column()) + QLatin1Char(',') + + QString::number(reinterpret_cast(index.internalPointer()), 16) + QLatin1String(" in ") + + QString::number(reinterpret_cast(index.model()), 16); +} + +static QString indexRowCol(const QModelIndex &index) +{ + if (!index.isValid()) { + return QStringLiteral("invalid"); + } + return QString::number(index.row()) + QLatin1Char(',') + QString::number(index.column()); +} + +static Akonadi::CollectionStatistics makeStats(qint64 unread, qint64 count, qint64 size) +{ + Akonadi::CollectionStatistics stats; + stats.setUnreadCount(unread); + stats.setCount(count); + stats.setSize(size); + return stats; +} + +void StatisticsProxyModelTest::initTestCase() +{ +} + +void StatisticsProxyModelTest::init() +{ + // Prepare the source model to use later on + m_model.clear(); + m_model.appendRow(makeStandardItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C") << QStringLiteral("D"))); + m_model.item(0, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("m") << QStringLiteral("n") << QStringLiteral("o") << QStringLiteral("p"))); + m_model.item(0, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("q") << QStringLiteral("r") << QStringLiteral("s") << QStringLiteral("t"))); + m_model.appendRow(makeStandardItems(QStringList() << QStringLiteral("E") << QStringLiteral("F") << QStringLiteral("G") << QStringLiteral("H"))); + m_model.item(1, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("x") << QStringLiteral("y") << QStringLiteral("z") << QStringLiteral("."))); + m_model.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3") << QStringLiteral("H4")); + + // Set Collection c1 for row A + Akonadi::Collection c1(1); + c1.setName(QStringLiteral("c1")); + c1.setStatistics(makeStats(2, 6, 9)); // unread, count, size in bytes + m_model.item(0, 0)->setData(QVariant::fromValue(c1), Akonadi::EntityTreeModel::CollectionRole); + + // Set Collection c2 for first child (row m) + Akonadi::Collection c2(2); + c2.setName(QStringLiteral("c2")); + c2.setStatistics(makeStats(1, 3, 4)); // unread, count, size in bytes + m_model.item(0, 0)->child(0)->setData(QVariant::fromValue(c2), Akonadi::EntityTreeModel::CollectionRole); + + // Set Collection c2 for first child (row m) + Akonadi::Collection c3(3); + c3.setName(QStringLiteral("c3")); + c3.setStatistics(makeStats(0, 1, 1)); // unread, count, size in bytes + m_model.item(0, 0)->child(1)->setData(QVariant::fromValue(c3), Akonadi::EntityTreeModel::CollectionRole); + + QCOMPARE(extractRowTexts(&m_model, 0), QStringLiteral("ABCD")); + QCOMPARE(extractRowTexts(&m_model, 0, m_model.index(0, 0)), QStringLiteral("mnop")); + QCOMPARE(extractRowTexts(&m_model, 1, m_model.index(0, 0)), QStringLiteral("qrst")); + QCOMPARE(extractRowTexts(&m_model, 1), QStringLiteral("EFGH")); + QCOMPARE(extractRowTexts(&m_model, 0, m_model.index(1, 0)), QStringLiteral("xyz.")); + QCOMPARE(extractHorizontalHeaderTexts(&m_model), QStringLiteral("H1H2H3H4")); +} + +void StatisticsProxyModelTest::shouldDoNothingIfNoExtraColumns() +{ + // Given a statistics proxy with no extra columns + StatisticsProxyModel pm; + pm.setExtraColumnsEnabled(false); + + // When setting it to a source model + pm.setSourceModel(&m_model); + + // Then the proxy should show the same as the model + QCOMPARE(pm.rowCount(), m_model.rowCount()); + QCOMPARE(pm.columnCount(), m_model.columnCount()); + + QCOMPARE(pm.rowCount(pm.index(0, 0)), 2); + QCOMPARE(pm.index(0, 0).parent(), QModelIndex()); + + // (verify that the mapFromSource(mapToSource(x)) == x roundtrip works) + for (int row = 0; row < pm.rowCount(); ++row) { + for (int col = 0; col < pm.columnCount(); ++col) { + QCOMPARE(pm.mapFromSource(pm.mapToSource(pm.index(row, col))), pm.index(row, col)); + } + } + + QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD")); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("mnop")); + QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("qrst")); + QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("EFGH")); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("xyz.")); + QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H1H2H3H4")); +} + +void StatisticsProxyModelTest::shouldShowExtraColumns() +{ + // Given a extra-columns proxy with three extra columns + StatisticsProxyModel pm; + + // When setting it to a source model + pm.setSourceModel(&m_model); + + // Then the proxy should show the extra column + QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD269 B")); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("mnop134 B")); + QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("qrst 11 B")); + QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("EFGH ")); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("xyz. ")); + QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H1H2H3H4UnreadTotalSize")); + + // Verify tree structure of proxy + const QModelIndex secondParent = pm.index(1, 0); + QVERIFY(!secondParent.parent().isValid()); + QCOMPARE(indexToText(pm.index(0, 0, secondParent).parent()), indexToText(secondParent)); + QCOMPARE(indexToText(pm.index(0, 3, secondParent).parent()), indexToText(secondParent)); + QVERIFY(indexToText(pm.index(0, 4)).startsWith(QLatin1String("0,4,"))); + QCOMPARE(indexToText(pm.index(0, 4, secondParent).parent()), indexToText(secondParent)); + QVERIFY(indexToText(pm.index(0, 5)).startsWith(QLatin1String("0,5,"))); + QCOMPARE(indexToText(pm.index(0, 5, secondParent).parent()), indexToText(secondParent)); + + QCOMPARE(pm.index(0, 0).sibling(0, 4).column(), 4); + QCOMPARE(pm.index(0, 4).sibling(0, 1).column(), 1); + + QVERIFY(!pm.canFetchMore(QModelIndex())); +} + +void StatisticsProxyModelTest::shouldShowToolTip() +{ + // Given a extra-columns proxy with three extra columns + StatisticsProxyModel pm; + pm.setSourceModel(&m_model); + + // When enabling tooltips and getting the tooltip for the first folder + pm.setToolTipEnabled(true); + QString toolTip = pm.index(0, 0).data(Qt::ToolTipRole).toString(); + + // Then the tooltip should contain the expected information + toolTip.remove(QStringLiteral("")); + toolTip.remove(QStringLiteral("")); + QVERIFY2(toolTip.contains(QLatin1String("Total Messages: 6")), qPrintable(toolTip)); + QVERIFY2(toolTip.contains(QLatin1String("Unread Messages: 2")), qPrintable(toolTip)); + QVERIFY2(toolTip.contains(QLatin1String("Storage Size: 9 B")), qPrintable(toolTip)); + QVERIFY2(toolTip.contains(QLatin1String("Subfolder Storage Size: 5 B")), qPrintable(toolTip)); +} + +void StatisticsProxyModelTest::shouldHandleDataChanged() +{ + // Given a extra-columns proxy with three extra columns + StatisticsProxyModel pm; + pm.setSourceModel(&m_model); + QSignalSpy dataChangedSpy(&pm, &QAbstractItemModel::dataChanged); + + // When ETM says the collection changed + m_model.item(0, 0)->setData(QStringLiteral("a"), Qt::EditRole); + + // Then the change should be notified to the proxy -- including the extra columns + QCOMPARE(dataChangedSpy.count(), 1); + QCOMPARE(indexRowCol(dataChangedSpy.at(0).at(0).toModelIndex()), QStringLiteral("0,0")); + QCOMPARE(indexRowCol(dataChangedSpy.at(0).at(1).toModelIndex()), QStringLiteral("0,6")); + QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBCD269 B")); +} + +void StatisticsProxyModelTest::shouldHandleDataChangedInExtraColumn() +{ + // Given a extra-columns proxy with three extra columns + StatisticsProxyModel pm; + pm.setSourceModel(&m_model); + QSignalSpy dataChangedSpy(&pm, &QAbstractItemModel::dataChanged); + + // When the proxy wants to signal a change in an extra column + Akonadi::Collection c1(1); + c1.setName(QStringLiteral("c1")); + c1.setStatistics(makeStats(3, 5, 8)); // changed: unread, count, size in bytes + m_model.item(0, 0)->setData(QVariant::fromValue(c1), Akonadi::EntityTreeModel::CollectionRole); + + // Then the change should be available and notified + QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD358 B")); + QCOMPARE(dataChangedSpy.count(), 1); + QCOMPARE(indexRowCol(dataChangedSpy.at(0).at(0).toModelIndex()), QStringLiteral("0,0")); + QCOMPARE(indexRowCol(dataChangedSpy.at(0).at(1).toModelIndex()), QStringLiteral("0,6")); +} + +#include "statisticsproxymodeltest.moc" + +QTEST_MAIN(StatisticsProxyModelTest) diff --git a/autotests/libs/subscriptiontest.cpp b/autotests/libs/subscriptiontest.cpp new file mode 100644 index 0000000..59551d4 --- /dev/null +++ b/autotests/libs/subscriptiontest.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "control.h" +#include "qtest_akonadi.h" +#include "subscriptionjob_p.h" + +#include + +using namespace Akonadi; + +class SubscriptionTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + } + + void testSubscribe() + { + Collection::List l; + l << Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2/foo2"))); + QVERIFY(l.first().isValid()); + auto sjob = new SubscriptionJob(this); + sjob->unsubscribe(l); + AKVERIFYEXEC(sjob); + + const Collection res2Col = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res2"))); + QVERIFY(res2Col.isValid()); + auto ljob = new CollectionFetchJob(res2Col, CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 1); + + ljob = new CollectionFetchJob(res2Col, CollectionFetchJob::FirstLevel, this); + ljob->fetchScope().setListFilter(CollectionFetchScope::NoFilter); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 2); + + sjob = new SubscriptionJob(this); + sjob->subscribe(l); + AKVERIFYEXEC(sjob); + + ljob = new CollectionFetchJob(res2Col, CollectionFetchJob::FirstLevel, this); + AKVERIFYEXEC(ljob); + QCOMPARE(ljob->collections().count(), 2); + } + + void testEmptySubscribe() + { + Collection::List l; + auto sjob = new SubscriptionJob(this); + AKVERIFYEXEC(sjob); + } + + void testInvalidSubscribe() + { + Collection::List l; + l << Collection(1); + auto sjob = new SubscriptionJob(this); + sjob->subscribe(l); + l << Collection(INT_MAX); + sjob->unsubscribe(l); + QVERIFY(!sjob->exec()); + } +}; + +QTEST_AKONADIMAIN(SubscriptionTest) + +#include "subscriptiontest.moc" diff --git a/autotests/libs/tagmodeltest.cpp b/autotests/libs/tagmodeltest.cpp new file mode 100644 index 0000000..6b6d31f --- /dev/null +++ b/autotests/libs/tagmodeltest.cpp @@ -0,0 +1,325 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include + +#include "fakemonitor.h" +#include "fakeserverdata.h" +#include "fakesession.h" +#include "modelspy.h" + +#include "tagmodel.h" +#include "tagmodel_p.h" + +static const QString serverContent1 = QStringLiteral( + "- T PLAIN 'Tag 1' 4" + "- - T PLAIN 'Tag 2' 3" + "- - - T PLAIN 'Tag 4' 1" + "- - T PLAIN 'Tag 3' 2" + "- T PLAIN 'Tag 5' 5"); + +class TagModelTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void testInitialFetch(); + + void testTagAdded_data(); + void testTagAdded(); + + void testTagChanged_data(); + void testTagChanged(); + + void testTagRemoved_data(); + void testTagRemoved(); + + void testTagMoved_data(); + void testTagMoved(); + +private: + QPair populateModel(const QString &serverContent) + { + auto const fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + fakeMonitor->setTypeMonitored(Akonadi::Monitor::Tags); + + auto const model = new TagModel(fakeMonitor, this); + + m_modelSpy = new ModelSpy{model, this}; + + auto const serverData = new FakeServerData(model, m_fakeSession, fakeMonitor, this); + serverData->setCommands(FakeJobResponse::interpret(serverData, serverContent)); + + // Give the model a chance to populate + QTest::qWait(100); + return qMakePair(serverData, model); + } + +private: + ModelSpy *m_modelSpy = nullptr; + FakeSession *m_fakeSession = nullptr; + QByteArray m_sessionName; +}; + +QModelIndex firstMatchedIndex(const QAbstractItemModel &model, const QString &pattern) +{ + if (pattern.isEmpty()) { + return {}; + } + const auto list = model.match(model.index(0, 0), Qt::DisplayRole, pattern, 1, Qt::MatchRecursive); + Q_ASSERT(!list.isEmpty()); + return list.first(); +} + +void TagModelTest::initTestCase() +{ + m_sessionName = "TagModelTest fake session"; + m_fakeSession = new FakeSession(m_sessionName, FakeSession::EndJobsImmediately); + m_fakeSession->setAsDefaultSession(); + + qRegisterMetaType("QModelIndex"); +} + +void TagModelTest::cleanupTestCase() +{ + delete m_fakeSession; +} + +void TagModelTest::testInitialFetch() +{ + auto const fakeMonitor = new FakeMonitor(this); + + fakeMonitor->setSession(m_fakeSession); + fakeMonitor->setCollectionMonitored(Collection::root()); + auto const model = new TagModel(fakeMonitor, this); + + auto const serverData = new FakeServerData(model, m_fakeSession, fakeMonitor, this); + const auto initialFetchResponse = FakeJobResponse::interpret(serverData, serverContent1); + serverData->setCommands(initialFetchResponse); + + m_modelSpy = new ModelSpy{model, this}; + m_modelSpy->startSpying(); + + const QList expectedSignals{{RowsAboutToBeInserted, 0, 0}, + {RowsInserted, 0, 0}, + {RowsAboutToBeInserted, 0, 0, QStringLiteral("Tag 1")}, + {RowsInserted, 0, 0, QStringLiteral("Tag 1")}, + {RowsAboutToBeInserted, 1, 1, QStringLiteral("Tag 1")}, + {RowsInserted, 1, 1, QStringLiteral("Tag 1")}, + {RowsAboutToBeInserted, 0, 0, QStringLiteral("Tag 2")}, + {RowsInserted, 0, 0, QStringLiteral("Tag 2")}, + {RowsAboutToBeInserted, 1, 1}, + {RowsInserted, 1, 1}}; + m_modelSpy->setExpectedSignals(expectedSignals); + + // Give the model a chance to run the event loop to process the signals. + QTest::qWait(10); + + // We get all the signals we expected. + QTRY_VERIFY(m_modelSpy->expectedSignals().isEmpty()); + + QTest::qWait(10); + // We didn't get signals we didn't expect. + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagAdded_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("addedTag"); + QTest::addColumn("parentTag"); + + QTest::newRow("add-tag01") << serverContent1 << "new Tag" + << "Tag 1"; + QTest::newRow("add-tag02") << serverContent1 << "new Tag" + << "Tag 2"; + QTest::newRow("add-tag03") << serverContent1 << "new Tag" + << "Tag 3"; + QTest::newRow("add-tag04") << serverContent1 << "new Tag" + << "Tag 4"; + QTest::newRow("add-tag05") << serverContent1 << "new Tag" + << "Tag 5"; +} + +void TagModelTest::testTagAdded() +{ + QFETCH(QString, serverContent); + QFETCH(QString, addedTag); + QFETCH(QString, parentTag); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto parentIndex = firstMatchedIndex(*model, parentTag); + const auto newRow = model->rowCount(parentIndex); + + auto const addCommand = new FakeTagAddedCommand(addedTag, parentTag, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({addCommand}); + + const QList expectedSignals{{RowsAboutToBeInserted, newRow, newRow, parentTag, QVariantList{addedTag}}, + {RowsInserted, newRow, newRow, parentTag, QVariantList{addedTag}}}; + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagChanged_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("tagName"); + + QTest::newRow("change-tag01") << serverContent1 << "Tag 1"; + QTest::newRow("change-tag02") << serverContent1 << "Tag 2"; + QTest::newRow("change-tag03") << serverContent1 << "Tag 3"; + QTest::newRow("change-tag04") << serverContent1 << "Tag 4"; + QTest::newRow("change-tag05") << serverContent1 << "Tag 5"; +} + +void TagModelTest::testTagChanged() +{ + QFETCH(QString, serverContent); + QFETCH(QString, tagName); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto changedIndex = firstMatchedIndex(*model, tagName); + const auto parentTag = changedIndex.parent().data().toString(); + const auto changedRow = changedIndex.row(); + + auto const changeCommand = new FakeTagChangedCommand(tagName, parentTag, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands(QList() << changeCommand); + + const QList expectedSignals{{DataChanged, changedRow, changedRow, parentTag, QVariantList{tagName}}}; + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagRemoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("removedTag"); + + QTest::newRow("remove-tag01") << serverContent1 << "Tag 1"; + QTest::newRow("remove-tag02") << serverContent1 << "Tag 2"; + QTest::newRow("remove-tag03") << serverContent1 << "Tag 3"; + QTest::newRow("remove-tag04") << serverContent1 << "Tag 4"; + QTest::newRow("remove-tag05") << serverContent1 << "Tag 5"; +} + +void TagModelTest::testTagRemoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, removedTag); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto removedIndex = firstMatchedIndex(*model, removedTag); + const auto parentTag = removedIndex.parent().data().toString(); + const auto sourceRow = removedIndex.row(); + + auto const removeCommand = new FakeTagRemovedCommand(removedTag, parentTag, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({removeCommand}); + + const auto removedTagList = QVariantList{removedTag}; + const auto parentTagVariant = parentTag.isEmpty() ? QVariant{} : parentTag; + const QList expectedSignals{ExpectedSignal{RowsAboutToBeRemoved, sourceRow, sourceRow, parentTagVariant, removedTagList}, + ExpectedSignal{RowsRemoved, sourceRow, sourceRow, parentTagVariant, removedTagList}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +void TagModelTest::testTagMoved_data() +{ + QTest::addColumn("serverContent"); + QTest::addColumn("changedTag"); + QTest::addColumn("newParent"); + + QTest::newRow("move-tag01") << serverContent1 << "Tag 1" + << "Tag 5"; + QTest::newRow("move-tag02") << serverContent1 << "Tag 2" + << "Tag 5"; + QTest::newRow("move-tag03") << serverContent1 << "Tag 3" + << "Tag 4"; + QTest::newRow("move-tag04") << serverContent1 << "Tag 4" + << "Tag 1"; + QTest::newRow("move-tag05") << serverContent1 << "Tag 5" + << "Tag 2"; + QTest::newRow("move-tag06") << serverContent1 << "Tag 3" << QString(); + QTest::newRow("move-tag07") << serverContent1 << "Tag 2" << QString(); +} + +void TagModelTest::testTagMoved() +{ + QFETCH(QString, serverContent); + QFETCH(QString, changedTag); + QFETCH(QString, newParent); + + const auto testDrivers = populateModel(serverContent); + auto const serverData = testDrivers.first; + auto const model = testDrivers.second; + + const auto changedIndex = firstMatchedIndex(*model, changedTag); + const auto parentTag = changedIndex.parent().data().toString(); + const auto sourceRow = changedIndex.row(); + + QModelIndex newParentIndex = firstMatchedIndex(*model, newParent); + const auto destRow = model->rowCount(newParentIndex); + + auto const moveCommand = new FakeTagMovedCommand(changedTag, parentTag, newParent, serverData); + + m_modelSpy->startSpying(); + serverData->setCommands({moveCommand}); + + const auto parentTagVariant = parentTag.isEmpty() ? QVariant{} : parentTag; + const auto newParentVariant = newParent.isEmpty() ? QVariant{} : newParent; + const QList expectedSignals{ + {RowsAboutToBeMoved, sourceRow, sourceRow, parentTagVariant, destRow, newParentVariant, QVariantList{changedTag}}, + {RowsMoved, sourceRow, sourceRow, parentTagVariant, destRow, newParentVariant, QVariantList{changedTag}}}; + + m_modelSpy->setExpectedSignals(expectedSignals); + serverData->processNotifications(); + + // Give the model a change to run the event loop to process the signals. + QTest::qWait(0); + + QVERIFY(m_modelSpy->isEmpty()); +} + +#include "tagmodeltest.moc" + +QTEST_MAIN(TagModelTest) diff --git a/autotests/libs/tagsynctest.cpp b/autotests/libs/tagsynctest.cpp new file mode 100644 index 0000000..7e59859 --- /dev/null +++ b/autotests/libs/tagsynctest.cpp @@ -0,0 +1,251 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagsync.h" +#include "agentinstance.h" +#include "agentmanager.h" +#include "control.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "resourceselectjob_p.h" +#include "tag.h" +#include "tagcreatejob.h" +#include "tagdeletejob.h" +#include "tagfetchjob.h" +#include "tagfetchscope.h" + +#include "qtest_akonadi.h" + +#include + +using namespace Akonadi; + +class TagSyncTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + Control::start(); + AkonadiTest::setAllResourcesOffline(); + cleanTags(); + } + + Tag::List getTags() + { + auto fetchJob = new TagFetchJob(); + fetchJob->fetchScope().setFetchRemoteId(true); + bool ret = fetchJob->exec(); + Q_ASSERT(ret); + return fetchJob->tags(); + } + + Tag::List getTagsWithRid() + { + Tag::List tags; + Q_FOREACH (const Tag &t, getTags()) { + if (!t.remoteId().isEmpty()) { + tags << t; + qDebug() << t.remoteId(); + } + } + return tags; + } + + void cleanTags() + { + Q_FOREACH (const Tag &t, getTags()) { + auto job = new TagDeleteJob(t); + bool ret = job->exec(); + Q_ASSERT(ret); + } + } + + void newTag() + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag::List remoteTags; + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + remoteTags << tag1; + + auto syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(QHash()); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + cleanTags(); + } + + void newTagWithItems() + { + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_2")); + AKVERIFYEXEC(select); + + Tag::List remoteTags; + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + remoteTags << tag1; + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setRemoteId(QStringLiteral("item1")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + QHash tagMembers; + tagMembers.insert(QString::fromLatin1(tag1.remoteId()), {item1}); + + auto syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(tagMembers); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + + // We need the id of the fetch + tag1 = resultTags.first(); + + auto fetchJob = new ItemFetchJob(tag1); + fetchJob->fetchScope().setFetchTags(true); + fetchJob->fetchScope().tagFetchScope().setFetchRemoteId(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().count(), tagMembers.value(QString::fromLatin1(tag1.remoteId())).count()); + QCOMPARE(fetchJob->items(), tagMembers.value(QString::fromLatin1(tag1.remoteId()))); + + cleanTags(); + } + + void existingTag() + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + + auto createJob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createJob); + + Tag::List remoteTags; + remoteTags << tag1; + + auto syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(QHash()); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + cleanTags(); + } + + void existingTagWithItems() + { + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_2")); + AKVERIFYEXEC(select); + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + + auto createJob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createJob); + + Tag::List remoteTags; + remoteTags << tag1; + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setRemoteId(QStringLiteral("item1")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + Item item2; + { + item2.setMimeType(QStringLiteral("application/octet-stream")); + item2.setRemoteId(QStringLiteral("item2")); + item2.setTag(tag1); + auto append = new ItemCreateJob(item2, res3, this); + AKVERIFYEXEC(append); + item2 = append->item(); + } + + QHash tagMembers; + tagMembers.insert(QString::fromLatin1(tag1.remoteId()), Item::List() << item1); + + auto syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(tagMembers); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTags(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + { + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().count(), 1); + } + { + auto fetchJob = new ItemFetchJob(item2, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().count(), 0); + } + + cleanTags(); + } + + void removeTag() + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag tag1(QStringLiteral("tag1")); + tag1.setRemoteId("rid1"); + + auto createJob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createJob); + + Tag::List remoteTags; + + auto syncer = new TagSync(this); + syncer->setFullTagList(remoteTags); + syncer->setTagMembers(QHash()); + AKVERIFYEXEC(syncer); + + Tag::List resultTags = getTagsWithRid(); + QCOMPARE(resultTags.count(), remoteTags.count()); + QCOMPARE(resultTags, remoteTags); + cleanTags(); + } +}; + +QTEST_AKONADIMAIN(TagSyncTest) + +#include "tagsynctest.moc" diff --git a/autotests/libs/tagtest.cpp b/autotests/libs/tagtest.cpp new file mode 100644 index 0000000..c88f9ad --- /dev/null +++ b/autotests/libs/tagtest.cpp @@ -0,0 +1,968 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "attributefactory.h" +#include "control.h" +#include "item.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "monitor.h" +#include "qtest_akonadi.h" +#include "resourceselectjob_p.h" +#include "tagattribute.h" +#include "tagcreatejob.h" +#include "tagdeletejob.h" +#include "tagfetchjob.h" +#include "tagfetchscope.h" +#include "tagmodifyjob.h" + +using namespace Akonadi; + +class TagTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testTag(); + void testCreateFetch(); + void testRID(); + void testDelete(); + void testDeleteRIDIsolation(); + void testModify(); + void testModifyFromResource(); + void testCreateMerge(); + void testAttributes(); + void testTagItem(); + void testCreateItem(); + void testCreateItemWithTags(); + void testRIDIsolation(); + void testFetchTagIdWithItem(); + void testFetchFullTagWithItem(); + void testModifyItemWithTagByGID(); + void testModifyItemWithTagByRID(); + void testMonitor(); + void testTagAttributeConfusionBug(); + void testFetchItemsByTag(); + void tagModifyJobShouldOnlySendModifiedAttributes(); +}; + +void TagTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + AkonadiTest::setAllResourcesOffline(); + AttributeFactory::registerAttribute(); + qRegisterMetaType(); + qRegisterMetaType>(); + qRegisterMetaType(); + + // Delete the default Knut tag - it's interfering with this test + auto fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + auto deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testTag() +{ + Tag tag1; + Tag tag2; + + // Invalid tags are equal + QVERIFY(tag1 == tag2); + + // Invalid tags with different GIDs are not equal + tag1.setGid("GID1"); + QVERIFY(tag1 != tag2); + tag2.setGid("GID2"); + QVERIFY(tag1 != tag2); + + // Invalid tags with equal GIDs are equal + tag1.setGid("GID2"); + QVERIFY(tag1 == tag2); + + // Valid tags with different IDs are not equal + tag1 = Tag(1); + tag2 = Tag(2); + QVERIFY(tag1 != tag2); + + // Valid tags with different IDs and equal GIDs are still not equal + tag1.setGid("GID1"); + tag2.setGid("GID1"); + QVERIFY(tag1 != tag2); + + // Valid tags with equal ID are equal regardless of GIDs + tag2 = Tag(1); + tag2.setGid("GID2"); + QVERIFY(tag1 == tag2); +} + +void TagTest::testCreateFetch() +{ + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + + { + auto fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + + auto deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); + AKVERIFYEXEC(deleteJob); + } + + { + auto fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 0); + } +} + +void TagTest::testRID() +{ + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + tag.setRemoteId("rid"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + + { + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().setFetchRemoteId(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid")); + + auto deleteJob = new TagDeleteJob(fetchJob->tags().first(), this); + AKVERIFYEXEC(deleteJob); + } + { + auto select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } +} + +void TagTest::testRIDIsolation() +{ + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + tag.setRemoteId("rid_0"); + + auto createJob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + + qint64 tagId; + { + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().setFetchRemoteId(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().count(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid_0")); + tagId = fetchJob->tags().first().id(); + } + + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_1")); + AKVERIFYEXEC(select); + } + + tag.setRemoteId("rid_1"); + createJob = new TagCreateJob(tag, this); + createJob->setMergeIfExisting(true); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + + { + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().setFetchRemoteId(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().count(), 1); + QCOMPARE(fetchJob->tags().first().gid(), QByteArray("gid")); + QCOMPARE(fetchJob->tags().first().type(), QByteArray("mytype")); + QCOMPARE(fetchJob->tags().first().remoteId(), QByteArray("rid_1")); + + QCOMPARE(fetchJob->tags().first().id(), tagId); + } + + auto deleteJob = new TagDeleteJob(Tag(tagId), this); + AKVERIFYEXEC(deleteJob); + + { + auto select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } +} + +void TagTest::testDelete() +{ + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Monitor::Tags); + QSignalSpy spy(&monitor, &Monitor::tagRemoved); + + Tag tag1; + { + tag1.setGid("tag1"); + auto createjob = new TagCreateJob(tag1, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag1 = createjob->tag(); + } + Tag tag2; + { + tag2.setGid("tag2"); + auto createjob = new TagCreateJob(tag2, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag2 = createjob->tag(); + } + { + auto deleteJob = new TagDeleteJob(tag1, this); + AKVERIFYEXEC(deleteJob); + } + + { + auto fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().gid(), tag2.gid()); + } + { + auto deleteJob = new TagDeleteJob(tag2, this); + AKVERIFYEXEC(deleteJob); + } + + // Collect Remove notification, so that they don't interfere with testDeleteRIDIsolation + QTRY_VERIFY(!spy.isEmpty()); +} + +void TagTest::testDeleteRIDIsolation() +{ + Tag tag; + tag.setGid("gid"); + tag.setType("mytype"); + tag.setRemoteId("rid_0"); + + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + auto createJob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + tag.setId(createJob->tag().id()); + } + + tag.setRemoteId("rid_1"); + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_1")); + AKVERIFYEXEC(select); + + auto createJob = new TagCreateJob(tag, this); + createJob->setMergeIfExisting(true); + AKVERIFYEXEC(createJob); + QVERIFY(createJob->tag().isValid()); + } + + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy signalSpy(monitor.get(), &Monitor::tagRemoved); + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); + + // Other tests notifications might interfere due to notification compression on server + QTRY_VERIFY(signalSpy.count() >= 1); + + Tag removedTag; + while (!signalSpy.isEmpty()) { + const Tag t = signalSpy.takeFirst().takeFirst().value(); + if (t.id() == tag.id()) { + removedTag = t; + break; + } + } + + QVERIFY(removedTag.isValid()); + QVERIFY(removedTag.remoteId().isEmpty()); + + { + auto select = new ResourceSelectJob(QStringLiteral(""), this); + AKVERIFYEXEC(select); + } +} + +void TagTest::testModify() +{ + Tag tag; + { + tag.setGid("gid"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + } + + // We can add an attribute + { + auto attr = tag.attribute(Tag::AddIfMissing); + attr->setDisplayName(QStringLiteral("display name")); + tag.setParent(Tag(0)); + tag.setType("mytype"); + auto modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(fetchJob->tags().first().hasAttribute()); + } + // We can update an attribute + { + auto attr = tag.attribute(Tag::AddIfMissing); + attr->setDisplayName(QStringLiteral("display name2")); + auto modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(fetchJob->tags().first().hasAttribute()); + QCOMPARE(fetchJob->tags().first().attribute()->displayName(), attr->displayName()); + } + // We can clear an attribute + { + tag.removeAttribute(); + auto modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(!fetchJob->tags().first().hasAttribute()); + } + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testModifyFromResource() +{ + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + + Tag tag; + { + tag.setGid("gid"); + tag.setRemoteId("rid"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + } + + { + tag.setRemoteId(QByteArray("")); + auto modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + // The tag is removed on the server, because we just removed the last + // RemoteID + auto fetchJob = new TagFetchJob(this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 0); + } +} + +void TagTest::testCreateMerge() +{ + Tag tag; + { + tag.setGid("gid"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + } + { + Tag tag2; + tag2.setGid("gid"); + auto createjob = new TagCreateJob(tag2, this); + createjob->setMergeIfExisting(true); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + QCOMPARE(createjob->tag().id(), tag.id()); + } + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testAttributes() +{ + Tag tag; + { + tag.setGid("gid2"); + auto attr = tag.attribute(Tag::AddIfMissing); + attr->setDisplayName(QStringLiteral("name")); + attr->setInToolbar(true); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag = createjob->tag(); + + { + auto fetchJob = new TagFetchJob(createjob->tag(), this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QVERIFY(fetchJob->tags().first().hasAttribute()); + // we need to clone because the returned attribute is just a reference and destroyed on the next line + // FIXME we should find a better solution for this (like returning a smart pointer or value object) + QScopedPointer tagAttr(fetchJob->tags().first().attribute()->clone()); + QVERIFY(tagAttr); + QCOMPARE(tagAttr->displayName(), QStringLiteral("name")); + QCOMPARE(tagAttr->inToolbar(), true); + } + } + // Try fetching multiple items + Tag tag2; + { + tag2.setGid("gid22"); + TagAttribute *attr = tag.attribute(Tag::AddIfMissing)->clone(); + attr->setDisplayName(QStringLiteral("name2")); + attr->setInToolbar(true); + tag2.addAttribute(attr); + auto createjob = new TagCreateJob(tag2, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + tag2 = createjob->tag(); + + { + auto fetchJob = new TagFetchJob(Tag::List() << tag << tag2, this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 2); + QVERIFY(fetchJob->tags().at(0).hasAttribute()); + QVERIFY(fetchJob->tags().at(1).hasAttribute()); + } + } + + auto deleteJob = new TagDeleteJob(Tag::List() << tag << tag2, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testTagItem() +{ + Akonadi::Monitor monitor; + monitor.itemFetchScope().setFetchTags(true); + monitor.setAllMonitored(true); + QVERIFY(AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady)); + + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + item1.setTag(tag); + + QSignalSpy tagsSpy(&monitor, &Monitor::itemsTagsChanged); + + auto modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + QTRY_VERIFY(tagsSpy.count() >= 1); + QTRY_COMPARE(tagsSpy.last().first().value().first().id(), item1.id()); + QTRY_COMPARE(tagsSpy.last().at(1).value>().size(), 1); // 1 added tag + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testCreateItem() +{ + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setTag(tag); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testCreateItemWithTags() +{ + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag1; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag1 = createjob->tag(); + } + Tag tag2; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid2")), this); + AKVERIFYEXEC(createjob); + tag2 = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setTags({tag1, tag2}); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + auto fetchTags = fetchJob->items().first().tags(); + + std::unique_ptr finally( + new TagDeleteJob({tag1, tag2}, this), + [] (TagDeleteJob *j) + { + j->exec(); + delete j; + }); + QCOMPARE(fetchTags.size(), 2); + QVERIFY(fetchTags.contains(tag1)); + QVERIFY(fetchTags.contains(tag2)); +} + +void TagTest::testFetchTagIdWithItem() +{ + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + item1.setTag(tag); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + fetchJob->fetchScope().tagFetchScope().setFetchIdOnly(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + Tag t = fetchJob->items().first().tags().first(); + QCOMPARE(t.id(), tag.id()); + QVERIFY(t.gid().isEmpty()); + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testFetchFullTagWithItem() +{ + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + // FIXME This should also be possible with create, but isn't + item1.setTag(tag); + } + + auto modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + fetchJob->fetchScope().tagFetchScope().setFetchIdOnly(false); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + Tag t = fetchJob->items().first().tags().first(); + QCOMPARE(t, tag); + QVERIFY(!t.gid().isEmpty()); + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testModifyItemWithTagByGID() +{ + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + { + Tag tag; + tag.setGid("gid2"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + Tag tag; + tag.setGid("gid2"); + item1.setTag(tag); + + auto modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + auto deleteJob = new TagDeleteJob(fetchJob->items().first().tags().first(), this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::testModifyItemWithTagByRID() +{ + { + auto select = new ResourceSelectJob(QStringLiteral("akonadi_knut_resource_0")); + AKVERIFYEXEC(select); + } + + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag3; + { + tag3.setGid("gid3"); + tag3.setRemoteId("rid3"); + auto createjob = new TagCreateJob(tag3, this); + AKVERIFYEXEC(createjob); + tag3 = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + } + + Tag tag; + tag.setRemoteId("rid2"); + item1.setTag(tag); + + auto modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new ItemFetchJob(item1, this); + fetchJob->fetchScope().setFetchTags(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().first().tags().size(), 1); + + { + auto deleteJob = new TagDeleteJob(fetchJob->items().first().tags().first(), this); + AKVERIFYEXEC(deleteJob); + } + + { + auto deleteJob = new TagDeleteJob(tag3, this); + AKVERIFYEXEC(deleteJob); + } + + { + auto select = new ResourceSelectJob(QStringLiteral("")); + AKVERIFYEXEC(select); + } +} + +void TagTest::testMonitor() +{ + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Tags); + monitor.tagFetchScope().fetchAttribute(); + QVERIFY(AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady)); + + Akonadi::Tag createdTag; + { + QSignalSpy addedSpy(&monitor, &Monitor::tagAdded); + QVERIFY(addedSpy.isValid()); + Tag tag; + tag.setGid("gid2"); + tag.setName(QStringLiteral("name2")); + tag.setType("type2"); + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + createdTag = createjob->tag(); + QCOMPARE(createdTag.type(), tag.type()); + QCOMPARE(createdTag.name(), tag.name()); + QCOMPARE(createdTag.gid(), tag.gid()); + // We usually pick up signals from the previous tests as well (due to server-side notification caching) + QTRY_VERIFY(addedSpy.count() >= 1); + QTRY_COMPARE(addedSpy.last().first().value().id(), createdTag.id()); + const auto notifiedTag = addedSpy.last().first().value(); + QCOMPARE(notifiedTag.type(), createdTag.type()); + QCOMPARE(notifiedTag.gid(), createdTag.gid()); + QVERIFY(notifiedTag.hasAttribute()); + QCOMPARE(notifiedTag.name(), createdTag.name()); // requires the TagAttribute + } + + { + QSignalSpy modifiedSpy(&monitor, &Monitor::tagChanged); + QVERIFY(modifiedSpy.isValid()); + createdTag.setName(QStringLiteral("name3")); + + auto modJob = new TagModifyJob(createdTag, this); + AKVERIFYEXEC(modJob); + // We usually pick up signals from the previous tests as well (due to server-side notification caching) + QTRY_VERIFY(modifiedSpy.count() >= 1); + QTRY_COMPARE(modifiedSpy.last().first().value().id(), createdTag.id()); + const auto notifiedTag = modifiedSpy.last().first().value(); + QCOMPARE(notifiedTag.type(), createdTag.type()); + QCOMPARE(notifiedTag.gid(), createdTag.gid()); + QVERIFY(notifiedTag.hasAttribute()); + QCOMPARE(notifiedTag.name(), createdTag.name()); // requires the TagAttribute + } + + { + QSignalSpy removedSpy(&monitor, &Monitor::tagRemoved); + QVERIFY(removedSpy.isValid()); + auto deletejob = new TagDeleteJob(createdTag, this); + AKVERIFYEXEC(deletejob); + QTRY_VERIFY(removedSpy.count() >= 1); + QTRY_COMPARE(removedSpy.last().first().value().id(), createdTag.id()); + const auto notifiedTag = removedSpy.last().first().value(); + QCOMPARE(notifiedTag.type(), createdTag.type()); + QCOMPARE(notifiedTag.gid(), createdTag.gid()); + QVERIFY(notifiedTag.hasAttribute()); + QCOMPARE(notifiedTag.name(), createdTag.name()); // requires the TagAttribute + } +} + +void TagTest::testTagAttributeConfusionBug() +{ + // Create two tags + Tag firstTag; + { + firstTag.setGid("gid"); + firstTag.setName(QStringLiteral("display name")); + auto createjob = new TagCreateJob(firstTag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + firstTag = createjob->tag(); + } + Tag secondTag; + { + secondTag.setGid("AnotherGID"); + secondTag.setName(QStringLiteral("another name")); + auto createjob = new TagCreateJob(secondTag, this); + AKVERIFYEXEC(createjob); + QVERIFY(createjob->tag().isValid()); + secondTag = createjob->tag(); + } + + Akonadi::Monitor monitor; + monitor.setTypeMonitored(Akonadi::Monitor::Tags); + QVERIFY(AkonadiTest::akWaitForSignal(&monitor, &Monitor::monitorReady)); + + const QList firstTagIdList{firstTag.id()}; + + // Modify attribute on the first tag + // and check the notification + { + QSignalSpy modifiedSpy(&monitor, &Akonadi::Monitor::tagChanged); + + firstTag.setName(QStringLiteral("renamed")); + auto modJob = new TagModifyJob(firstTag, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new TagFetchJob(firstTagIdList, this); + QVERIFY(fetchJob->fetchScope().fetchAllAttributes()); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + QCOMPARE(fetchJob->tags().first().name(), firstTag.name()); + + QTRY_VERIFY(modifiedSpy.count() >= 1); + QTRY_COMPARE(modifiedSpy.last().first().value().id(), firstTag.id()); + const auto notifiedTag = modifiedSpy.last().first().value(); + QCOMPARE(notifiedTag.name(), firstTag.name()); + } + + // Cleanup + auto deleteJob = new TagDeleteJob(firstTag, this); + AKVERIFYEXEC(deleteJob); + auto anotherDeleteJob = new TagDeleteJob(secondTag, this); + AKVERIFYEXEC(anotherDeleteJob); +} + +void TagTest::testFetchItemsByTag() +{ + const Collection res3 = Collection(AkonadiTest::collectionIdFromPath(QStringLiteral("res3"))); + Tag tag; + { + auto createjob = new TagCreateJob(Tag(QStringLiteral("gid1")), this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + Item item1; + { + item1.setMimeType(QStringLiteral("application/octet-stream")); + auto append = new ItemCreateJob(item1, res3, this); + AKVERIFYEXEC(append); + item1 = append->item(); + // FIXME This should also be possible with create, but isn't + item1.setTag(tag); + } + + auto modJob = new ItemModifyJob(item1, this); + AKVERIFYEXEC(modJob); + + auto fetchJob = new ItemFetchJob(tag, this); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->items().size(), 1); + Item i = fetchJob->items().first(); + QCOMPARE(i, item1); + + auto deleteJob = new TagDeleteJob(tag, this); + AKVERIFYEXEC(deleteJob); +} + +void TagTest::tagModifyJobShouldOnlySendModifiedAttributes() +{ + // Given a tag with an attribute + Tag tag(QStringLiteral("tagWithAttr")); + auto attr = new Akonadi::TagAttribute; + attr->setDisplayName(QStringLiteral("display name")); + tag.addAttribute(attr); + { + auto createjob = new TagCreateJob(tag, this); + AKVERIFYEXEC(createjob); + tag = createjob->tag(); + } + + // When one job modifies this attribute, and another one does an unrelated modify job + Tag attrModTag(tag.id()); + auto modAttr = attrModTag.attribute(Tag::AddIfMissing); + modAttr->setDisplayName(QStringLiteral("modified")); + auto attrModJob = new TagModifyJob(attrModTag, this); + AKVERIFYEXEC(attrModJob); + + tag.setType(Tag::GENERIC); + // this job shouldn't send the old attribute again + auto modJob = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob); + + // Then the tag should have both the modified attribute and the modified type + { + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().fetchAttribute(); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + const Tag fetchedTag = fetchJob->tags().at(0); + QVERIFY(fetchedTag.hasAttribute()); + QCOMPARE(fetchedTag.attribute()->displayName(), QStringLiteral("modified")); + QCOMPARE(fetchedTag.type(), Tag::GENERIC); + } + + // And when adding a new attribute next to the old one + auto attr2 = AttributeFactory::createAttribute("SecondType"); + tag.addAttribute(attr2); + // this job shouldn't send the old attribute again + auto modJob2 = new TagModifyJob(tag, this); + AKVERIFYEXEC(modJob2); + + // Then the tag should have the modified attribute and the second one + { + auto fetchJob = new TagFetchJob(this); + fetchJob->fetchScope().setFetchAllAttributes(true); + AKVERIFYEXEC(fetchJob); + QCOMPARE(fetchJob->tags().size(), 1); + const Tag fetchedTag = fetchJob->tags().at(0); + QVERIFY(fetchedTag.hasAttribute()); + QCOMPARE(fetchedTag.attribute()->displayName(), QStringLiteral("modified")); + QCOMPARE(fetchedTag.type(), Tag::GENERIC); + QVERIFY(fetchedTag.attribute("SecondType")); + } +} + +#include "tagtest.moc" + +QTEST_AKONADIMAIN(TagTest) diff --git a/autotests/libs/tagtest_simple.cpp b/autotests/libs/tagtest_simple.cpp new file mode 100644 index 0000000..e97fb41 --- /dev/null +++ b/autotests/libs/tagtest_simple.cpp @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2015 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "attributefactory.h" +#include "qtest_akonadi.h" +#include "tag.h" +#include "tagattribute.h" +#include "testattribute.h" + +using namespace Akonadi; + +// Tag tests not requiring a full Akonadi test environment +// this is mainly to test memory management of attributes, so this is best used with valgrind/ASan +class TagTestSimple : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testCustomAttributes(); + void testTagAttribute(); +}; + +void TagTestSimple::testCustomAttributes() +{ + Tag t2; + { + Tag t1; + auto attr = new TestAttribute; + attr->deserialize("hello"); + t1.addAttribute(attr); + t2 = t1; + } + QVERIFY(t2.hasAttribute("EXTRA")); + auto attr = t2.attribute(); + QCOMPARE(attr->serialized(), QByteArray("hello")); +} + +void TagTestSimple::testTagAttribute() +{ + Tag t2; + { + Tag t1; + auto attr = AttributeFactory::createAttribute("TAG"); + t1.addAttribute(attr); + t1.setName(QStringLiteral("hello")); + t2 = t1; + } + QVERIFY(t2.hasAttribute()); + auto attr = t2.attribute(); + QVERIFY(attr); + QCOMPARE(t2.name(), attr->displayName()); +} + +#include "tagtest_simple.moc" + +QTEST_MAIN(TagTestSimple) diff --git a/autotests/libs/test_model_helpers.h b/autotests/libs/test_model_helpers.h new file mode 100644 index 0000000..2f0f508 --- /dev/null +++ b/autotests/libs/test_model_helpers.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2015 Klarälvdalens Datakonsult AB a KDAB Group company, info@kdab.com + SPDX-FileContributor: David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include + +namespace TestModelHelpers +{ +// Prepares one row for a QStandardItemModel +inline QList makeStandardItems(const QStringList &texts) +{ + QList items; + items.reserve(texts.count()); + for (const QString &txt : std::as_const(texts)) { + items << new QStandardItem(txt); + } + return items; +} + +// Extracts a full row from a model as a string +// Works best if every cell contains only one character +inline QString extractRowTexts(QAbstractItemModel *model, int row, const QModelIndex &parent = QModelIndex()) +{ + QString result; + const int colCount = model->columnCount(); + for (int col = 0; col < colCount; ++col) { + const QString txt = model->index(row, col, parent).data().toString(); + result += txt.isEmpty() ? QStringLiteral(" ") : txt; + } + return result; +} + +// Extracts all headers +inline QString extractHorizontalHeaderTexts(QAbstractItemModel *model) +{ + QString result; + const int colCount = model->columnCount(); + for (int col = 0; col < colCount; ++col) { + const QString txt = model->headerData(col, Qt::Horizontal).toString(); + result += txt.isEmpty() ? QStringLiteral(" ") : txt; + } + return result; +} + +inline QString rowSpyToText(const QSignalSpy &spy) +{ + if (!spy.isValid()) { + return QStringLiteral("THE SIGNALSPY IS INVALID!"); + } + QString str; + for (int i = 0; i < spy.count(); ++i) { + str += spy.at(i).at(1).toString() + QLatin1Char(',') + spy.at(i).at(2).toString(); + if (i + 1 < spy.count()) { + str += QLatin1Char(';'); + } + } + return str; +} + +} diff --git a/autotests/libs/testattribute.h b/autotests/libs/testattribute.h new file mode 100644 index 0000000..744a35a --- /dev/null +++ b/autotests/libs/testattribute.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "attribute.h" + +#include + +/* Attribute used for testing by various unit tests. */ +class TestAttribute : public Akonadi::Attribute +{ +public: + TestAttribute() + { + } + QByteArray type() const override + { + return "EXTRA"; + } + QByteArray serialized() const override + { + return data; + } + void deserialize(const QByteArray &ba) override + { + data = ba; + } + TestAttribute *clone() const override + { + auto a = new TestAttribute; + a->data = data; + return a; + } + QByteArray data; +}; + diff --git a/autotests/libs/testenvironmenttest.cpp b/autotests/libs/testenvironmenttest.cpp new file mode 100644 index 0000000..baf72bb --- /dev/null +++ b/autotests/libs/testenvironmenttest.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "qtest_akonadi.h" +#include "servermanager.h" + +#include +#include +#include + +using namespace Akonadi; + +/** + This test verifies that the testrunner set everything up correctly, so all the + other tests work as expected. +*/ +class TestEnvironmentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testDBus() + { + QVERIFY(QDBusConnection::sessionBus().isConnected()); + } + + void testAkonadiServer() + { + QVERIFY(ServerManager::isRunning()); + } + + void testResources() + { + QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered( + ServerManager::agentServiceName(ServerManager::Resource, QStringLiteral("akonadi_knut_resource_0")))); + QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered( + ServerManager::agentServiceName(ServerManager::Resource, QStringLiteral("akonadi_knut_resource_1")))); + QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered( + ServerManager::agentServiceName(ServerManager::Resource, QStringLiteral("akonadi_knut_resource_2")))); + } +}; + +QTEST_AKONADIMAIN(TestEnvironmentTest) + +#include "testenvironmenttest.moc" diff --git a/autotests/libs/testresource/CMakeLists.txt b/autotests/libs/testresource/CMakeLists.txt new file mode 100644 index 0000000..a09a531 --- /dev/null +++ b/autotests/libs/testresource/CMakeLists.txt @@ -0,0 +1,38 @@ +include(${CMAKE_SOURCE_DIR}/KF5AkonadiMacros.cmake) + +kde_enable_exceptions() + +remove_definitions(-DTRANSLATION_DOMAIN=\"libakonadi5\") +add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_knut_resource\") + +# Disabled for now, resourcetester remained in kdepim-runtime +#add_subdirectory( tests ) + +set( knutresource_SRCS knutresource.cpp) + +ecm_qt_declare_logging_category(knutresource_SRCS HEADER knutresource_debug.h IDENTIFIER KNUTRESOURCE_LOG CATEGORY_NAME org.kde.pim.knut) + + +kconfig_add_kcfg_files(knutresource_SRCS settings.kcfgc) + +kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/knutresource.kcfg org.kde.Akonadi.Knut.Settings) + +qt_add_dbus_adaptor(knutresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Knut.Settings.xml settings.h KnutSettings +) + +add_executable(akonadi_knut_resource ${knutresource_SRCS}) +set_target_properties(akonadi_knut_resource PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(akonadi_knut_resource + KF5::AkonadiXml + KF5::AkonadiCore + KF5::KIOCore + KF5::AkonadiAgentBase + Qt::Xml + KF5::I18n +) + +install( TARGETS akonadi_knut_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) +install( FILES knutresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" ) +install( FILES knut-template.xml DESTINATION ${KDE_INSTALL_DATADIR_KF5}/akonadi_knut_resource/ ) diff --git a/autotests/libs/testresource/Info.plist.template b/autotests/libs/testresource/Info.plist.template new file mode 100644 index 0000000..c39ddb9 --- /dev/null +++ b/autotests/libs/testresource/Info.plist.template @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + LSUIElement + 1 + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + diff --git a/autotests/libs/testresource/Messages.sh b/autotests/libs/testresource/Messages.sh new file mode 100644 index 0000000..28b7871 --- /dev/null +++ b/autotests/libs/testresource/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/akonadi_knut_resource.pot diff --git a/autotests/libs/testresource/knut-template.xml b/autotests/libs/testresource/knut-template.xml new file mode 100644 index 0000000..4319a8b --- /dev/null +++ b/autotests/libs/testresource/knut-template.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/autotests/libs/testresource/knutresource.cpp b/autotests/libs/testresource/knutresource.cpp new file mode 100644 index 0000000..643bd1a --- /dev/null +++ b/autotests/libs/testresource/knutresource.cpp @@ -0,0 +1,370 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "knutresource.h" +#include "knutresource_debug.h" +#include "settingsadaptor.h" +#include "xmlreader.h" +#include "xmlwriter.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +KnutResource::KnutResource(const QString &id) + : ResourceBase(id) + , mWatcher(new QFileSystemWatcher(this)) + , mSettings(new KnutSettings()) +{ + changeRecorder()->itemFetchScope().fetchFullPayload(); + changeRecorder()->fetchCollection(true); + + new SettingsAdaptor(mSettings); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings, QDBusConnection::ExportAdaptors); + connect(this, &KnutResource::reloadConfiguration, this, &KnutResource::load); + connect(mWatcher, &QFileSystemWatcher::fileChanged, this, &KnutResource::load); + load(); +} + +KnutResource::~KnutResource() +{ + delete mSettings; +} + +void KnutResource::load() +{ + if (!mWatcher->files().isEmpty()) { + mWatcher->removePaths(mWatcher->files()); + } + + // file loading + QString fileName = mSettings->dataFile(); + if (fileName.isEmpty()) { + Q_EMIT status(Broken, i18n("No data file selected.")); + return; + } + + if (!QFile::exists(fileName)) { + fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/akonadi_knut_resource/knut-template.xml")); + } + + if (!mDocument.loadFile(fileName)) { + Q_EMIT status(Broken, mDocument.lastError()); + return; + } + + if (mSettings->fileWatchingEnabled()) { + mWatcher->addPath(fileName); + } + + Q_EMIT status(Idle, i18n("File '%1' loaded successfully.", fileName)); + synchronize(); +} + +void KnutResource::save() +{ + if (mSettings->readOnly()) { + return; + } + const QString fileName = mSettings->dataFile(); + if (!mDocument.writeToFile(fileName)) { + Q_EMIT error(mDocument.lastError()); + return; + } +} + +void KnutResource::configure(WId windowId) +{ + QString oldFile = mSettings->dataFile(); + if (oldFile.isEmpty()) { + oldFile = QDir::homePath(); + } + + // TODO: Use windowId + Q_UNUSED(windowId) + const QString newFile = + QFileDialog::getSaveFileName(nullptr, + i18n("Select Data File"), + QString(), + QStringLiteral("*.xml |") + i18nc("Filedialog filter for Akonadi data file", "Akonadi Knut Data File")); + + if (newFile.isEmpty() || oldFile == newFile) { + return; + } + + mSettings->setDataFile(newFile); + mSettings->save(); + load(); + + Q_EMIT configurationDialogAccepted(); +} + +void KnutResource::retrieveCollections() +{ + const Collection::List collections = mDocument.collections(); + collectionsRetrieved(collections); + const Tag::List tags = mDocument.tags(); + Q_FOREACH (const Tag &tag, tags) { + auto createjob = new TagCreateJob(tag); + createjob->setMergeIfExisting(true); + } +} + +void KnutResource::retrieveItems(const Akonadi::Collection &collection) +{ + Item::List items = mDocument.items(collection, false); + if (!mDocument.lastError().isEmpty()) { + cancelTask(mDocument.lastError()); + return; + } + + itemsRetrieved(items); +} + +#ifdef DO_IT_THE_OLD_WAY +bool KnutResource::retrieveItem(const Item &item, const QSet &parts) +{ + Q_UNUSED(parts) + + const QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + cancelTask(i18n("No item found for remoteid %1", item.remoteId())); + return false; + } + + Item i = XmlReader::elementToItem(itemElem, true); + i.setId(item.id()); + itemRetrieved(i); + return true; +} +#endif + +bool KnutResource::retrieveItems(const Item::List &items, const QSet &parts) +{ + Q_UNUSED(parts) + + Item::List results; + results.reserve(items.size()); + for (const auto &item : items) { + const QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + cancelTask(i18n("No item found for remoteid %1", item.remoteId())); + return false; + } + + Item i = XmlReader::elementToItem(itemElem, true); + i.setParentCollection(item.parentCollection()); + i.setId(item.id()); + results.push_back(i); + } + + itemsRetrieved(results); + return true; +} + +void KnutResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + QDomElement parentElem = mDocument.collectionElementByRemoteId(parent.remoteId()); + if (parentElem.isNull()) { + Q_EMIT error(i18n("Parent collection not found in DOM tree.")); + changeProcessed(); + return; + } + + Collection c(collection); + c.setRemoteId(QUuid::createUuid().toString()); + if (XmlWriter::writeCollection(c, parentElem).isNull()) { + Q_EMIT error(i18n("Unable to write collection.")); + changeProcessed(); + } else { + save(); + changeCommitted(c); + } +} + +void KnutResource::collectionChanged(const Akonadi::Collection &collection) +{ + QDomElement oldElem = mDocument.collectionElementByRemoteId(collection.remoteId()); + if (oldElem.isNull()) { + Q_EMIT error(i18n("Modified collection not found in DOM tree.")); + changeProcessed(); + return; + } + + QDomElement newElem; + newElem = XmlWriter::collectionToElement(collection, mDocument.document()); + // move all items/collections over to the new node + const QDomNodeList children = oldElem.childNodes(); + const int numberOfChildren = children.count(); + for (int i = 0; i < numberOfChildren; ++i) { + const QDomElement child = children.at(i).toElement(); + qCDebug(KNUTRESOURCE_LOG) << "reparenting " << child.tagName() << child.attribute(QStringLiteral("rid")); + if (child.isNull()) { + continue; + } + if (child.tagName() == QLatin1String("item") || child.tagName() == QStringLiteral("collection")) { + newElem.appendChild(child); // reparents + --i; // children, despite being const is modified by the reparenting + } + } + oldElem.parentNode().replaceChild(newElem, oldElem); + save(); + changeCommitted(collection); +} + +void KnutResource::collectionRemoved(const Akonadi::Collection &collection) +{ + const QDomElement colElem = mDocument.collectionElementByRemoteId(collection.remoteId()); + if (colElem.isNull()) { + Q_EMIT error(i18n("Deleted collection not found in DOM tree.")); + changeProcessed(); + return; + } + + colElem.parentNode().removeChild(colElem); + save(); + changeProcessed(); +} + +void KnutResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + QDomElement parentElem = mDocument.collectionElementByRemoteId(collection.remoteId()); + if (parentElem.isNull()) { + Q_EMIT error(i18n("Parent collection '%1' not found in DOM tree.", collection.remoteId())); + changeProcessed(); + return; + } + + Item i(item); + i.setRemoteId(QUuid::createUuid().toString()); + if (XmlWriter::writeItem(i, parentElem).isNull()) { + Q_EMIT error(i18n("Unable to write item.")); + changeProcessed(); + } else { + save(); + changeCommitted(i); + } +} + +void KnutResource::itemChanged(const Akonadi::Item &item, const QSet &parts) +{ + Q_UNUSED(parts) + + const QDomElement oldElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (oldElem.isNull()) { + Q_EMIT error(i18n("Modified item not found in DOM tree.")); + changeProcessed(); + return; + } + + const QDomElement newElem = XmlWriter::itemToElement(item, mDocument.document()); + oldElem.parentNode().replaceChild(newElem, oldElem); + save(); + changeCommitted(item); +} + +void KnutResource::itemRemoved(const Akonadi::Item &item) +{ + const QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + Q_EMIT error(i18n("Deleted item not found in DOM tree.")); + changeProcessed(); + return; + } + + itemElem.parentNode().removeChild(itemElem); + save(); + changeProcessed(); +} + +void KnutResource::itemMoved(const Item &item, const Collection &collectionSource, const Collection &collectionDestination) +{ + const QDomElement oldElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (oldElem.isNull()) { + qCWarning(KNUTRESOURCE_LOG) << "Moved item not found in DOM tree"; + changeProcessed(); + return; + } + + QDomElement sourceParentElem = mDocument.collectionElementByRemoteId(collectionSource.remoteId()); + if (sourceParentElem.isNull()) { + Q_EMIT error(i18n("Parent collection '%1' not found in DOM tree.", collectionSource.remoteId())); + changeProcessed(); + return; + } + + QDomElement destParentElem = mDocument.collectionElementByRemoteId(collectionDestination.remoteId()); + if (destParentElem.isNull()) { + Q_EMIT error(i18n("Parent collection '%1' not found in DOM tree.", collectionDestination.remoteId())); + changeProcessed(); + return; + } + QDomElement itemElem = mDocument.itemElementByRemoteId(item.remoteId()); + if (itemElem.isNull()) { + Q_EMIT error(i18n("No item found for remoteid %1", item.remoteId())); + } + + sourceParentElem.removeChild(itemElem); + destParentElem.appendChild(itemElem); + + if (XmlWriter::writeItem(item, destParentElem).isNull()) { + Q_EMIT error(i18n("Unable to write item.")); + } else { + save(); + } + changeProcessed(); +} + +QSet KnutResource::parseQuery(const QString &queryString) +{ + QSet resultSet; + Akonadi::SearchQuery query = Akonadi::SearchQuery::fromJSON(queryString.toLatin1()); + foreach (const Akonadi::SearchTerm &term, query.term().subTerms()) { + if (term.key() == QLatin1String("resource")) { + resultSet << term.value().toInt(); + } + } + return resultSet; +} + +void KnutResource::search(const QString &query, const Collection &collection) +{ + Q_UNUSED(collection) + const QVector result = parseQuery(query).values().toVector(); + qCDebug(KNUTRESOURCE_LOG) << "KNUT QUERY:" << query; + qCDebug(KNUTRESOURCE_LOG) << "KNUT RESOURCE:" << result; + searchFinished(result, Akonadi::AgentSearchInterface::Uid); +} + +void KnutResource::addSearch(const QString &query, const QString &queryLanguage, const Collection &resultCollection) +{ + Q_UNUSED(query) + Q_UNUSED(queryLanguage) + Q_UNUSED(resultCollection) + qCDebug(KNUTRESOURCE_LOG) << "addSearch: query=" << query << ", queryLanguage=" << queryLanguage << ", resultCollection=" << resultCollection.id(); +} + +void KnutResource::removeSearch(const Collection &resultCollection) +{ + Q_UNUSED(resultCollection) + qCDebug(KNUTRESOURCE_LOG) << "removeSearch:" << resultCollection.id(); +} + +AKONADI_RESOURCE_MAIN(KnutResource) diff --git a/autotests/libs/testresource/knutresource.desktop b/autotests/libs/testresource/knutresource.desktop new file mode 100644 index 0000000..7607b5b --- /dev/null +++ b/autotests/libs/testresource/knutresource.desktop @@ -0,0 +1,56 @@ +[Desktop Entry] +Name=Knut +Name[ca]=Knut +Name[ca@valencia]=Knut +Name[cs]=Knut +Name[de]=Knut +Name[en_GB]=Knut +Name[es]=Knut +Name[fi]=Knut +Name[it]=Knut +Name[nl]=Knut +Name[pl]=Knut +Name[pt]=Knut +Name[pt_BR]=Knut +Name[ru]=Knut +Name[sk]=Knut +Name[sl]=Knut +Name[sr]=КНУТ +Name[sr@ijekavian]=КНУТ +Name[sr@ijekavianlatin]=KNUT +Name[sr@latin]=KNUT +Name[sv]=Knut +Name[uk]=Knut +Name[x-test]=xxKnutxx +Name[zh_CN]=Knut +Comment=An agent for debugging purpose +Comment[ca]=Un agent per a propòsits de depuració +Comment[ca@valencia]=Un agent per a propòsits de depuració +Comment[cs]=Agent pro ladicí účely +Comment[de]=Ein Agent zur Fehlersuche +Comment[en_GB]=An agent for debugging purpose +Comment[es]=Un agente para el propósito de depuración +Comment[fi]=Virheenpaikannukseen tarkoitettu agentti +Comment[it]=Un agente per scopi di debug +Comment[nl]=Een agent voor debugging-doeleinden +Comment[pl]=Agent na potrzeby diagnostyczne +Comment[pt]=Um agente para fins de depuração +Comment[pt_BR]=Um agente para depuração +Comment[ru]=Агент Akonadi для целей отладки +Comment[sk]=Agent na ladiace účely +Comment[sl]=Posrednik za namene razhroščevanja +Comment[sr]=Агент за потребе исправљања +Comment[sr@ijekavian]=Агент за потребе исправљања +Comment[sr@ijekavianlatin]=Agent za potrebe ispravljanja +Comment[sr@latin]=Agent za potrebe ispravljanja +Comment[sv]=En modul för felsökningssyften +Comment[uk]=Агент для діагностики +Comment[x-test]=xxAn agent for debugging purposexx +Comment[zh_CN]=调试用代理 +Type=AkonadiResource +Exec=akonadi_knut_resource +Icon=tools-report-bug + +X-Akonadi-MimeTypes=text/calendar,text/directory +X-Akonadi-Capabilities=Resource +X-Akonadi-Identifier=akonadi_knut_resource diff --git a/autotests/libs/testresource/knutresource.h b/autotests/libs/testresource/knutresource.h new file mode 100644 index 0000000..3b89411 --- /dev/null +++ b/autotests/libs/testresource/knutresource.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "settings.h" + +class QFileSystemWatcher; + +class KnutResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV2, public Akonadi::AgentSearchInterface +{ + Q_OBJECT + +public: + using Akonadi::AgentBase::ObserverV2::collectionChanged; // So we don't trigger -Woverloaded-virtual + explicit KnutResource(const QString &id); + ~KnutResource() override; + +public Q_SLOTS: + void configure(WId windowId) override; + +protected: + void retrieveCollections() override; + void retrieveItems(const Akonadi::Collection &collection) override; +#ifdef DO_IT_THE_OLD_WAY + bool retrieveItem(const Akonadi::Item &item, const QSet &parts) override; +#endif + bool retrieveItems(const Akonadi::Item::List &items, const QSet &parts) override; + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override; + void collectionChanged(const Akonadi::Collection &collection) override; + void collectionRemoved(const Akonadi::Collection &collection) override; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + void itemChanged(const Akonadi::Item &item, const QSet &parts) override; + void itemRemoved(const Akonadi::Item &ref) override; + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination) override; + + void search(const QString &query, const Akonadi::Collection &collection) override; + void addSearch(const QString &query, const QString &queryLanguage, const Akonadi::Collection &resultCollection) override; + void removeSearch(const Akonadi::Collection &resultCollection) override; + +private: + static QSet parseQuery(const QString &queryString); + +private Q_SLOTS: + void load(); + void save(); + +private: + Akonadi::XmlDocument mDocument; + QFileSystemWatcher *mWatcher = nullptr; + KnutSettings *mSettings = nullptr; +}; + diff --git a/autotests/libs/testresource/knutresource.kcfg b/autotests/libs/testresource/knutresource.kcfg new file mode 100644 index 0000000..fbe4549 --- /dev/null +++ b/autotests/libs/testresource/knutresource.kcfg @@ -0,0 +1,21 @@ + + + + + + + + + + + false + + + true + + + diff --git a/autotests/libs/testresource/settings.kcfgc b/autotests/libs/testresource/settings.kcfgc new file mode 100644 index 0000000..4d50009 --- /dev/null +++ b/autotests/libs/testresource/settings.kcfgc @@ -0,0 +1,8 @@ +File=knutresource.kcfg +ClassName=KnutSettings +Mutators=true +ItemAccessors=true +SetUserTexts=true +Singleton=false +#IncludeFiles= +GlobalEnums=true diff --git a/autotests/libs/testresource/tests/CMakeLists.txt b/autotests/libs/testresource/tests/CMakeLists.txt new file mode 100644 index 0000000..37e23ed --- /dev/null +++ b/autotests/libs/testresource/tests/CMakeLists.txt @@ -0,0 +1,6 @@ +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knut-empty.xml ${CMAKE_CURRENT_BINARY_DIR}/knut-empty.xml COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knut-step1.xml ${CMAKE_CURRENT_BINARY_DIR}/knut-step1.xml COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knut-step2.xml ${CMAKE_CURRENT_BINARY_DIR}/knut-step2.xml COPYONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/knutdemo.xml ${CMAKE_CURRENT_BINARY_DIR}/knutdemo.xml COPYONLY) + +akonadi_add_resourcetest( knutdemo knutdemo.js ) diff --git a/autotests/libs/testresource/tests/knut-empty.xml b/autotests/libs/testresource/tests/knut-empty.xml new file mode 100644 index 0000000..4319a8b --- /dev/null +++ b/autotests/libs/testresource/tests/knut-empty.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/autotests/libs/testresource/tests/knut-step1.xml b/autotests/libs/testresource/tests/knut-step1.xml new file mode 100644 index 0000000..338c69a --- /dev/null +++ b/autotests/libs/testresource/tests/knut-step1.xml @@ -0,0 +1,28 @@ + + + + + From: Volker Krause <vkrause@kde.org> +To: kde-commits@kde.org +Subject: playground/pim/akonaditest/resourcetester +MIME-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 8bit +Date: Sun, 22 Mar 2009 12:50:30 +0000 +Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org> + +SVN commit 942677 by vkrause: + +Add a safety timeout in case we do not receive the synchronized() signal +or the resource hangs during syncing. The first seems to happen randomly +if syncing is extremely fast. + + + M +40 -0 resourcesynchronizationjob.cpp + M +1 -1 resourcesynchronizationjob.h + + + + + diff --git a/autotests/libs/testresource/tests/knut-step2.xml b/autotests/libs/testresource/tests/knut-step2.xml new file mode 100644 index 0000000..d518526 --- /dev/null +++ b/autotests/libs/testresource/tests/knut-step2.xml @@ -0,0 +1,28 @@ + + + + + From: Volker Krause <vkrause@kde.org> +To: kde-commits@kde.org +Subject: playground/pim/akonaditest/resourcetester +MIME-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 8bit +Date: Sun, 22 Mar 2009 12:50:30 +0000 +Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org> + +SVN commit 942677 by vkrause: + +Add a safety timeout in case we do not receive the synchronized() signal +or the resource hangs during syncing. The first seems to happen randomly +if syncing is extremely fast. + + + M +40 -0 resourcesynchronizationjob.cpp + M +1 -1 resourcesynchronizationjob.h + + + + + diff --git a/autotests/libs/testresource/tests/knutdemo.js b/autotests/libs/testresource/tests/knutdemo.js new file mode 100644 index 0000000..607df87 --- /dev/null +++ b/autotests/libs/testresource/tests/knutdemo.js @@ -0,0 +1,65 @@ +Resource.setType( "akonadi_knut_resource" ); + +// read test +Resource.setPathOption( "DataFile", "knutdemo.xml" ); +Resource.setOption( "FileWatchingEnabled", false ); +Resource.create(); + +XmlOperations.setXmlFile( "knutdemo.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + +Resource.destroy(); + +// empty resource +Resource.setPathOption( "DataFile", "newfile.xml" ); +Resource.create(); + +XmlOperations.setXmlFile( "knut-empty.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + +// folder creation +CollectionTest.setParent( "Knut test data" ); +CollectionTest.addContentType( "message/rfc822" ); +CollectionTest.setName( "test folder" ); +CollectionTest.create(); +//Resource.recreate(); + +// item creation +ItemTest.setParentCollection( "Knut test data/test folder" ); +ItemTest.setMimeType( "message/rfc822" ); +ItemTest.setPayloadFromFile( "testmail.mbox" ); +ItemTest.create(); + +Resource.recreate(); + +XmlOperations.setXmlFile( "knut-step1.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.setCollectionKey( "None" ); +XmlOperations.ignoreCollectionField( "RemoteId" ); +XmlOperations.setItemKey( "None" ); +XmlOperations.ignoreItemField( "RemoteId" ); +XmlOperations.assertEqual(); + +// folder modification +CollectionTest.setCollection( "Knut test data/test folder" ); +CollectionTest.setName( "changed folder" ); +CollectionTest.update(); + +Resource.recreate(); + +XmlOperations.setXmlFile( "knut-step2.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + +// folder deletion +CollectionTest.setCollection( "Knut test data/changed folder" ); +CollectionTest.remove(); + +Resource.recreate(); + +XmlOperations.setXmlFile( "knut-empty.xml" ); +XmlOperations.setRootCollections( Resource.identifier() ); +XmlOperations.assertEqual(); + diff --git a/autotests/libs/testresource/tests/knutdemo.xml b/autotests/libs/testresource/tests/knutdemo.xml new file mode 100644 index 0000000..88b75ca --- /dev/null +++ b/autotests/libs/testresource/tests/knutdemo.xml @@ -0,0 +1,72 @@ + + + + ("Posteingang" "mail-folder-inbox") + + + + wcW + + + Subject: Welcome to the Knut resource +To: new-user@this-computer.local +From: knut@your.computer.local +MIME-Version: 1.0 +Content-Type: text/plain +Date: Thu, 01 Jan 2009 15:08:50 +0000 + +This is a mail body + + \SEEN + + + + + + + + + +BEGIN:VCARD +EMAIL:vkrause@kde.org +FN:Volker Krause +GEO:52.500000;13.366667 +N:Krause;Volker;;; +NAME:Volker Krause +ORG:KDE +REV:2003-02-27T20:08:42Z +ROLE:Author of this file +TZ:+02:00 +UID:bb2slGmqxb +URL:http://www.akonadi-project.org +VERSION:3.0 +END:VCARD + + + + + + +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN +VERSION:2.0 +BEGIN:VTODO +DTSTAMP:20090101T154017Z +ORGANIZER:MAILTO:vkrause@kde.org +CREATED:20040505T094143Z +UID:libkcal-1506191911.958 +LAST-MODIFIED:20040512T133925Z +SUMMARY:Add a demo task to this file +PRIORITY:3 +DUE;VALUE=DATE:20090101 +COMPLETED:20090101T133925Z +PERCENT-COMPLETE:100 +END:VTODO +END:VCALENDAR + + + + + + + diff --git a/autotests/libs/testresource/tests/testmail.mbox b/autotests/libs/testresource/tests/testmail.mbox new file mode 100644 index 0000000..f14d60d --- /dev/null +++ b/autotests/libs/testresource/tests/testmail.mbox @@ -0,0 +1,19 @@ +From: Volker Krause +To: kde-commits@kde.org +Subject: playground/pim/akonaditest/resourcetester +MIME-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 8bit +Date: Sun, 22 Mar 2009 12:50:30 +0000 +Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org> + +SVN commit 942677 by vkrause: + +Add a safety timeout in case we do not receive the synchronized() signal +or the resource hangs during syncing. The first seems to happen randomly +if syncing is extremely fast. + + + M +40 -0 resourcesynchronizationjob.cpp + M +1 -1 resourcesynchronizationjob.h diff --git a/autotests/libs/testrunner/CMakeLists.txt b/autotests/libs/testrunner/CMakeLists.txt new file mode 100644 index 0000000..1c68588 --- /dev/null +++ b/autotests/libs/testrunner/CMakeLists.txt @@ -0,0 +1,28 @@ +kde_enable_exceptions() + +set(akonaditest_SRCS + main.cpp + setup.cpp + config.cpp + shellscript.cpp + testrunner.cpp +) + +ecm_qt_declare_logging_category(akonaditest_SRCS HEADER akonaditest_debug.h IDENTIFIER AKONADITEST_LOG CATEGORY_NAME org.kde.pim.akonaditest) + +add_executable(akonaditest ${akonaditest_SRCS}) + +target_link_libraries(akonaditest + KF5::AkonadiPrivate + KF5::AkonadiCore + KF5::I18n + KF5::ConfigCore + Qt::Xml + Qt::DBus + Qt::Widgets +) + +install(TARGETS akonaditest ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + +# Set the akonaditest path (needed by AkonadiMacros.cmake when invoked in kdepimlibs) +set(_akonaditest_DIR ${CMAKE_CURRENT_BINARY_DIR} CACHE PATH "akonaditest path") diff --git a/autotests/libs/testrunner/config.cpp b/autotests/libs/testrunner/config.cpp new file mode 100644 index 0000000..9dac013 --- /dev/null +++ b/autotests/libs/testrunner/config.cpp @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" //krazy:exclude=includes +#include "akonaditest_debug.h" + +#include +#include +#include +#include +#include + +Q_GLOBAL_STATIC(Config, globalConfig) // NOLINT(readability-redundant-member-init) + +Config::Config() +{ +} + +Config::~Config() +{ +} + +Config *Config::instance(const QString &pathToConfig) +{ + globalConfig()->readConfiguration(pathToConfig); + return globalConfig(); +} + +Config *Config::instance() +{ + return globalConfig(); +} + +void Config::readConfiguration(const QString &configfile) +{ + QFile file(configfile); + + if (!file.open(QIODevice::ReadOnly)) { + qFatal("Error reading file %s: %s", qPrintable(configfile), qUtf8Printable(file.errorString())); + } + + mBasePath = QFileInfo(configfile).absolutePath() + QLatin1Char('/'); + qCDebug(AKONADITEST_LOG) << "Base path" << mBasePath; + QXmlStreamReader reader(&file); + + while (!reader.atEnd()) { + reader.readNext(); + if (reader.name() == QLatin1String("config")) { + while (!reader.atEnd() && !(reader.name() == QLatin1String("config") && reader.isEndElement())) { + reader.readNext(); + if (reader.name() == QLatin1String("backends")) { + QStringList backends; + while (!reader.atEnd() && !(reader.name() == QLatin1String("backends") && reader.isEndElement())) { + reader.readNext(); + if (reader.name() == QLatin1String("backend")) { + backends << reader.readElementText(); + } + } + setBackends(backends); + } else if (reader.name() == QLatin1String("datahome")) { + setXdgDataHome(mBasePath + reader.readElementText()); + } else if (reader.name() == QLatin1String("agent")) { + const auto attrs = reader.attributes(); + insertAgent(reader.readElementText(), attrs.value(QLatin1String("synchronize")) == QLatin1String("true")); + } else if (reader.name() == QLatin1String("envvar")) { + const auto attrs = reader.attributes(); + const auto name = attrs.value(QLatin1String("name")); + if (name.isEmpty()) { + qCWarning(AKONADITEST_LOG) << "Given envvar with no name."; + } else { + mEnvVars[name.toString()] = reader.readElementText(); + } + } else if (reader.name() == QLatin1String("dbbackend")) { + setDbBackend(reader.readElementText()); + } + } + } + } +} + +QString Config::xdgDataHome() const +{ + return mXdgDataHome; +} + +QString Config::xdgConfigHome() const +{ + return mXdgConfigHome; +} + +QString Config::basePath() const +{ + return mBasePath; +} + +QStringList Config::backends() const +{ + return mBackends; +} + +QString Config::dbBackend() const +{ + return mDbBackend; +} + +void Config::setXdgDataHome(const QString &dataHome) +{ + const QDir dataHomeDir(dataHome); + mXdgDataHome = dataHomeDir.absolutePath(); +} + +void Config::setXdgConfigHome(const QString &configHome) +{ + const QDir configHomeDir(configHome); + mXdgConfigHome = configHomeDir.absolutePath(); +} + +void Config::setBackends(const QStringList &backends) +{ + mBackends = backends; +} + +bool Config::setDbBackend(const QString &backend) +{ + if (mBackends.isEmpty() || mBackends.contains(backend)) { + mDbBackend = backend; + return true; + } else { + return false; + } +} + +void Config::insertAgent(const QString &agent, bool sync) +{ + mAgents.append(qMakePair(agent, sync)); +} + +QList> Config::agents() const +{ + return mAgents; +} + +QHash Config::envVars() const +{ + return mEnvVars; +} diff --git a/autotests/libs/testrunner/config.h b/autotests/libs/testrunner/config.h new file mode 100644 index 0000000..9a29a75 --- /dev/null +++ b/autotests/libs/testrunner/config.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include +#include +#include + +class Config +{ +public: + Config(); + ~Config(); + static Config *instance(); + static Config *instance(const QString &pathToConfig); + QString xdgDataHome() const; + QString xdgConfigHome() const; + QString basePath() const; + QStringList backends() const; + bool setDbBackend(const QString &backend); + QString dbBackend() const; + QList> agents() const; + QHash envVars() const; + +protected: + void setXdgDataHome(const QString &dataHome); + void setXdgConfigHome(const QString &configHome); + void setBackends(const QStringList &backends); + void insertAgent(const QString &agent, bool sync); + +private: + void readConfiguration(const QString &configFile); + +private: + QString mBasePath; + QString mXdgDataHome; + QString mXdgConfigHome; + QStringList mBackends; + QString mDbBackend; + QList> mAgents; + QHash mEnvVars; +}; + diff --git a/autotests/libs/testrunner/config.xml b/autotests/libs/testrunner/config.xml new file mode 100644 index 0000000..dede7cd --- /dev/null +++ b/autotests/libs/testrunner/config.xml @@ -0,0 +1,7 @@ + + + xdglocal + + QSQLITE3 + + diff --git a/autotests/libs/testrunner/main.cpp b/autotests/libs/testrunner/main.cpp new file mode 100644 index 0000000..3b7e4d1 --- /dev/null +++ b/autotests/libs/testrunner/main.cpp @@ -0,0 +1,139 @@ +/* + * + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "akonaditest_debug.h" +#include "config.h" //krazy:exclude=includes +#include "setup.h" +#include "shellscript.h" +#include "testrunner.h" + +#include + +#include + +#include +#include +#include +#include + +static SetupTest *setup = nullptr; +static TestRunner *runner = nullptr; + +void sigHandler(int signal) +{ + qCCritical(AKONADITEST_LOG, "Received signal %d", signal); + static int sigCounter = 0; + if (sigCounter == 0) { // try clean shutdown + if (runner) { + runner->terminate(); + } + if (setup) { + setup->shutdown(); + } + } else if (sigCounter == 1) { // force shutdown + if (setup) { + setup->shutdownHarder(); + } + } else { // give up and just exit + exit(255); + } + ++sigCounter; +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + app.setQuitLockEnabled(false); + + KAboutData aboutdata(QStringLiteral("akonadi-TES"), + i18n("Akonadi Testing Environment Setup"), + QStringLiteral("1.0"), + i18n("Setup Environment"), + KAboutLicense::GPL, + i18n("(c) 2008 Igor Trindade Oliveira")); + KAboutData::setApplicationData(aboutdata); + + QCommandLineParser parser; + parser.addOption( + {{QStringLiteral("c"), QStringLiteral("config")}, i18n("Configuration file to open"), QStringLiteral("configfile"), QStringLiteral("config.xml")}); + parser.addOption({{QStringLiteral("b"), QStringLiteral("backend")}, i18n("Database backend"), QStringLiteral("backend"), QStringLiteral("sqlite")}); + parser.addOption({QStringList{QStringLiteral("!+[test]")}, i18n("Test to run automatically, interactive if none specified"), QString()}); + parser.addOption({QStringList{QStringLiteral("testenv")}, i18n("Path where testenvironment would be saved"), QStringLiteral("path")}); + + aboutdata.setupCommandLine(&parser); + parser.process(app); + aboutdata.processCommandLine(&parser); + + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(qApp, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(qApp, &QGuiApplication::saveStateRequest, disableSessionManagement); + + if (parser.isSet(QStringLiteral("config"))) { + const auto backend = parser.value(QStringLiteral("backend")); + if (backend != QLatin1String("sqlite") && backend != QLatin1String("mysql") && backend != QLatin1String("pgsql")) { + qCritical("Invalid backend specified. Supported values are: sqlite,mysql,pgsql"); + return 1; + } + + Config::instance(parser.value(QStringLiteral("config"))); + + if (!Config::instance()->setDbBackend(backend)) { + qCritical("Current configuration does not support the selected backend"); + return 1; + } + } + +#ifdef Q_OS_UNIX + signal(SIGINT, sigHandler); + signal(SIGQUIT, sigHandler); +#endif + + setup = new SetupTest(); + + if (!setup->startAkonadiDaemon()) { + delete setup; + qCCritical(AKONADITEST_LOG, "Failed to start Akonadi server!"); + return 1; + } + + ShellScript sh; + sh.setEnvironmentVariables(setup->environmentVariables()); + + if (parser.isSet(QStringLiteral("testenv"))) { + sh.makeShellScript(parser.value(QStringLiteral("testenv"))); + } else { +#ifdef Q_OS_WIN + sh.makeShellScript(setup->basePath() + QLatin1String("testenvironment.ps1")); +#else + sh.makeShellScript(setup->basePath() + QLatin1String("testenvironment.sh")); +#endif + } + + if (!parser.positionalArguments().isEmpty()) { + QStringList testArgs; + for (int i = 0; i < parser.positionalArguments().count(); ++i) { + testArgs << parser.positionalArguments().at(i); + } + runner = new TestRunner(testArgs); + QObject::connect(setup, &SetupTest::setupDone, runner, &TestRunner::run); + QObject::connect(setup, &SetupTest::serverExited, runner, &TestRunner::triggerTermination); + QObject::connect(runner, &TestRunner::finished, setup, &SetupTest::shutdown); + } + + int exitCode = app.exec(); + if (runner) { + exitCode += runner->exitCode(); + delete runner; + } + + delete setup; + setup = nullptr; + + return exitCode; +} diff --git a/autotests/libs/testrunner/setup.cpp b/autotests/libs/testrunner/setup.cpp new file mode 100644 index 0000000..19870a9 --- /dev/null +++ b/autotests/libs/testrunner/setup.cpp @@ -0,0 +1,442 @@ +/* + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * SPDX-FileCopyrightText: 2013 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "setup.h" +#include "akonaditest_debug.h" +#include "config.h" //krazy:exclude=includes + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +bool SetupTest::startAkonadiDaemon() +{ + Q_ASSERT(Akonadi::ServerManager::hasInstanceIdentifier()); + + if (!mAkonadiDaemonProcess) { + mAkonadiDaemonProcess = std::make_unique(); + connect(mAkonadiDaemonProcess.get(), qOverload(&KProcess::finished), this, &SetupTest::slotAkonadiDaemonProcessFinished); + } + + mAkonadiDaemonProcess->setProgram(Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadi_control")), {QStringLiteral("--instance"), instanceId()}); + mAkonadiDaemonProcess->start(); + const bool started = mAkonadiDaemonProcess->waitForStarted(5000); + qCInfo(AKONADITEST_LOG) << "Started akonadi daemon with pid:" << mAkonadiDaemonProcess->processId(); + return started; +} + +void SetupTest::stopAkonadiDaemon() +{ + if (!mAkonadiDaemonProcess) { + return; + } + disconnect(mAkonadiDaemonProcess.get(), qOverload(&KProcess::finished), this, nullptr); + mAkonadiDaemonProcess->terminate(); + const bool finished = mAkonadiDaemonProcess->waitForFinished(5000); + if (!finished) { + qCDebug(AKONADITEST_LOG) << "Problem finishing process."; + } + mAkonadiDaemonProcess.reset(); +} + +void SetupTest::setupAgents() +{ + if (mAgentsCreated) { + return; + } + mAgentsCreated = true; + Config *config = Config::instance(); + const auto agents = config->agents(); + for (const auto &[instance, sync] : agents) { + qCDebug(AKONADITEST_LOG) << "Creating agent" << instance << "..."; + ++mSetupJobCount; + auto job = new Akonadi::AgentInstanceCreateJob(instance, this); + job->setProperty("sync", sync); + connect(job, &Akonadi::AgentInstanceCreateJob::result, this, &SetupTest::agentCreationResult); + job->start(); + } + + checkSetupDone(); +} + +void SetupTest::agentCreationResult(KJob *job) +{ + qCDebug(AKONADITEST_LOG) << "Agent created"; + --mSetupJobCount; + if (job->error()) { + qCritical() << "Failed to create agent:" << job->errorString(); + setupFailed(); + } else { + const bool needsSync = job->property("sync").toBool(); + const auto instance = qobject_cast(job)->instance(); + qCDebug(AKONADITEST_LOG) << "Agent" << instance.identifier() << "created"; + if (needsSync) { + ++mSetupJobCount; + qCDebug(AKONADITEST_LOG) << "Scheduling Agent sync of" << instance.identifier(); + auto sync = new Akonadi::ResourceSynchronizationJob(instance, this); + connect(sync, &Akonadi::ResourceSynchronizationJob::result, this, &SetupTest::synchronizationResult); + sync->start(); + } + } + + checkSetupDone(); +} + +void SetupTest::synchronizationResult(KJob *job) +{ + auto instance = qobject_cast(job)->resource(); + qCDebug(AKONADITEST_LOG) << "Sync of" << instance.identifier() << "done"; + + --mSetupJobCount; + if (job->error()) { + qCritical() << job->errorString(); + setupFailed(); + } + + checkSetupDone(); +} + +void SetupTest::serverStateChanged(Akonadi::ServerManager::State state) +{ + if (state == Akonadi::ServerManager::Running) { + setupAgents(); + } else if (mShuttingDown && state == Akonadi::ServerManager::NotRunning) { + shutdownHarder(); + } +} + +void SetupTest::copyXdgDirectory(const QString &src, const QString &dst) +{ + qCDebug(AKONADITEST_LOG) << "Copying" << src << "to" << dst; + const QDir srcDir(src); + const auto entries = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); + for (const auto &fi : entries) { + if (fi.isDir()) { + if (fi.fileName() == QLatin1String("akonadi")) { + // namespace according to instance identifier +#ifdef Q_OS_WIN + const bool isXdgConfig = src.contains(QLatin1String("/xdgconfig/")); + copyDirectory(fi.absoluteFilePath(), + dst + QStringLiteral("/akonadi/") + (isXdgConfig ? QStringLiteral("config/") : QStringLiteral("data/")) + + QStringLiteral("instance/") + instanceId()); +#else + copyDirectory(fi.absoluteFilePath(), dst + QStringLiteral("/akonadi/instance/") + instanceId()); +#endif + } else { + copyDirectory(fi.absoluteFilePath(), dst + QLatin1Char('/') + fi.fileName()); + } + } else { + if (fi.fileName().startsWith(QLatin1String("akonadi_")) && fi.fileName().endsWith(QLatin1String("rc"))) { + // namespace according to instance identifier + const QString baseName = fi.fileName().left(fi.fileName().size() - 2); + const QString dstPath = dst + QLatin1Char('/') + Akonadi::ServerManager::addNamespace(baseName) + QStringLiteral("rc"); + if (!QFile::copy(fi.absoluteFilePath(), dstPath)) { + qCWarning(AKONADITEST_LOG) << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath; + } + } else { + const QString dstPath = dst + QLatin1Char('/') + fi.fileName(); + if (!QFile::copy(fi.absoluteFilePath(), dstPath)) { + qCWarning(AKONADITEST_LOG) << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath; + } + } + } + } +} + +void SetupTest::copyDirectory(const QString &src, const QString &dst) +{ + const QDir srcDir(src); + QDir::root().mkpath(dst); + const auto entries = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); + for (const auto &fi : entries) { + const QString dstPath = dst + QLatin1Char('/') + fi.fileName(); + if (fi.isDir()) { + copyDirectory(fi.absoluteFilePath(), dstPath); + } else { + if (!QFile::copy(fi.absoluteFilePath(), dstPath)) { + qCWarning(AKONADITEST_LOG) << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath; + } + } + } +} + +void SetupTest::createTempEnvironment() +{ + qCDebug(AKONADITEST_LOG) << "Creating test environment in" << basePath(); + + const Config *config = Config::instance(); +#ifdef Q_OS_WIN + // Always copy the generic xdgconfig dir + copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath()); + if (!config->xdgConfigHome().isEmpty()) { + copyXdgDirectory(config->xdgConfigHome(), basePath()); + } + copyXdgDirectory(config->xdgDataHome(), basePath()); + setEnvironmentVariable("XDG_DATA_HOME", basePath()); + setEnvironmentVariable("XDG_CONFIG_HOME", basePath()); + writeAkonadiserverrc(basePath() + QStringLiteral("/akonadi/config/instance/%1/akonadiserverrc").arg(instanceId())); +#else + const QDir tmpDir(basePath()); + const QString testRunnerDataDir = QStringLiteral("data"); + const QString testRunnerConfigDir = QStringLiteral("config"); + const QString testRunnerTmpDir = QStringLiteral("tmp"); + + tmpDir.mkpath(testRunnerConfigDir); + tmpDir.mkpath(testRunnerDataDir); + tmpDir.mkpath(testRunnerTmpDir); + + // Always copy the generic xdgconfig dir + copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath() + testRunnerConfigDir); + if (!config->xdgConfigHome().isEmpty()) { + copyXdgDirectory(config->xdgConfigHome(), basePath() + testRunnerConfigDir); + } + copyXdgDirectory(config->xdgDataHome(), basePath() + testRunnerDataDir); + + setEnvironmentVariable("XDG_DATA_HOME", basePath() + testRunnerDataDir); + setEnvironmentVariable("XDG_CONFIG_HOME", basePath() + testRunnerConfigDir); + setEnvironmentVariable("TMPDIR", basePath() + testRunnerTmpDir); + writeAkonadiserverrc(basePath() + testRunnerConfigDir + QStringLiteral("/akonadi/instance/%1/akonadiserverrc").arg(instanceId())); +#endif + + QString backend; + if (Config::instance()->dbBackend() == QLatin1String("pgsql")) { + backend = QStringLiteral("postgresql"); + } else { + backend = Config::instance()->dbBackend(); + } + setEnvironmentVariable("TESTRUNNER_DB_ENVIRONMENT", backend); +} + +void SetupTest::writeAkonadiserverrc(const QString &path) +{ + QString backend; + if (Config::instance()->dbBackend() == QLatin1String("sqlite")) { + backend = QStringLiteral("QSQLITE3"); + } else if (Config::instance()->dbBackend() == QLatin1String("mysql")) { + backend = QStringLiteral("QMYSQL"); + } else if (Config::instance()->dbBackend() == QLatin1String("pgsql")) { + backend = QStringLiteral("QPSQL"); + } else { + qCCritical(AKONADITEST_LOG, "Invalid backend name %s", qPrintable(backend)); + return; + } + + QSettings settings(path, QSettings::IniFormat); + settings.beginGroup(QStringLiteral("General")); + settings.setValue(QStringLiteral("Driver"), backend); + settings.endGroup(); + settings.beginGroup(QStringLiteral("Search")); + settings.setValue(QStringLiteral("Manager"), QStringLiteral("Dummy")); + settings.endGroup(); + settings.beginGroup(QStringLiteral("Debug")); + settings.setValue(QStringLiteral("Tracer"), QStringLiteral("null")); + settings.endGroup(); + qCDebug(AKONADITEST_LOG) << "Written akonadiserverrc to" << settings.fileName(); +} + +void SetupTest::cleanTempEnvironment() const +{ +#ifdef Q_OS_WIN + QDir(basePath() + QStringLiteral("akonadi/config/instance/") + instanceId()).removeRecursively(); + QDir(basePath() + QStringLiteral("akonadi/data/instance/") + instanceId()).removeRecursively(); +#else + QDir(basePath()).removeRecursively(); +#endif +} + +SetupTest::SetupTest() + : mAkonadiDaemonProcess(nullptr) + , mShuttingDown(false) + , mAgentsCreated(false) + , mTrackAkonadiProcess(true) + , mSetupJobCount(0) + , mExitCode(0) +{ + setupInstanceId(); + cleanTempEnvironment(); + createTempEnvironment(); + + // switch off agent auto-starting by default, can be re-enabled if really needed inside the config.xml + setEnvironmentVariable("AKONADI_DISABLE_AGENT_AUTOSTART", QStringLiteral("true")); + setEnvironmentVariable("AKONADI_TESTRUNNER_PID", QString::number(QCoreApplication::instance()->applicationPid())); + // enable all debugging, so we get some useful information when test fails + setEnvironmentVariable("QT_LOGGING_RULES", + QStringLiteral("* = true\n" + "qt.* = false\n" + "kf5.coreaddons.desktopparser.debug = false")); + + // avoid KIO starting klauncher which can get the CI stuck + setEnvironmentVariable("KDE_FORK_SLAVES", QStringLiteral("yes")); + setEnvironmentVariable("KIO_DISABLE_CACHE_CLEANER", QStringLiteral("yes")); + + QHashIterator iter(Config::instance()->envVars()); + while (iter.hasNext()) { + iter.next(); + qCDebug(AKONADITEST_LOG) << "Setting environment variable" << iter.key() << "=" << iter.value(); + setEnvironmentVariable(iter.key().toLocal8Bit(), iter.value()); + } + + // No kres-migrator please + KConfig migratorConfig(basePath() + QStringLiteral("config/kres-migratorrc")); + KConfigGroup migrationCfg(&migratorConfig, "Migration"); + migrationCfg.writeEntry("Enabled", false); + + connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, &SetupTest::serverStateChanged); + + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Akonadi.Testrunner-") + + QString::number(QCoreApplication::instance()->applicationPid())); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), this, QDBusConnection::ExportScriptableSlots); +} + +SetupTest::~SetupTest() +{ + cleanTempEnvironment(); +} + +void SetupTest::shutdown() +{ + if (mShuttingDown) { + return; + } + mShuttingDown = true; + + switch (Akonadi::ServerManager::self()->state()) { + case Akonadi::ServerManager::Running: + case Akonadi::ServerManager::Starting: + case Akonadi::ServerManager::Upgrading: + qCInfo(AKONADITEST_LOG) << "Shutting down Akonadi control..."; + Akonadi::ServerManager::self()->stop(); + // safety timeout + QTimer::singleShot(30 * 1000, this, &SetupTest::shutdownHarder); + break; + case Akonadi::ServerManager::NotRunning: + case Akonadi::ServerManager::Broken: + shutdownHarder(); + break; + case Akonadi::ServerManager::Stopping: + // safety timeout + QTimer::singleShot(30 * 1000, this, &SetupTest::shutdownHarder); + break; + } +} + +void SetupTest::shutdownHarder() +{ + qCDebug(AKONADITEST_LOG) << "Forcing akonaditest shutdown"; + mShuttingDown = false; + stopAkonadiDaemon(); + QCoreApplication::instance()->exit(mExitCode); +} + +void SetupTest::restartAkonadiServer() +{ + qCDebug(AKONADITEST_LOG) << "Restarting Akonadi"; + disconnect(mAkonadiDaemonProcess.get(), qOverload(&KProcess::finished), this, nullptr); + Akonadi::ServerManager::self()->stop(); + const bool shutdownResult = mAkonadiDaemonProcess->waitForFinished(); + if (!shutdownResult) { + qCWarning(AKONADITEST_LOG) << "Akonadi control did not shut down in time, killing it."; + mAkonadiDaemonProcess->kill(); + } + // we don't use Control::start() since we want to be able to kill + // it forcefully, if necessary, and know the pid + startAkonadiDaemon(); + // from here on, the server exiting is an error again + connect(mAkonadiDaemonProcess.get(), qOverload(&KProcess::finished), this, &SetupTest::slotAkonadiDaemonProcessFinished); +} + +QString SetupTest::basePath() const +{ +#ifdef Q_OS_WIN + // On Windows we are forced to share the same data directory as production instances + // because there's no way to override QStandardPaths like we can on Unix. + // This means that on Windows we rely on Instances providing us the necessary isolation + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); +#else + QString sysTempDirPath = QDir::tempPath(); +#ifdef Q_OS_UNIX + // QDir::tempPath() makes sure to use the fully sym-link exploded + // absolute path to the temp dir. That is nice, but on OSX it makes + // that path really long. MySQL chokes on this, for it's socket path, + // so work around that + sysTempDirPath = QStringLiteral("/tmp"); +#endif + + const QDir sysTempDir(sysTempDirPath); + const QString tempDir = QStringLiteral("/aktestrunner-%1/").arg(QCoreApplication::instance()->applicationPid()); + if (!sysTempDir.exists(tempDir)) { + sysTempDir.mkdir(tempDir); + } + return sysTempDirPath + tempDir; +#endif +} + +void SetupTest::slotAkonadiDaemonProcessFinished(int exitCode) +{ + if (mTrackAkonadiProcess || exitCode != EXIT_SUCCESS) { + qCWarning(AKONADITEST_LOG) << "Akonadi server process was terminated externally!"; + Q_EMIT serverExited(exitCode); + } + mAkonadiDaemonProcess.reset(); +} + +void SetupTest::trackAkonadiProcess(bool track) +{ + mTrackAkonadiProcess = track; +} + +QString SetupTest::instanceId() const +{ + return QStringLiteral("testrunner-") + QString::number(QCoreApplication::instance()->applicationPid()); +} + +void SetupTest::setupInstanceId() +{ + setEnvironmentVariable("AKONADI_INSTANCE", instanceId()); +} + +void SetupTest::checkSetupDone() +{ + qCDebug(AKONADITEST_LOG) << "checkSetupDone: pendingJobs =" << mSetupJobCount << ", exitCode =" << mExitCode; + if (mSetupJobCount == 0) { + if (mExitCode != 0) { + qCInfo(AKONADITEST_LOG) << "Setup has failed, aborting test."; + shutdown(); + } else { + qCInfo(AKONADITEST_LOG) << "Setup successful"; + Q_EMIT setupDone(); + } + } +} + +void SetupTest::setupFailed() +{ + mExitCode = 1; +} + +void SetupTest::setEnvironmentVariable(const QByteArray &name, const QString &value) +{ + mEnvVars.push_back(qMakePair(name, value.toLocal8Bit())); + qputenv(name.constData(), value.toLatin1()); +} + +QVector SetupTest::environmentVariables() const +{ + return mEnvVars; +} diff --git a/autotests/libs/testrunner/setup.h b/autotests/libs/testrunner/setup.h new file mode 100644 index 0000000..f46600b --- /dev/null +++ b/autotests/libs/testrunner/setup.h @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +class KProcess; +class KJob; + +class SetupTest : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Testrunner") + +public: + SetupTest(); + ~SetupTest(); + + /** + Sets the instance identifier for the Akonadi session. + Call this before using any other Akonadi API! + */ + void setupInstanceId(); + bool startAkonadiDaemon(); + void stopAkonadiDaemon(); + QString basePath() const; + + /// Identifier used for the Akonadi session + QString instanceId() const; + + /// set an environment variable + void setEnvironmentVariable(const QByteArray &name, const QString &value); + + /// retrieve all modified environment variables, for writing the shell script + using EnvVar = QPair; + QVector environmentVariables() const; + +public Q_SLOTS: + Q_SCRIPTABLE void shutdown(); + Q_SCRIPTABLE void shutdownHarder(); + /** Synchronously restarts the server. */ + Q_SCRIPTABLE void restartAkonadiServer(); + Q_SCRIPTABLE void trackAkonadiProcess(bool track); + +Q_SIGNALS: + void setupDone(); + void serverExited(int exitCode); + +private Q_SLOTS: + void serverStateChanged(Akonadi::ServerManager::State state); + void slotAkonadiDaemonProcessFinished(int exitCode); + void agentCreationResult(KJob *job); + void synchronizationResult(KJob *job); + +private: + void setupAgents(); + void copyXdgDirectory(const QString &src, const QString &dst); + void copyDirectory(const QString &src, const QString &dst); + void createTempEnvironment(); + void cleanTempEnvironment() const; + void setupFailed(); + void writeAkonadiserverrc(const QString &path); + void checkSetupDone(); + +private: + std::unique_ptr mAkonadiDaemonProcess; + bool mShuttingDown; + bool mAgentsCreated; + bool mTrackAkonadiProcess; + int mSetupJobCount; + int mExitCode; + QVector mEnvVars; +}; + diff --git a/autotests/libs/testrunner/shellscript.cpp b/autotests/libs/testrunner/shellscript.cpp new file mode 100644 index 0000000..39c5c4b --- /dev/null +++ b/autotests/libs/testrunner/shellscript.cpp @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "shellscript.h" +#include "akonaditest_debug.h" +#include "config.h" //krazy:exclude=includes + +#include +#include +#include + +ShellScript::ShellScript() +{ +} + +void ShellScript::writeEnvironmentVariables() +{ + for (const auto &envvar : std::as_const(mEnvVars)) { +#ifdef Q_OS_WIN + const auto tmpl = QStringLiteral( + "$env:_old_%1=$env:%1\r\n" + "$env:%1=\"%2\"\r\n"); +#else + const auto tmpl = QStringLiteral( + "_old_%1=$%1\n" + "%1=\"%2\"\n" + "export %1\n"); +#endif + mScript += tmpl.arg(QString::fromLocal8Bit(envvar.first), QString::fromLocal8Bit(envvar.second).replace(QLatin1Char('"'), QStringLiteral("\\\""))); + } + +#ifdef Q_OS_WIN + mScript += QStringLiteral("\r\n\r\n"); +#else + mScript += QStringLiteral("\n\n"); +#endif +} + +void ShellScript::writeShutdownFunction() +{ +#ifdef Q_OS_WIN + const auto tmpl = QStringLiteral( + "Function shutdownTestEnvironment()\r\n" + "{\r\n" + " qdbus %1 %2 %3\r\n" + "%4" + "}\r\n\r\n"); + const auto restoreTmpl = QStringLiteral(" $env:%1=$env:_old_%1\r\n"); +#else + const auto tmpl = QStringLiteral( + "function shutdown-testenvironment()\n" + "{\n" + " qdbus %1 %2 %3\n" + "%4" + "}\n\n"); + const auto restoreTmpl = QStringLiteral( + " %1=$_old_%1\n" + " export %1\n"); +#endif + QString restore; + for (const auto &envvar : std::as_const(mEnvVars)) { + restore += restoreTmpl.arg(QString::fromLocal8Bit(envvar.first)); + } + + mScript += tmpl.arg(QStringLiteral("org.kde.Akonadi.Testrunner-%1").arg(qApp->applicationPid()), + QStringLiteral("/"), + QStringLiteral("org.kde.Akonadi.Testrunner.shutdown"), + restore); +} + +void ShellScript::makeShellScript(const QString &fileName) +{ + qCDebug(AKONADITEST_LOG) << "Writing environment shell script to" << fileName; + QFile file(fileName); // can user define the file name/location? + + if (file.open(QIODevice::WriteOnly)) { + writeEnvironmentVariables(); + writeShutdownFunction(); + + file.write(mScript.toLatin1()); + file.close(); + } else { + qCritical() << "Failed to write" << fileName; + } +} + +void ShellScript::setEnvironmentVariables(const QVector &envVars) +{ + mEnvVars = envVars; +} diff --git a/autotests/libs/testrunner/shellscript.h b/autotests/libs/testrunner/shellscript.h new file mode 100644 index 0000000..6ee3314 --- /dev/null +++ b/autotests/libs/testrunner/shellscript.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include +#include +#include + +class ShellScript +{ +public: + ShellScript(); + void makeShellScript(const QString &filename); + + using EnvVar = QPair; + void setEnvironmentVariables(const QVector &envVars); + +private: + void writeEnvironmentVariables(); + void writeShutdownFunction(); + + QString mScript; + QVector mEnvVars; +}; diff --git a/autotests/libs/testrunner/testrunner-config.xsd b/autotests/libs/testrunner/testrunner-config.xsd new file mode 100644 index 0000000..5cf95ce --- /dev/null +++ b/autotests/libs/testrunner/testrunner-config.xsd @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autotests/libs/testrunner/testrunner.cpp b/autotests/libs/testrunner/testrunner.cpp new file mode 100644 index 0000000..afb7f25 --- /dev/null +++ b/autotests/libs/testrunner/testrunner.cpp @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "testrunner.h" +#include "akonaditest_debug.h" + +#include + +TestRunner::TestRunner(const QStringList &args, QObject *parent) + : QObject(parent) + , mArguments(args) + , mExitCode(0) + , mProcess(nullptr) +{ +} + +int TestRunner::exitCode() const +{ + return mExitCode; +} + +void TestRunner::run() +{ + qCDebug(AKONADITEST_LOG) << "Starting test" << mArguments; + mProcess = new KProcess(this); + mProcess->setProgram(mArguments); + connect(mProcess, QOverload::of(&KProcess::finished), this, &TestRunner::processFinished); + connect(mProcess, &KProcess::errorOccurred, this, &TestRunner::processError); + // environment setup seems to have been done by setuptest globally already + mProcess->start(); + if (!mProcess->waitForStarted()) { + qCWarning(AKONADITEST_LOG) << mArguments << "failed to start!"; + mExitCode = 255; + Q_EMIT finished(); + } +} + +void TestRunner::triggerTermination(int exitCode) +{ + processFinished(exitCode); +} + +void TestRunner::processFinished(int exitCode) +{ + // Only update the exit code when it is 0. This prevents overwriting a non-zero + // value with 0. This can happen when multiple processes finish or triggerTermination + // is called after a process has finished. + if (mExitCode == 0) { + mExitCode = exitCode; + qCInfo(AKONADITEST_LOG) << "Test finished with exist code" << exitCode; + } + Q_EMIT finished(); +} + +void TestRunner::processError(QProcess::ProcessError error) +{ + qCWarning(AKONADITEST_LOG) << mArguments << "exited with an error:" << error; + mExitCode = 255; + Q_EMIT finished(); +} + +void TestRunner::terminate() +{ + if (mProcess) { + mProcess->terminate(); + } +} diff --git a/autotests/libs/testrunner/testrunner.h b/autotests/libs/testrunner/testrunner.h new file mode 100644 index 0000000..27284a4 --- /dev/null +++ b/autotests/libs/testrunner/testrunner.h @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include +#include +#include + +class KProcess; + +class TestRunner : public QObject +{ + Q_OBJECT + +public: + explicit TestRunner(const QStringList &args, QObject *parent = nullptr); + int exitCode() const; + void terminate(); + +public Q_SLOTS: + void run(); + void triggerTermination(int); + +Q_SIGNALS: + void finished(); + +private Q_SLOTS: + void processFinished(int exitCode); + void processError(QProcess::ProcessError error); + +private: + QStringList mArguments; + int mExitCode; + KProcess *mProcess = nullptr; +}; + diff --git a/autotests/libs/testsearchplugin/CMakeLists.txt b/autotests/libs/testsearchplugin/CMakeLists.txt new file mode 100644 index 0000000..f9e6c7a --- /dev/null +++ b/autotests/libs/testsearchplugin/CMakeLists.txt @@ -0,0 +1,7 @@ +kde_enable_exceptions() + +add_library(akonadi_test_searchplugin MODULE testsearchplugin.cpp) + +target_link_libraries(akonadi_test_searchplugin KF5::AkonadiCore) + +install( TARGETS akonadi_test_searchplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}/akonadi ) diff --git a/autotests/libs/testsearchplugin/akonadi_test_searchplugin.json b/autotests/libs/testsearchplugin/akonadi_test_searchplugin.json new file mode 100644 index 0000000..d65f0c0 --- /dev/null +++ b/autotests/libs/testsearchplugin/akonadi_test_searchplugin.json @@ -0,0 +1,7 @@ +{ + "X-Akonadi-PluginType": "SearchPlugin", + "X-Akonadi-Library": "akonadi_test_searchplugin", + "X-Akonadi-LoadByDefault": false, + + "X-Akonadi-PluginName": "Akonadi Test Search Plugin" +} diff --git a/autotests/libs/testsearchplugin/testsearchplugin.cpp b/autotests/libs/testsearchplugin/testsearchplugin.cpp new file mode 100644 index 0000000..030e0c3 --- /dev/null +++ b/autotests/libs/testsearchplugin/testsearchplugin.cpp @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2014 Christian Mollekopf + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "testsearchplugin.h" + +#include "searchquery.h" +#include + +QSet TestSearchPlugin::search(const QString &query, const QVector &collections, const QStringList &mimeTypes) +{ + Q_UNUSED(collections) + Q_UNUSED(mimeTypes) + const QSet result = parseQuery(query); + qDebug() << "PLUGIN QUERY:" << query; + qDebug() << "PLUGIN RESULT:" << result; + return parseQuery(query); +} + +QSet TestSearchPlugin::parseQuery(const QString &queryString) +{ + QSet resultSet; + Akonadi::SearchQuery query = Akonadi::SearchQuery::fromJSON(queryString.toLatin1()); + foreach (const Akonadi::SearchTerm &term, query.term().subTerms()) { + if (term.key() == QLatin1String("plugin")) { + resultSet << term.value().toInt(); + } + } + return resultSet; +} diff --git a/autotests/libs/testsearchplugin/testsearchplugin.h b/autotests/libs/testsearchplugin/testsearchplugin.h new file mode 100644 index 0000000..50cc593 --- /dev/null +++ b/autotests/libs/testsearchplugin/testsearchplugin.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "../../../src/server/search/abstractsearchplugin.h" +#include +#include + +class TestSearchPlugin : public QObject, public Akonadi::AbstractSearchPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::AbstractSearchPlugin) + Q_PLUGIN_METADATA(IID "org.kde.akonadi.TestSearchPlugin" FILE "akonadi_test_searchplugin.json") +public: + QSet search(const QString &query, const QVector &collections, const QStringList &mimeTypes) override; + + static QSet parseQuery(const QString &queryString); +}; + diff --git a/autotests/libs/transactiontest.cpp b/autotests/libs/transactiontest.cpp new file mode 100644 index 0000000..63bec0f --- /dev/null +++ b/autotests/libs/transactiontest.cpp @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "transactiontest.h" + +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "control.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "session.h" +#include "transactionjobs.h" + +#include + +using namespace Akonadi; + +QTEST_AKONADIMAIN(TransactionTest) + +void TransactionTest::initTestCase() +{ + AkonadiTest::checkTestIsIsolated(); + Control::start(); +} + +void TransactionTest::testTransaction() +{ + Collection basisCollection; + + auto listJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); + AKVERIFYEXEC(listJob); + const Collection::List list = listJob->collections(); + for (const Collection &col : list) + if (col.name() == QLatin1String("res3")) { + basisCollection = col; + } + + Collection testCollection; + testCollection.setParentCollection(basisCollection); + testCollection.setName(QStringLiteral("transactionTest")); + testCollection.setRemoteId(QStringLiteral("transactionTestRemoteId")); + auto job = new CollectionCreateJob(testCollection, Session::defaultSession()); + + AKVERIFYEXEC(job); + + testCollection = job->collection(); + + auto beginTransaction1 = new TransactionBeginJob(Session::defaultSession()); + AKVERIFYEXEC(beginTransaction1); + + auto beginTransaction2 = new TransactionBeginJob(Session::defaultSession()); + AKVERIFYEXEC(beginTransaction2); + + auto commitTransaction2 = new TransactionCommitJob(Session::defaultSession()); + AKVERIFYEXEC(commitTransaction2); + + auto commitTransaction1 = new TransactionCommitJob(Session::defaultSession()); + AKVERIFYEXEC(commitTransaction1); + + auto commitTransactionX = new TransactionCommitJob(Session::defaultSession()); + QVERIFY(commitTransactionX->exec() == false); + + auto beginTransaction3 = new TransactionBeginJob(Session::defaultSession()); + AKVERIFYEXEC(beginTransaction3); + + Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setPayload("body data"); + auto appendJob = new ItemCreateJob(item, testCollection, Session::defaultSession()); + AKVERIFYEXEC(appendJob); + + auto rollbackTransaction3 = new TransactionRollbackJob(Session::defaultSession()); + AKVERIFYEXEC(rollbackTransaction3); + + auto fetchJob = new ItemFetchJob(testCollection, Session::defaultSession()); + AKVERIFYEXEC(fetchJob); + + QVERIFY(fetchJob->items().isEmpty()); + + auto deleteJob = new CollectionDeleteJob(testCollection, Session::defaultSession()); + AKVERIFYEXEC(deleteJob); +} diff --git a/autotests/libs/transactiontest.h b/autotests/libs/transactiontest.h new file mode 100644 index 0000000..841b4db --- /dev/null +++ b/autotests/libs/transactiontest.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class TransactionTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testTransaction(); +}; + diff --git a/autotests/libs/unittestenv/config.xml b/autotests/libs/unittestenv/config.xml new file mode 100644 index 0000000..2b5dd0b --- /dev/null +++ b/autotests/libs/unittestenv/config.xml @@ -0,0 +1,7 @@ + + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc b/autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc new file mode 100644 index 0000000..c5e90d8 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi-firstrunrc @@ -0,0 +1,4 @@ +[ProcessedDefaults] +defaultaddressbook=done +defaultcalendar=done +defaultnotebook=done diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc new file mode 100644 index 0000000..0d9e3cf --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_0rc @@ -0,0 +1,4 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res1.xml +FileWatchingEnabled=false + diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc new file mode 100644 index 0000000..87df3c6 --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_1rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res2.xml +FileWatchingEnabled=false diff --git a/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc new file mode 100644 index 0000000..274fbfc --- /dev/null +++ b/autotests/libs/unittestenv/xdgconfig/akonadi_knut_resource_2rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res3.xml +FileWatchingEnabled=false diff --git a/autotests/libs/unittestenv/xdglocal/testdata-res1.xml b/autotests/libs/unittestenv/xdglocal/testdata-res1.xml new file mode 100644 index 0000000..db51834 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata-res1.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + testmailbody + From: <test@user.tst> + \SEEN + \FLAGGED + \DRAFT + + + testmailbody1 + From: <test1@user.tst> + \FLAGGED + tagrid + + + testmailbody2 + From: <test2@user.tst> + + + testmailbody3 + From: <test3@user.tst> + + + testmailbody4 + From: <test4@user.tst> + + + testmailbody5 + From: <test5@user.tst> + + + testmailbody6 + From: <test6@user.tst> + + + testmailbody7 + From: <test7@user.tst> + + + testmailbody8 + From: <test8@user.tst> + + + testmailbody9 + From: <test9@user.tst> + + + testmailbody10 + From: <test10@user.tst> + + + testmailbody11 + From: <test11@user.tst> + + + testmailbody12 + From: <test12@user.tst> + + + testmailbody13 + From: <test13@user.tst> + + + testmailbody14 + From: <test14@user.tst> + + + + + diff --git a/autotests/libs/unittestenv/xdglocal/testdata-res2.xml b/autotests/libs/unittestenv/xdglocal/testdata-res2.xml new file mode 100644 index 0000000..b12f3b3 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata-res2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/autotests/libs/unittestenv/xdglocal/testdata-res3.xml b/autotests/libs/unittestenv/xdglocal/testdata-res3.xml new file mode 100644 index 0000000..0c3b7a8 --- /dev/null +++ b/autotests/libs/unittestenv/xdglocal/testdata-res3.xml @@ -0,0 +1,4 @@ + + + + diff --git a/autotests/private/CMakeLists.txt b/autotests/private/CMakeLists.txt new file mode 100644 index 0000000..6b67298 --- /dev/null +++ b/autotests/private/CMakeLists.txt @@ -0,0 +1,33 @@ +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +macro(add_unit_test _source) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} NAME_PREFIX "AkonadiPrivate-" ${_source}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiPrivate-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + target_link_libraries(${_name} + akonadi_shared + akonadiprivate_static + Qt::Network + Qt::Widgets + Qt::Test + ${CMAKE_EXE_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_unit_test(akstandarddirstest.cpp) +add_unit_test(akdbustest.cpp) +add_unit_test(notificationmessagetest.cpp) +add_unit_test(externalpartstoragetest.cpp) +if (NOT MSVC) + # TODO: Make compile on Windows, right now it + # causes some weird linking issues. + add_unit_test(protocoltest.cpp) +endif() +add_unit_test(imapparsertest.cpp) +add_unit_test(imapsettest.cpp) +add_unit_test(compressionstreamtest.cpp) diff --git a/autotests/private/akdbustest.cpp b/autotests/private/akdbustest.cpp new file mode 100644 index 0000000..b27eeae --- /dev/null +++ b/autotests/private/akdbustest.cpp @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include + +#include + +using namespace Akonadi; + +Q_DECLARE_METATYPE(DBus::AgentType) + +class DBusTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testServiceName() + { + akTestSetInstanceIdentifier(QString()); + QCOMPARE(DBus::serviceName(DBus::Server), QLatin1String("org.freedesktop.Akonadi")); + akTestSetInstanceIdentifier(QStringLiteral("foo")); + QCOMPARE(DBus::serviceName(DBus::Server), QLatin1String("org.freedesktop.Akonadi.foo")); + } + + void testParseAgentServiceName_data() + { + QTest::addColumn("instanceId"); + QTest::addColumn("serviceName"); + QTest::addColumn("agentId"); + QTest::addColumn("agentType"); + QTest::addColumn("valid"); + + // generic invalid + QTest::newRow("empty") << QString() << QString() << QString() << DBus::Unknown << false; + QTest::newRow("wrong base") << QString() << "org.freedesktop.Agent.foo" << QString() << DBus::Unknown << false; + QTest::newRow("wrong type") << QString() << "org.freedesktop.Akonadi.Randomizer.akonadi_maildir_resource_0" << QString() << DBus::Unknown << false; + QTest::newRow("too long") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo.bar" << QString() << DBus::Unknown << false; + + // single instance cases + QTest::newRow("agent, no multi-instance") << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0" + << "akonadi_maildir_resource_0" << DBus::Agent << true; + QTest::newRow("resource, no multi-instance") << QString() << "org.freedesktop.Akonadi.Resource.akonadi_maildir_resource_0" + << "akonadi_maildir_resource_0" << DBus::Resource << true; + QTest::newRow("preproc, no multi-instance") << QString() << "org.freedesktop.Akonadi.Preprocessor.akonadi_maildir_resource_0" + << "akonadi_maildir_resource_0" << DBus::Preprocessor << true; + QTest::newRow("multi-instance name in single-instance setup") + << QString() << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo" << QString() << DBus::Unknown << false; + + // multi-instance cases + QTest::newRow("agent, multi-instance") << "foo" + << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo" + << "akonadi_maildir_resource_0" << DBus::Agent << true; + QTest::newRow("resource, multi-instance") << "foo" + << "org.freedesktop.Akonadi.Resource.akonadi_maildir_resource_0.foo" + << "akonadi_maildir_resource_0" << DBus::Resource << true; + QTest::newRow("preproc, multi-instance") << "foo" + << "org.freedesktop.Akonadi.Preprocessor.akonadi_maildir_resource_0.foo" + << "akonadi_maildir_resource_0" << DBus::Preprocessor << true; + QTest::newRow("single-instance name in multi-instance setup") + << "foo" + << "org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0" << QString() << DBus::Unknown << false; + } + + void testParseAgentServiceName() + { + QFETCH(QString, instanceId); + QFETCH(QString, serviceName); + QFETCH(QString, agentId); + QFETCH(DBus::AgentType, agentType); + QFETCH(bool, valid); + + akTestSetInstanceIdentifier(instanceId); + + const auto service = DBus::parseAgentServiceName(serviceName); + QCOMPARE(service.has_value(), valid); + if (service.has_value()) { + QCOMPARE(service->identifier, agentId); + QCOMPARE(service->agentType, agentType); + } + } + + void testAgentServiceName() + { + akTestSetInstanceIdentifier(QString()); + QCOMPARE(DBus::agentServiceName(QStringLiteral("akonadi_maildir_resource_0"), DBus::Agent), + QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0")); + + akTestSetInstanceIdentifier(QStringLiteral("foo")); + QCOMPARE(DBus::agentServiceName(QStringLiteral("akonadi_maildir_resource_0"), DBus::Agent), + QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_maildir_resource_0.foo")); + } + + void testParseInstanceIdentifier_data() + { + QTest::addColumn("serviceName"); + QTest::addColumn("hasInstance"); + + QTest::newRow("server") << QStringLiteral("org.freedesktop.Akonadi") << false; + QTest::newRow("server instance") << QStringLiteral("org.freedesktop.Akonadi.instance") << true; + QTest::newRow("control") << QStringLiteral("org.freedesktop.Akonadi.Control") << false; + QTest::newRow("control instance") << QStringLiteral("org.freedesktop.Akonadi.Control.instance") << true; + QTest::newRow("control lock") << QStringLiteral("org.freedesktop.Akonadi.Control.lock") << false; + QTest::newRow("control lock instance") << QStringLiteral("org.freedesktop.Akonadi.Control.lock.instance") << true; + QTest::newRow("janitor") << QStringLiteral("org.freedesktop.Akonadi.Janitor") << false; + QTest::newRow("janitor instance") << QStringLiteral("org.freedesktop.Akonadi.Janitor.instance") << true; + QTest::newRow("agentserver") << QStringLiteral("org.freedesktop.Akonadi.AgentServer") << false; + QTest::newRow("agentserver instance") << QStringLiteral("org.freedesktop.Akonadi.AgentServer.instance") << true; + QTest::newRow("upgrading") << QStringLiteral("org.freedesktop.Akonadi.upgrading") << false; + QTest::newRow("upgrading instance") << QStringLiteral("org.freedesktop.Akonadi.upgrading.instance") << true; + QTest::newRow("agent") << QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_agent_identifier") << false; + QTest::newRow("agent instance") << QStringLiteral("org.freedesktop.Akonadi.Agent.akonadi_agent_identifier.instance") << true; + QTest::newRow("resource") << QStringLiteral("org.freedesktop.Akonadi.Resource.akonadi_resource_identifier") << false; + QTest::newRow("resource instance") << QStringLiteral("org.freedesktop.Akonadi.Resource.akonadi_resource_identifier.instance") << true; + QTest::newRow("preprocessor") << QStringLiteral("org.freedesktop.Akonadi.Preprocessor.akonadi_preprocessor_identifier") << false; + QTest::newRow("preprocessor instance") << QStringLiteral("org.freedesktop.Akonadi.Preprocessor.akonadi_preprocessor_identifier.instance") << true; + } + + void testParseInstanceIdentifier() + { + QFETCH(QString, serviceName); + QFETCH(bool, hasInstance); + + const auto identifier = DBus::parseInstanceIdentifier(serviceName); + QCOMPARE(identifier.has_value(), hasInstance); + if (hasInstance) { + QCOMPARE(*identifier, QStringLiteral("instance")); + } + } +}; + +AKTEST_MAIN(DBusTest) + +#include "akdbustest.moc" diff --git a/autotests/private/akstandarddirstest.cpp b/autotests/private/akstandarddirstest.cpp new file mode 100644 index 0000000..1c9dbe8 --- /dev/null +++ b/autotests/private/akstandarddirstest.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include +#include + +#define QL1S(x) QStringLiteral(x) + +using namespace Akonadi; + +class AkStandardDirsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCondigFile() + { + akTestSetInstanceIdentifier(QString()); + QVERIFY(StandardDirs::agentsConfigFile(StandardDirs::ReadOnly).endsWith(QL1S("agentsrc"))); + QVERIFY(StandardDirs::agentsConfigFile(StandardDirs::ReadWrite).endsWith(QL1S("agentsrc"))); + QVERIFY(!StandardDirs::agentsConfigFile(StandardDirs::ReadWrite).endsWith(QL1S("foo/agentsrc"))); + + akTestSetInstanceIdentifier(QL1S("foo")); + QVERIFY(StandardDirs::agentsConfigFile(StandardDirs::ReadOnly).endsWith(QL1S("agentsrc"))); + QVERIFY(StandardDirs::agentsConfigFile(StandardDirs::ReadWrite).endsWith(QL1S("instance/foo/agentsrc"))); + } + + void testSaveDir() + { + akTestSetInstanceIdentifier(QString()); + QVERIFY(StandardDirs::saveDir("data").endsWith(QL1S("/akonadi"))); + QVERIFY(!StandardDirs::saveDir("data").endsWith(QL1S("foo/akonadi"))); + + akTestSetInstanceIdentifier(QL1S("foo")); + QVERIFY(StandardDirs::saveDir("data").endsWith(QL1S("/akonadi/instance/foo"))); + } +}; + +AKTEST_MAIN(AkStandardDirsTest) + +#include "akstandarddirstest.moc" diff --git a/autotests/private/compressionstreamtest.cpp b/autotests/private/compressionstreamtest.cpp new file mode 100644 index 0000000..40afbd8 --- /dev/null +++ b/autotests/private/compressionstreamtest.cpp @@ -0,0 +1,152 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "compressionstream_p.h" + +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +class CompressionStreamTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testCompression_data() + { + QTest::addColumn("testData"); + + QTest::newRow("Null") << QByteArray{}; + QTest::newRow("Empty") << QByteArray(""); + QTest::newRow("Hello world") << QByteArray("Hello world"); + } + + void testCompression() + { + QFETCH(QByteArray, testData); + + QByteArray compressedData; + QBuffer compressedBuffer(&compressedData); + compressedBuffer.open(QIODevice::WriteOnly); + + { + CompressionStream stream(&compressedBuffer); + QVERIFY(stream.open(QIODevice::WriteOnly)); + QCOMPARE(stream.write(testData), testData.size()); + stream.close(); + QVERIFY(!stream.error()); + } + + compressedBuffer.close(); + compressedBuffer.open(QIODevice::ReadOnly); + QByteArray decompressedData; + + { + CompressionStream stream(&compressedBuffer); + QVERIFY(stream.open(QIODevice::ReadOnly)); + decompressedData = stream.readAll(); + stream.close(); + QVERIFY(!stream.error()); + } + + QCOMPARE(decompressedData.size(), testData.size()); + QCOMPARE(decompressedData, testData); + } + + void testUnbufferedCompressionOfLargeText() + { + std::array loremIpsum = { + "Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit.", "Integer", "dictum", + "massa", "orci,", "eget", "tempor", "neque", "euismod", "a.", "Suspendisse", "mi", "arcu,", + "facilisis", "eu", "risus", "at,", "varius", "vehicula", "mi.", "Proin", "tristique", "eros", + "nisl,", "vel", "porttitor", "erat", "elementum", "at.", "Quisque", "et", "ex", "id", + "risus", "hendrerit", "rhoncus", "eu", "vel", "enim.", "Vivamus", "at", "lorem", "laoreet", + "ex", "mattis", "feugiat", "vitae", "sit", "amet", "sem.", "Vestibulum", "in", "ante", + "sagittis,", "venenatis", "nibh", "et,", "consectetur", "est.", "Donec", "cursus", "enim", "ac", + "pellentesque", "euismod.", "Nullam", "interdum", "metus", "sed", "blandit", "dapibus.", "Ut", "nec", + "euismod", "magna.", "Aenean", "gravida", "elit", "metus,", "eget", "vehicula", "nibh", "euismod", + "ut.", "Vestibulum", "risus", "lectus,", "molestie", "elementum", "lobortis", "at,", "finibus", "a", + "quam."}; + + QByteArray testData; + QRandomGenerator *generator = QRandomGenerator::system(); + while (testData.size() < 10 * 1024) { + testData += QByteArray(" ") + loremIpsum[generator->bounded(100)].c_str(); + } + + QByteArray compressedData; + QBuffer compressedBuffer(&compressedData); + compressedBuffer.open(QIODevice::WriteOnly); + + { + CompressionStream stream(&compressedBuffer); + QVERIFY(stream.open(QIODevice::WriteOnly | QIODevice::Unbuffered)); + qint64 written = 0; + for (int i = 0; i < testData.size(); ++i) { + written += stream.write(testData.constData() + i, 1); + } + QCOMPARE(written, testData.size()); + stream.close(); + QVERIFY(!stream.error()); + } + + compressedBuffer.close(); + compressedBuffer.open(QIODevice::ReadOnly); + + QByteArray decompressedData; + { + CompressionStream stream(&compressedBuffer); + QVERIFY(stream.open(QIODevice::ReadOnly | QIODevice::Unbuffered)); + while (!stream.atEnd()) { + char buf[3] = {}; + const auto read = stream.read(buf, sizeof(buf)); + decompressedData.append(buf, read); + } + stream.close(); + QVERIFY(!stream.error()); + } + + QCOMPARE(decompressedData.size(), testData.size()); + QCOMPARE(decompressedData, testData); + } + + void testDetection_data() + { + QTest::addColumn>("data"); + QTest::addColumn("isValid"); + + QTest::newRow("Too short - random") << QVector{0x65, 0x66} << false; + QTest::newRow("Too short - valid start") << QVector{0xfd, 0x37, 0x7a, 0x58, 0x5a} << false; + QTest::newRow("Valid magic only") << QVector{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00} << true; + QTest::newRow("Valid input") << QVector{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00, 0x00, 0x04, 0xe6, 0xd6, 0xb4, 0x46, 0x02, 0x00, 0x21, + 0x01, 0x16, 0x00, 0x00, 0x00, 0x74, 0x2f, 0xe5, 0xa3, 0x01, 0x00, 0x01, 0x41, 0x0a, 0x00, + 0x00, 0x00, 0x8f, 0xe8, 0x69, 0xe6, 0x2b, 0x6a, 0xcd, 0x94, 0x00, 0x01, 0x1a, 0x02, 0xdc, + 0x2e, 0xa5, 0x7e, 0x1f, 0xb6, 0xf3, 0x7d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x59, 0x5a} + << true; + } + + void testDetection() + { + QFETCH(QVector, data); + QFETCH(bool, isValid); + + QByteArray ba(reinterpret_cast(data.constData()), data.size()); + QBuffer buffer(&ba); + QVERIFY(buffer.open(QIODevice::ReadOnly)); + + QCOMPARE(CompressionStream::isCompressed(&buffer), isValid); + QCOMPARE(buffer.pos(), 0); + } +}; + +QTEST_GUILESS_MAIN(CompressionStreamTest) + +#include "compressionstreamtest.moc" diff --git a/autotests/private/externalpartstoragetest.cpp b/autotests/private/externalpartstoragetest.cpp new file mode 100644 index 0000000..fafd369 --- /dev/null +++ b/autotests/private/externalpartstoragetest.cpp @@ -0,0 +1,304 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +class ExternalPartStorageTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testResolveAbsolutePath_data(); + void testResolveAbsolutePath(); + + void testUpdateFileNameRevision_data(); + void testUpdateFileNameRevision(); + + void testNameForPartId_data(); + void testNameForPartId(); + + void testPartCreate(); + void testPartUpdate(); + void testPartDelete(); + void testPartCreateTrxRollback(); + void testPartUpdateTrxRollback(); + void testPartDeleteTrxRollback(); + void testPartCreateTrxCommit(); + void testPartUpdateTrxCommit(); + void testPartDeleteTrxCommit(); +}; + +void ExternalPartStorageTest::testResolveAbsolutePath_data() +{ + QTest::addColumn("filename"); + QTest::addColumn("expectedLevelCache"); + QTest::addColumn("isAbsolute"); + + QTest::newRow("1_r0") << QStringLiteral("1_r0") << QStringLiteral("01") << false; + QTest::newRow("23_r0") << QStringLiteral("23_r0") << QStringLiteral("23") << false; + QTest::newRow("567_r0") << QStringLiteral("567_r0") << QStringLiteral("67") << false; + QTest::newRow("123456_r0") << QStringLiteral("123456_r0") << QStringLiteral("56") << false; + QTest::newRow("absolute path") << QStringLiteral("/tmp/akonadi/file_db_data/99_r3") << QString() << true; +} + +void ExternalPartStorageTest::testResolveAbsolutePath() +{ + QFETCH(QString, filename); + QFETCH(QString, expectedLevelCache); + QFETCH(bool, isAbsolute); + + // Calculate the new levelled-cache hierarchy path + const QString expectedBasePath = StandardDirs::saveDir("data", QStringLiteral("file_db_data")) + QDir::separator(); + const QString expectedPath = expectedBasePath + expectedLevelCache + QDir::separator(); + const QString expectedFilePath = expectedPath + filename; + + // File does not exist, will return path for the new levelled hierarchy + // (unless the path is absolute, then it returns the absolute path) + bool exists = false; + QString path = ExternalPartStorage::resolveAbsolutePath(filename, &exists); + QVERIFY(!exists); + if (isAbsolute) { + // Absolute path is no further resolved + QCOMPARE(path, filename); + return; + } + QCOMPARE(path, expectedFilePath); + + // Create the file in the old flat hierarchy + QFile(expectedBasePath + filename).open(QIODevice::WriteOnly); + QCOMPARE(ExternalPartStorage::resolveAbsolutePath(filename, &exists), QString(expectedBasePath + filename)); + QVERIFY(exists); + QVERIFY(QFile::remove(expectedBasePath + filename)); + + // Create the file in the new hierarchy - will return the same as the first + QDir().mkpath(expectedPath); + QFile(expectedFilePath).open(QIODevice::WriteOnly); + exists = false; + // Check that this time we got the new path and exists flag is correctly set + path = ExternalPartStorage::resolveAbsolutePath(filename, &exists); + QCOMPARE(path, expectedFilePath); + QVERIFY(exists); + + // Clean up + QVERIFY(QFile::remove(path)); +} + +void ExternalPartStorageTest::testUpdateFileNameRevision_data() +{ + QTest::addColumn("name"); + QTest::addColumn("expectedName"); + + QTest::newRow("no revision") << QByteArray("1234") << QByteArray("1234_r0"); + QTest::newRow("r0") << QByteArray("1234_r0") << QByteArray("1234_r1"); + QTest::newRow("r12") << QByteArray("1234_r12") << QByteArray("1234_r13"); + QTest::newRow("r123456") << QByteArray("1234_r123456") << QByteArray("1234_r123457"); +} + +void ExternalPartStorageTest::testUpdateFileNameRevision() +{ + QFETCH(QByteArray, name); + QFETCH(QByteArray, expectedName); + + const QByteArray newName = ExternalPartStorage::updateFileNameRevision(name); + QCOMPARE(newName, expectedName); +} + +void ExternalPartStorageTest::testNameForPartId_data() +{ + QTest::addColumn("id"); + QTest::addColumn("expectedName"); + + QTest::newRow("0") << 0LL << QByteArray("0_r0"); + QTest::newRow("12") << 12LL << QByteArray("12_r0"); + QTest::newRow("9876543") << 9876543LL << QByteArray("9876543_r0"); +} + +void ExternalPartStorageTest::testNameForPartId() +{ + QFETCH(qint64, id); + QFETCH(QByteArray, expectedName); + + const QByteArray name = ExternalPartStorage::nameForPartId(id); + QCOMPARE(name, expectedName); +} + +void ExternalPartStorageTest::testPartCreate() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 1, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QFile f(filePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("blabla")); + f.close(); + QVERIFY(f.remove()); +} + +void ExternalPartStorageTest::testPartUpdate() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 10, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + QByteArray newfilename; + QVERIFY(ExternalPartStorage::self()->updatePartFile("newdata", filename, newfilename)); + QCOMPARE(ExternalPartStorage::updateFileNameRevision(filename), newfilename); + const QString newFilePath = ExternalPartStorage::resolveAbsolutePath(newfilename); + QVERIFY(!QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + + QFile f(newFilePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("newdata")); + f.close(); + QVERIFY(f.remove()); +} + +void ExternalPartStorageTest::testPartDelete() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 2, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(ExternalPartStorage::self()->removePartFile(filePath)); + QVERIFY(!QFile::exists(filePath)); +} + +void ExternalPartStorageTest::testPartCreateTrxRollback() +{ + ExternalPartStorageTransaction trx; + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 3, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QFile f(filePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("blabla")); + f.close(); + QVERIFY(trx.rollback()); + QVERIFY(!QFile::exists(filePath)); +} + +void ExternalPartStorageTest::testPartUpdateTrxRollback() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 10, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + + QByteArray newfilename; + QVERIFY(ExternalPartStorage::self()->updatePartFile("newdata", filename, newfilename)); + QCOMPARE(ExternalPartStorage::updateFileNameRevision(filename), newfilename); + const QString newFilePath = ExternalPartStorage::resolveAbsolutePath(newfilename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + + QFile f(newFilePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("newdata")); + f.close(); + + trx.rollback(); + QVERIFY(QFile::exists(filePath)); + QVERIFY(!QFile::exists(newFilePath)); + + QFile f2(filePath); + QVERIFY(f2.open(QIODevice::ReadOnly)); + QCOMPARE(f2.readAll(), QByteArray("blabla")); + f2.close(); + QVERIFY(f2.remove()); +} + +void ExternalPartStorageTest::testPartDeleteTrxRollback() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 4, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + QVERIFY(ExternalPartStorage::self()->removePartFile(filePath)); + QVERIFY(QFile::exists(filePath)); + QVERIFY(trx.rollback()); + QVERIFY(QFile::exists(filePath)); + + QFile::remove(filePath); +} + +void ExternalPartStorageTest::testPartCreateTrxCommit() +{ + ExternalPartStorageTransaction trx; + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 6, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(trx.commit()); + QVERIFY(QFile::exists(filePath)); + + QFile f(filePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("blabla")); + f.close(); + QVERIFY(f.remove()); +} + +void ExternalPartStorageTest::testPartUpdateTrxCommit() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 10, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + + QByteArray newfilename; + QVERIFY(ExternalPartStorage::self()->updatePartFile("newdata", filename, newfilename)); + QCOMPARE(ExternalPartStorage::updateFileNameRevision(filename), newfilename); + const QString newFilePath = ExternalPartStorage::resolveAbsolutePath(newfilename); + QVERIFY(QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + + QFile f(newFilePath); + QVERIFY(f.open(QIODevice::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray("newdata")); + f.close(); + + trx.commit(); + QVERIFY(!QFile::exists(filePath)); + QVERIFY(QFile::exists(newFilePath)); + QVERIFY(QFile::remove(newFilePath)); +} + +void ExternalPartStorageTest::testPartDeleteTrxCommit() +{ + QByteArray filename; + QVERIFY(ExternalPartStorage::self()->createPartFile("blabla", 7, filename)); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(filename); + QVERIFY(QFile::exists(filePath)); + + ExternalPartStorageTransaction trx; + QVERIFY(ExternalPartStorage::self()->removePartFile(filePath)); + QVERIFY(QFile::exists(filePath)); + QVERIFY(trx.commit()); + QVERIFY(!QFile::exists(filePath)); +} + +AKTEST_MAIN(ExternalPartStorageTest) + +#include "externalpartstoragetest.moc" diff --git a/autotests/private/imapparsertest.cpp b/autotests/private/imapparsertest.cpp new file mode 100644 index 0000000..fd36dc4 --- /dev/null +++ b/autotests/private/imapparsertest.cpp @@ -0,0 +1,591 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imapparsertest.h" +#include "private/imapparser_p.h" +#include + +#include + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) + +using namespace Akonadi; + +QTEST_MAIN(ImapParserTest) + +void ImapParserTest::testStripLeadingSpaces() +{ + QByteArray input = " a "; + int result; + + // simple leading spaces at the beginning + result = ImapParser::stripLeadingSpaces(input, 0); + QCOMPARE(result, 2); + + // simple leading spaces in the middle + result = ImapParser::stripLeadingSpaces(input, 1); + QCOMPARE(result, 2); + + // no leading spaces + result = ImapParser::stripLeadingSpaces(input, 2); + QCOMPARE(result, 2); + + // trailing spaces + result = ImapParser::stripLeadingSpaces(input, 3); + QCOMPARE(result, 5); + + // out of bounds access + result = ImapParser::stripLeadingSpaces(input, input.length()); + QCOMPARE(result, input.length()); +} + +void ImapParserTest::testParseQuotedString() +{ + QByteArray input = R"("quoted \"NIL\" string inside")"; + QByteArray result; + int consumed; + + // the whole thing + consumed = ImapParser::parseQuotedString(input, result, 0); + QCOMPARE(result, QByteArray("quoted \"NIL\" string inside")); + QCOMPARE(consumed, 32); + + // unquoted string + consumed = ImapParser::parseQuotedString(input, result, 1); + QCOMPARE(result, QByteArray("quoted")); + QCOMPARE(consumed, 7); + + // whitespaces in quoted string + consumed = ImapParser::parseQuotedString(input, result, 14); + QCOMPARE(result, QByteArray(" string inside")); + QCOMPARE(consumed, 32); + + // whitespaces before unquoted string + consumed = ImapParser::parseQuotedString(input, result, 15); + QCOMPARE(result, QByteArray("string")); + QCOMPARE(consumed, 24); + + // NIL and emptiness tests + input = R"(NIL "NIL" "")"; + + // unquoted NIL + consumed = ImapParser::parseQuotedString(input, result, 0); + QVERIFY(result.isNull()); + QCOMPARE(consumed, 3); + + // quoted NIL + consumed = ImapParser::parseQuotedString(input, result, 3); + QCOMPARE(result, QByteArray("NIL")); + QCOMPARE(consumed, 9); + + // quoted empty string + consumed = ImapParser::parseQuotedString(input, result, 9); + QCOMPARE(result, QByteArray("")); + QCOMPARE(consumed, 12); + + // unquoted string at input end + input = "some string"; + consumed = ImapParser::parseQuotedString(input, result, 4); + QCOMPARE(result, QByteArray("string")); + QCOMPARE(consumed, 11); + + // out of bounds access + consumed = ImapParser::parseQuotedString(input, result, input.length()); + QVERIFY(result.isEmpty()); + QCOMPARE(consumed, input.length()); + + // de-quoting + input = R"("\"some \\ quoted stuff\"")"; + consumed = ImapParser::parseQuotedString(input, result, 0); + QCOMPARE(result, QByteArray("\"some \\ quoted stuff\"")); + QCOMPARE(consumed, input.length()); + + // linebreak as separator + input = "LOGOUT\nFOO"; + consumed = ImapParser::parseQuotedString(input, result, 0); + QCOMPARE(result, QByteArray("LOGOUT")); + QCOMPARE(consumed, 6); +} + +void ImapParserTest::testParseString() +{ + QByteArray input = "\"quoted\" unquoted {7}\nliteral {0}\n empty literal"; + QByteArray result; + int consumed; + + // quoted strings + consumed = ImapParser::parseString(input, result, 0); + QCOMPARE(result, QByteArray("quoted")); + QCOMPARE(consumed, 8); + + // unquoted string + consumed = ImapParser::parseString(input, result, 8); + QCOMPARE(result, QByteArray("unquoted")); + QCOMPARE(consumed, 17); + + // literal string + consumed = ImapParser::parseString(input, result, 17); + QCOMPARE(result, QByteArray("literal")); + QCOMPARE(consumed, 29); + + // empty literal string + consumed = ImapParser::parseString(input, result, 29); + QCOMPARE(result, QByteArray("")); + QCOMPARE(consumed, 34); + + // out of bounds access + consumed = ImapParser::parseString(input, result, input.length()); + QCOMPARE(result, QByteArray()); + QCOMPARE(consumed, input.length()); +} + +void ImapParserTest::testParseParenthesizedList_data() +{ + QTest::addColumn("input"); + QTest::addColumn>("result"); + QTest::addColumn("consumed"); + + QList reference; + + QTest::newRow("null") << QByteArray() << reference << 0; + QTest::newRow("empty") << QByteArray("()") << reference << 2; + QTest::newRow("empty with space") << QByteArray(" ( )") << reference << 4; + QTest::newRow("no list") << QByteArray("some list-less text") << reference << 0; + QTest::newRow("\n") << QByteArray() << reference << 0; + + reference << "entry1"; + reference << "entry2()"; + reference << "(sub list)"; + reference << ")))"; + reference << "entry3"; + QTest::newRow("complex") << QByteArray("(entry1 \"entry2()\" (sub list) \")))\" {6}\nentry3) end") << reference << 47; + + reference.clear(); + reference << "foo"; + reference << "\n\nbar\n"; + reference << "bla"; + QTest::newRow("newline literal") << QByteArray("(foo {6}\n\n\nbar\n bla)") << reference << 20; + + reference.clear(); + reference << "partid"; + reference << "body"; + QTest::newRow("CRLF literal separator") << QByteArray("(partid {4}\r\nbody)") << reference << 18; + + reference.clear(); + reference << "partid"; + reference << "\n\rbody\n\r"; + QTest::newRow("CRLF literal separator 2") << QByteArray("(partid {8}\r\n\n\rbody\n\r)") << reference << 22; + + reference.clear(); + reference << "NAME"; + reference << "net)"; + QTest::newRow("spurious newline") << QByteArray("(NAME \"net)\"\n)") << reference << 14; + + reference.clear(); + reference << "(42 \"net)\")"; + reference << "(0 \"\")"; + QTest::newRow("list of lists") << QByteArray("((42 \"net)\") (0 \"\"))") << reference << 20; +} + +void ImapParserTest::testParseParenthesizedList() +{ + QFETCH(QByteArray, input); + QFETCH(QList, result); + QFETCH(int, consumed); + + QList realResult; + + int reallyConsumed = ImapParser::parseParenthesizedList(input, realResult, 0); + QCOMPARE(realResult, result); + QCOMPARE(reallyConsumed, consumed); + + // briefly also test the other overload + QVarLengthArray realVLAResult; + reallyConsumed = ImapParser::parseParenthesizedList(input, realVLAResult, 0); + QCOMPARE(reallyConsumed, consumed); + + // newline literal (based on itemappendtest bug) + input = "(foo {6}\n\n\nbar\n bla)"; + ImapParser::parseParenthesizedList(input, result); +} + +void ImapParserTest::testParseNumber() +{ + QByteArray input = " 1a23.4"; + qint64 result; + int pos; + bool ok; + + // empty string + pos = ImapParser::parseNumber(QByteArray(), result, &ok); + QCOMPARE(ok, false); + QCOMPARE(pos, 0); + + // leading spaces at the beginning + pos = ImapParser::parseNumber(input, result, &ok, 0); + QCOMPARE(ok, true); + QCOMPARE(pos, 2); + QCOMPARE(result, 1LL); + + // multiple digits + pos = ImapParser::parseNumber(input, result, &ok, 3); + QCOMPARE(ok, true); + QCOMPARE(pos, 5); + QCOMPARE(result, 23LL); + + // number at input end + pos = ImapParser::parseNumber(input, result, &ok, 6); + QCOMPARE(ok, true); + QCOMPARE(pos, 7); + QCOMPARE(result, 4LL); + + // out of bounds access + pos = ImapParser::parseNumber(input, result, &ok, input.length()); + QCOMPARE(ok, false); + QCOMPARE(pos, input.length()); +} + +void ImapParserTest::testQuote_data() +{ + QTest::addColumn("unquoted"); + QTest::addColumn("quoted"); + + QTest::newRow("empty") << QByteArray("") << QByteArray("\"\""); + QTest::newRow("simple") << QByteArray("bla") << QByteArray("\"bla\""); + QTest::newRow("double-quotes") << QByteArray(R"("test"test")") << QByteArray(R"("\"test\"test\"")"); + QTest::newRow("backslash") << QByteArray("\\") << QByteArray(R"("\\")"); + QByteArray binaryNonEncoded; + binaryNonEncoded += '\000'; + QByteArray binaryEncoded("\""); + binaryEncoded += '\000'; + binaryEncoded += '"'; + QTest::newRow("binary") << binaryNonEncoded << binaryEncoded; + + QTest::newRow("LF") << QByteArray("\n") << QByteArray(R"("\n")"); + QTest::newRow("CR") << QByteArray("\r") << QByteArray(R"("\r")"); + QTest::newRow("double quote") << QByteArray("\"") << QByteArray(R"("\"")"); + QTest::newRow("mixed 1") << QByteArray("a\nb\\c") << QByteArray(R"("a\nb\\c")"); + QTest::newRow("mixed 2") << QByteArray("\"a\rb\"") << QByteArray(R"("\"a\rb\"")"); +} + +void ImapParserTest::testQuote() +{ + QFETCH(QByteArray, unquoted); + QFETCH(QByteArray, quoted); + QCOMPARE(ImapParser::quote(unquoted), quoted); +} + +void ImapParserTest::testMessageParser_data() +{ + QTest::addColumn>("input"); + QTest::addColumn("tag"); + QTest::addColumn("data"); + QTest::addColumn("complete"); + QTest::addColumn>("continuations"); + + QList input; + QList continuations; + QTest::newRow("empty") << input << QByteArray() << QByteArray() << false << continuations; + + input << "*"; + QTest::newRow("tag-only") << input << QByteArray("*") << QByteArray() << true << continuations; + + input.clear(); + input << "20 UID FETCH (foo)"; + QTest::newRow("simple") << input << QByteArray("20") << QByteArray("UID FETCH (foo)") << true << continuations; + + input.clear(); + input << "1 (bla (" + << ") blub)"; + QTest::newRow("parenthesis") << input << QByteArray("1") << QByteArray("(bla () blub)") << true << continuations; + + input.clear(); + input << "1 {3}" + << "bla"; + continuations << 0; + QTest::newRow("literal") << input << QByteArray("1") << QByteArray("{3}bla") << true << continuations; + + input.clear(); + input << "1 FETCH (UID 5 DATA {3}" + << "bla" + << " RID 5)"; + QTest::newRow("parenthesisEnclosedLiteral") << input << QByteArray("1") << QByteArray("FETCH (UID 5 DATA {3}bla RID 5)") << true << continuations; + + input.clear(); + input << "1 {3}" + << "bla {4}" + << "blub"; + continuations.clear(); + continuations << 0 << 1; + QTest::newRow("2literal") << input << QByteArray("1") << QByteArray("{3}bla {4}blub") << true << continuations; + + input.clear(); + input << "1 {4}" + << "A{9}"; + continuations.clear(); + continuations << 0; + QTest::newRow("literal in literal") << input << QByteArray("1") << QByteArray("{4}A{9}") << true << continuations; + + input.clear(); + input << "* FETCH (UID 1 DATA {3}" + << "bla" + << " ENVELOPE {4}" + << "blub" + << " RID 5)"; + continuations.clear(); + continuations << 0 << 2; + QTest::newRow("enclosed2literal") << input << QByteArray("*") << QByteArray("FETCH (UID 1 DATA {3}bla ENVELOPE {4}blub RID 5)") << true << continuations; + + input.clear(); + input << "1 DATA {0}"; + continuations.clear(); + QTest::newRow("empty literal") << input << QByteArray("1") << QByteArray("DATA {0}") << true << continuations; +} + +void ImapParserTest::testMessageParser() +{ + QFETCH(QList, input); + QFETCH(QByteArray, tag); + QFETCH(QByteArray, data); + QFETCH(bool, complete); + QFETCH(QList, continuations); + QList cont = continuations; + + auto parser = new ImapParser(); + QVERIFY(parser->tag().isEmpty()); + QVERIFY(parser->data().isEmpty()); + + for (int i = 0; i < input.count(); ++i) { + bool res = parser->parseNextLine(input.at(i)); + if (i != input.count() - 1) { + QVERIFY(!res); + } else { + QCOMPARE(res, complete); + } + if (parser->continuationStarted()) { + QVERIFY(cont.contains(i)); + cont.removeAll(i); + } + } + + QCOMPARE(parser->tag(), tag); + QCOMPARE(parser->data(), data); + QVERIFY(cont.isEmpty()); + + // try again, this time with a not fresh parser + parser->reset(); + QVERIFY(parser->tag().isEmpty()); + QVERIFY(parser->data().isEmpty()); + cont = continuations; + + for (int i = 0; i < input.count(); ++i) { + bool res = parser->parseNextLine(input.at(i)); + if (i != input.count() - 1) { + QVERIFY(!res); + } else { + QCOMPARE(res, complete); + } + if (parser->continuationStarted()) { + QVERIFY(cont.contains(i)); + cont.removeAll(i); + } + } + + QCOMPARE(parser->tag(), tag); + QCOMPARE(parser->data(), data); + QVERIFY(cont.isEmpty()); + + delete parser; +} + +void ImapParserTest::testParseSequenceSet_data() +{ + QTest::addColumn("data"); + QTest::addColumn("begin"); + QTest::addColumn("result"); + QTest::addColumn("end"); + + QByteArray data(" 1 0:* 3:4,8:* *:5,1"); + + QTest::newRow("empty") << QByteArray() << 0 << ImapInterval::List() << 0; + QTest::newRow("input end") << data << 20 << ImapInterval::List() << 20; + + ImapInterval::List result; + result << ImapInterval(1, 1); + QTest::newRow("single value 1") << data << 0 << result << 2; + QTest::newRow("single value 2") << data << 1 << result << 2; + QTest::newRow("single value 3") << data << 19 << result << 20; + + result.clear(); + result << ImapInterval(); + QTest::newRow("full interval") << data << 2 << result << 6; + + result.clear(); + result << ImapInterval(3, 4) << ImapInterval(8); + QTest::newRow("complex 1") << data << 7 << result << 14; + + result.clear(); + result << ImapInterval(0, 5) << ImapInterval(1, 1); + QTest::newRow("complex 2") << data << 14 << result << 20; +} + +void ImapParserTest::testParseSequenceSet() +{ + QFETCH(QByteArray, data); + QFETCH(int, begin); + QFETCH(ImapInterval::List, result); + QFETCH(int, end); + + ImapSet res; + int pos = ImapParser::parseSequenceSet(data, res, begin); + QCOMPARE(res.intervals(), result); + QCOMPARE(pos, end); +} + +void ImapParserTest::testParseDateTime_data() +{ + QTest::addColumn("data"); + QTest::addColumn("begin"); + QTest::addColumn("result"); + QTest::addColumn("end"); + + QTest::newRow("emtpy") << QByteArray() << 0 << QDateTime() << 0; + + QByteArray data(" \"28-May-2006 01:03:35 +0200\""); + QByteArray data2("22-Jul-2008 16:31:48 +0000"); + + QDateTime dt(QDate(2006, 5, 27), QTime(23, 3, 35), Qt::UTC); + QDateTime dt2(QDate(2008, 7, 22), QTime(16, 31, 48), Qt::UTC); + + QTest::newRow("quoted 1") << data << 0 << dt << 29; + QTest::newRow("quoted 2") << data << 1 << dt << 29; + QTest::newRow("unquoted") << data << 2 << dt << 28; + QTest::newRow("unquoted2") << data2 << 0 << dt2 << 26; + QTest::newRow("invalid") << data << 4 << QDateTime() << 4; +} + +void ImapParserTest::testParseDateTime() +{ + QFETCH(QByteArray, data); + QFETCH(int, begin); + QFETCH(QDateTime, result); + QFETCH(int, end); + + QDateTime actualResult; + int actualEnd = ImapParser::parseDateTime(data, actualResult, begin); + QCOMPARE(actualResult, result); + QCOMPARE(actualEnd, end); +} + +void ImapParserTest::testBulkParser_data() +{ + QTest::addColumn("input"); + QTest::addColumn("data"); + + QTest::newRow("empty") << QByteArray("* PRE {0} POST\n") << QByteArray("PRE {0} POST\n"); + QTest::newRow("small block") << QByteArray("* PRE {2}\nXX POST\n") << QByteArray("PRE {2}\nXX POST\n"); + QTest::newRow("small block 2") << QByteArray("* (PRE {2}\nXX\n POST)\n") << QByteArray("(PRE {2}\nXX\n POST)\n"); + QTest::newRow("large block") << QByteArray("* PRE {10}\n0123456789\n") << QByteArray("PRE {10}\n0123456789\n"); + QTest::newRow("store failure") << QByteArray("3 UID STORE (FOO bar ENV {3}\n(a) HEAD {3}\na\n\n BODY {3}\nabc)\n") + << QByteArray("UID STORE (FOO bar ENV {3}\n(a) HEAD {3}\na\n\n BODY {3}\nabc)\n"); +} + +void ImapParserTest::testBulkParser() +{ + QFETCH(QByteArray, input); + QFETCH(QByteArray, data); + + auto parser = new ImapParser(); + QBuffer buffer; + buffer.setData(input); + QVERIFY(buffer.open(QBuffer::ReadOnly)); + + // reading continuation as a single block + forever { + if (buffer.atEnd()) { + break; + } + if (parser->continuationSize() > 0) { + parser->parseBlock(buffer.read(parser->continuationSize())); + } else if (buffer.canReadLine()) { + const QByteArray line = buffer.readLine(); + bool res = parser->parseNextLine(line); + QCOMPARE(res, buffer.atEnd()); + } + } + QCOMPARE(parser->data(), data); + + // reading continuations as smaller blocks + buffer.reset(); + parser->reset(); + forever { + if (buffer.atEnd()) { + break; + } + if (parser->continuationSize() > 4) { + parser->parseBlock(buffer.read(4)); + } else if (parser->continuationSize() > 0) { + parser->parseBlock(buffer.read(parser->continuationSize())); + } else if (buffer.canReadLine()) { + bool res = parser->parseNextLine(buffer.readLine()); + QCOMPARE(res, buffer.atEnd()); + } + } + + delete parser; +} + +void ImapParserTest::testJoin_data() +{ + QTest::addColumn>("list"); + QTest::addColumn("joined"); + QTest::newRow("empty") << QList() << QByteArray(); + QTest::newRow("one") << (QList() << "abab") << QByteArray("abab"); + QTest::newRow("two") << (QList() << "abab" + << "cdcd") + << QByteArray("abab cdcd"); + QTest::newRow("three") << (QList() << "abab" + << "cdcd" + << "efef") + << QByteArray("abab cdcd efef"); +} + +void ImapParserTest::testJoin() +{ + QFETCH(QList, list); + QFETCH(QByteArray, joined); + QCOMPARE(ImapParser::join(list, " "), joined); +} + +void ImapParserTest::benchParseQuotedString_data() +{ + QTest::addColumn("data"); + QTest::addColumn("expectedResult"); + QTest::addColumn("expectedConsumed"); + + QTest::newRow("quoted") << QByteArray("\"foo bar asdf\"") << QByteArray("foo bar asdf") << 14; + QTest::newRow("unquoted") << QByteArray("foo bar asdf") << QByteArray("foo") << 3; +} + +void ImapParserTest::benchParseQuotedString() +{ + QFETCH(QByteArray, data); + QFETCH(QByteArray, expectedResult); + QFETCH(int, expectedConsumed); + + QByteArray result; + QBENCHMARK { + int consumed = ImapParser::parseQuotedString(data, result, 0); + // use data, to prevent it from getting optimized away + if (consumed != expectedConsumed || result != expectedResult) { + // NOTE: don't use QCOMPARE in the outer hot loop, it's quite slow + // just do it when something fails + QCOMPARE(result, expectedResult); + QCOMPARE(consumed, expectedConsumed); + } + } +} diff --git a/autotests/private/imapparsertest.h b/autotests/private/imapparsertest.h new file mode 100644 index 0000000..ac476f8 --- /dev/null +++ b/autotests/private/imapparsertest.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ImapParserTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testStripLeadingSpaces(); + void testParseQuotedString(); + void testParseString(); + void testParseParenthesizedList_data(); + void testParseParenthesizedList(); + void testParseNumber(); + void testQuote_data(); + void testQuote(); + void testMessageParser_data(); + void testMessageParser(); + void testParseSequenceSet_data(); + void testParseSequenceSet(); + void testParseDateTime_data(); + void testParseDateTime(); + void testBulkParser_data(); + void testBulkParser(); + void testJoin_data(); + void testJoin(); + + void benchParseQuotedString_data(); + void benchParseQuotedString(); +}; + diff --git a/autotests/private/imapsettest.cpp b/autotests/private/imapsettest.cpp new file mode 100644 index 0000000..476d545 --- /dev/null +++ b/autotests/private/imapsettest.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imapsettest.h" +#include "private/imapset_p.h" + +#include + +QTEST_MAIN(ImapSetTest) + +Q_DECLARE_METATYPE(QList) + +using namespace Akonadi; + +void ImapSetTest::testAddList_data() +{ + QTest::addColumn>("source"); + QTest::addColumn("intervals"); + QTest::addColumn("seqset"); + + // empty set + QList source; + ImapInterval::List intervals; + QTest::newRow("empty") << source << intervals << QByteArray(); + + // single value + source << 4; + intervals << ImapInterval(4, 4); + QTest::newRow("single value") << source << intervals << QByteArray("4"); + + // single 2-value interval + source << 5; + intervals.clear(); + intervals << ImapInterval(4, 5); + QTest::newRow("single 2 interval") << source << intervals << QByteArray("4:5"); + + // single large interval + source << 6 << 3 << 7 << 2 << 8; + intervals.clear(); + intervals << ImapInterval(2, 8); + QTest::newRow("single 7 interval") << source << intervals << QByteArray("2:8"); + + // double interval + source << 12; + intervals << ImapInterval(12, 12); + QTest::newRow("double interval") << source << intervals << QByteArray("2:8,12"); +} + +void ImapSetTest::testAddList() +{ + QFETCH(QList, source); + QFETCH(ImapInterval::List, intervals); + QFETCH(QByteArray, seqset); + + ImapSet set; + set.add(source); + ImapInterval::List result = set.intervals(); + QCOMPARE(result, intervals); + QCOMPARE(set.toImapSequenceSet(), seqset); +} diff --git a/autotests/private/imapsettest.h b/autotests/private/imapsettest.h new file mode 100644 index 0000000..8a320ca --- /dev/null +++ b/autotests/private/imapsettest.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class ImapSetTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testAddList_data(); + void testAddList(); +}; + diff --git a/autotests/private/notificationmessagetest.cpp b/autotests/private/notificationmessagetest.cpp new file mode 100644 index 0000000..d0b4975 --- /dev/null +++ b/autotests/private/notificationmessagetest.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationmessagetest.h" +#include + +#include +#include + +QTEST_APPLESS_MAIN(NotificationMessageTest) + +using namespace Akonadi; +using namespace Akonadi::Protocol; + +void NotificationMessageTest::testCompress() +{ + ChangeNotificationList list; + FetchCollectionsResponse collection(1); + CollectionChangeNotification msg; + msg.setCollection(std::move(collection)); + msg.setOperation(CollectionChangeNotification::Add); + + QVERIFY(CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); + + msg.setOperation(CollectionChangeNotification::Modify); + QVERIFY(!CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); + QCOMPARE(list.first().staticCast()->operation(), CollectionChangeNotification::Add); + + msg.setOperation(CollectionChangeNotification::Remove); + QVERIFY(CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 2); +} + +void NotificationMessageTest::testCompress2() +{ + ChangeNotificationList list; + FetchCollectionsResponse collection(1); + CollectionChangeNotification msg; + msg.setCollection(std::move(collection)); + msg.setOperation(CollectionChangeNotification::Modify); + + QVERIFY(CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); + + msg.setOperation(CollectionChangeNotification::Remove); + QVERIFY(CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 2); + QCOMPARE(list.first().staticCast()->operation(), CollectionChangeNotification::Modify); + QCOMPARE(list.last().staticCast()->operation(), CollectionChangeNotification::Remove); +} + +void NotificationMessageTest::testCompress3() +{ + ChangeNotificationList list; + FetchCollectionsResponse collection(1); + CollectionChangeNotification msg; + msg.setCollection(std::move(collection)); + msg.setOperation(CollectionChangeNotification::Modify); + + QVERIFY(CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); + + QVERIFY(!CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); +} + +void NotificationMessageTest::testPartModificationMerge() +{ + ChangeNotificationList list; + FetchCollectionsResponse collection(1); + CollectionChangeNotification msg; + msg.setCollection(std::move(collection)); + msg.setOperation(CollectionChangeNotification::Modify); + msg.setChangedParts(QSet() << "PART1"); + + QVERIFY(CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); + + msg.setChangedParts(QSet() << "PART2"); + QVERIFY(!CollectionChangeNotification::appendAndCompress(list, CollectionChangeNotificationPtr::create(msg))); + QCOMPARE(list.count(), 1); + QCOMPARE(list.first().staticCast()->changedParts(), + (QSet() << "PART1" + << "PART2")); +} diff --git a/autotests/private/notificationmessagetest.h b/autotests/private/notificationmessagetest.h new file mode 100644 index 0000000..03c02b5 --- /dev/null +++ b/autotests/private/notificationmessagetest.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class NotificationMessageTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCompress(); + void testCompress2(); + void testCompress3(); + void testPartModificationMerge(); +}; + diff --git a/autotests/private/protocoltest.cpp b/autotests/private/protocoltest.cpp new file mode 100644 index 0000000..f1b2bfa --- /dev/null +++ b/autotests/private/protocoltest.cpp @@ -0,0 +1,656 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "protocoltest.h" + +#include "private/scope_p.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Protocol; + +void ProtocolTest::testFactory_data() +{ + QTest::addColumn("type"); + QTest::addColumn("response"); + QTest::addColumn("success"); + + QTest::newRow("invalid cmd") << Command::Invalid << false << false; + QTest::newRow("invalid resp") << Command::Invalid << true << false; + QTest::newRow("hello cmd") << Command::Hello << false << false; + QTest::newRow("hello resp") << Command::Hello << true << true; + QTest::newRow("login cmd") << Command::Login << false << true; + QTest::newRow("login resp") << Command::Login << true << true; + QTest::newRow("logout cmd") << Command::Logout << false << true; + QTest::newRow("logout resp") << Command::Logout << true << true; + QTest::newRow("transaction cmd") << Command::Transaction << false << true; + QTest::newRow("transaction resp") << Command::Transaction << true << true; + QTest::newRow("createItem cmd") << Command::CreateItem << false << true; + QTest::newRow("createItem resp") << Command::CreateItem << true << true; + QTest::newRow("copyItems cmd") << Command::CopyItems << false << true; + QTest::newRow("copyItems resp") << Command::CopyItems << true << true; + QTest::newRow("deleteItems cmd") << Command::DeleteItems << false << true; + QTest::newRow("deleteItems resp") << Command::DeleteItems << true << true; + QTest::newRow("fetchItems cmd") << Command::FetchItems << false << true; + QTest::newRow("fetchItems resp") << Command::FetchItems << true << true; + QTest::newRow("linkItems cmd") << Command::LinkItems << false << true; + QTest::newRow("linkItems resp") << Command::LinkItems << true << true; + QTest::newRow("modifyItems cmd") << Command::ModifyItems << false << true; + QTest::newRow("modifyItems resp") << Command::ModifyItems << true << true; + QTest::newRow("moveItems cmd") << Command::MoveItems << false << true; + QTest::newRow("moveItems resp") << Command::MoveItems << true << true; + QTest::newRow("createCollection cmd") << Command::CreateCollection << false << true; + QTest::newRow("createCollection resp") << Command::CreateCollection << true << true; + QTest::newRow("copyCollection cmd") << Command::CopyCollection << false << true; + QTest::newRow("copyCollection resp") << Command::CopyCollection << true << true; + QTest::newRow("deleteCollection cmd") << Command::DeleteCollection << false << true; + QTest::newRow("deleteCollection resp") << Command::DeleteCollection << true << true; + QTest::newRow("fetchCollections cmd") << Command::FetchCollections << false << true; + QTest::newRow("fetchCollections resp") << Command::FetchCollections << true << true; + QTest::newRow("fetchCollectionStats cmd") << Command::FetchCollectionStats << false << true; + QTest::newRow("fetchCollectionStats resp") << Command::FetchCollectionStats << false << true; + QTest::newRow("modifyCollection cmd") << Command::ModifyCollection << false << true; + QTest::newRow("modifyCollection resp") << Command::ModifyCollection << true << true; + QTest::newRow("moveCollection cmd") << Command::MoveCollection << false << true; + QTest::newRow("moveCollection resp") << Command::MoveCollection << true << true; + QTest::newRow("search cmd") << Command::Search << false << true; + QTest::newRow("search resp") << Command::Search << true << true; + QTest::newRow("searchResult cmd") << Command::SearchResult << false << true; + QTest::newRow("searchResult resp") << Command::SearchResult << true << true; + QTest::newRow("storeSearch cmd") << Command::StoreSearch << false << true; + QTest::newRow("storeSearch resp") << Command::StoreSearch << true << true; + QTest::newRow("createTag cmd") << Command::CreateTag << false << true; + QTest::newRow("createTag resp") << Command::CreateTag << true << true; + QTest::newRow("deleteTag cmd") << Command::DeleteTag << false << true; + QTest::newRow("deleteTag resp") << Command::DeleteTag << true << true; + QTest::newRow("fetchTags cmd") << Command::FetchTags << false << true; + QTest::newRow("fetchTags resp") << Command::FetchTags << true << true; + QTest::newRow("modifyTag cmd") << Command::ModifyTag << false << true; + QTest::newRow("modifyTag resp") << Command::ModifyTag << true << true; + QTest::newRow("fetchRelations cmd") << Command::FetchRelations << false << true; + QTest::newRow("fetchRelations resp") << Command::FetchRelations << true << true; + QTest::newRow("modifyRelation cmd") << Command::ModifyRelation << false << true; + QTest::newRow("modifyRelation resp") << Command::ModifyRelation << true << true; + QTest::newRow("removeRelations cmd") << Command::RemoveRelations << false << true; + QTest::newRow("removeRelations resp") << Command::RemoveRelations << true << true; + QTest::newRow("selectResource cmd") << Command::SelectResource << false << true; + QTest::newRow("selectResource resp") << Command::SelectResource << true << true; + QTest::newRow("streamPayload cmd") << Command::StreamPayload << false << true; + QTest::newRow("streamPayload resp") << Command::StreamPayload << true << true; + QTest::newRow("itemChangeNotification cmd") << Command::ItemChangeNotification << false << true; + QTest::newRow("itemChangeNotification resp") << Command::ItemChangeNotification << true << false; + QTest::newRow("collectionChangeNotification cmd") << Command::CollectionChangeNotification << false << true; + QTest::newRow("collectionChangeNotification resp") << Command::CollectionChangeNotification << true << false; + QTest::newRow("tagChangeNotification cmd") << Command::TagChangeNotification << false << true; + QTest::newRow("tagChangENotification resp") << Command::TagChangeNotification << true << false; + QTest::newRow("relationChangeNotification cmd") << Command::RelationChangeNotification << false << true; + QTest::newRow("relationChangeNotification resp") << Command::RelationChangeNotification << true << false; + QTest::newRow("_responseBit cmd") << Command::_ResponseBit << false << false; + QTest::newRow("_responseBit resp") << Command::_ResponseBit << true << false; +} + +void ProtocolTest::testFactory() +{ + QFETCH(Command::Type, type); + QFETCH(bool, response); + QFETCH(bool, success); + + CommandPtr result; + if (response) { + result = Factory::response(type); + } else { + result = Factory::command(type); + } + + QCOMPARE(result->isValid(), success); + QCOMPARE(result->isResponse(), response); + if (success) { + QCOMPARE(result->type(), type); + } +} + +void ProtocolTest::testCommand() +{ + // There is no way to construct a valid Command directly + auto cmd = CommandPtr::create(); + QCOMPARE(cmd->type(), Command::Invalid); + QVERIFY(!cmd->isValid()); + QVERIFY(!cmd->isResponse()); + + CommandPtr cmdTest = serializeAndDeserialize(cmd); + QCOMPARE(cmdTest->type(), Command::Invalid); + QVERIFY(!cmd->isValid()); + QVERIFY(!cmd->isResponse()); +} + +void ProtocolTest::testResponse_data() +{ + QTest::addColumn("isError"); + QTest::addColumn("errorCode"); + QTest::addColumn("errorString"); + + QTest::newRow("no error") << false << 0 << QString(); + QTest::newRow("error") << true << 10 << QStringLiteral("Oh noes, there was an error!"); +} + +void ProtocolTest::testResponse() +{ + QFETCH(bool, isError); + QFETCH(int, errorCode); + QFETCH(QString, errorString); + + Response response; + if (isError) { + response.setError(errorCode, errorString); + } + + const auto res = serializeAndDeserialize(ResponsePtr::create(response)); + QCOMPARE(res->type(), Command::Invalid); + QVERIFY(!res->isValid()); + QVERIFY(res->isResponse()); + QCOMPARE(res->isError(), isError); + QCOMPARE(res->errorCode(), errorCode); + QCOMPARE(res->errorMessage(), errorString); + QVERIFY(*res == response); + const bool notEquals = (*res != response); + QVERIFY(!notEquals); +} + +void ProtocolTest::testAncestor() +{ + Ancestor in; + in.setId(42); + in.setRemoteId(QStringLiteral("remoteId")); + in.setName(QStringLiteral("Col 42")); + in.setAttributes({{"Attr1", "Val 1"}, {"Attr2", "Röndom útéef řetězec"}}); + + const Ancestor out = serializeAndDeserialize(in); + QCOMPARE(out.id(), 42); + QCOMPARE(out.remoteId(), QStringLiteral("remoteId")); + QCOMPARE(out.name(), QStringLiteral("Col 42")); + QCOMPARE(out.attributes(), Attributes({{"Attr1", "Val 1"}, {"Attr2", "Röndom útéef řetězec"}})); + QVERIFY(out == in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testFetchScope_data() +{ + QTest::addColumn("fullPayload"); + QTest::addColumn>("requestedParts"); + QTest::addColumn>("expectedParts"); + QTest::addColumn>("expectedPayloads"); + QTest::newRow("full payload (via flag") << true << QVector{"PLD:HEAD", "ATR:MYATR"} + << QVector{"PLD:HEAD", "ATR:MYATR", "PLD:RFC822"} << QVector{"PLD:HEAD", "PLD:RFC822"}; + QTest::newRow("full payload (via part name") << false << QVector{"PLD:HEAD", "ATR:MYATR", "PLD:RFC822"} + << QVector{"PLD:HEAD", "ATR:MYATR", "PLD:RFC822"} << QVector{"PLD:HEAD", "PLD:RFC822"}; + QTest::newRow("full payload (via both") << true << QVector{"PLD:HEAD", "ATR:MYATR", "PLD:RFC822"} + << QVector{"PLD:HEAD", "ATR:MYATR", "PLD:RFC822"} << QVector{"PLD:HEAD", "PLD:RFC822"}; + QTest::newRow("without full payload") << false << QVector{"PLD:HEAD", "ATR:MYATR"} << QVector{"PLD:HEAD", "ATR:MYATR"} + << QVector{"PLD:HEAD"}; +} + +void ProtocolTest::testFetchScope() +{ + QFETCH(bool, fullPayload); + QFETCH(QVector, requestedParts); + QFETCH(QVector, expectedParts); + QFETCH(QVector, expectedPayloads); + + ItemFetchScope in; + for (unsigned i = ItemFetchScope::CacheOnly; i <= ItemFetchScope::VirtReferences; i = i << 1) { + QVERIFY(!in.fetch(static_cast(i))); + } + QVERIFY(in.fetch(ItemFetchScope::None)); + + in.setRequestedParts(requestedParts); + in.setChangedSince(QDateTime(QDate(2015, 8, 10), QTime(23, 52, 20), Qt::UTC)); + in.setAncestorDepth(ItemFetchScope::AllAncestors); + in.setFetch(ItemFetchScope::CacheOnly); + in.setFetch(ItemFetchScope::CheckCachedPayloadPartsOnly); + in.setFetch(ItemFetchScope::FullPayload, fullPayload); + in.setFetch(ItemFetchScope::AllAttributes); + in.setFetch(ItemFetchScope::Size); + in.setFetch(ItemFetchScope::MTime); + in.setFetch(ItemFetchScope::RemoteRevision); + in.setFetch(ItemFetchScope::IgnoreErrors); + in.setFetch(ItemFetchScope::Flags); + in.setFetch(ItemFetchScope::RemoteID); + in.setFetch(ItemFetchScope::GID); + in.setFetch(ItemFetchScope::Tags); + in.setFetch(ItemFetchScope::Relations); + in.setFetch(ItemFetchScope::VirtReferences); + + const ItemFetchScope out = serializeAndDeserialize(in); + QCOMPARE(out.requestedParts(), expectedParts); + QCOMPARE(out.requestedPayloads(), expectedPayloads); + QCOMPARE(out.changedSince(), QDateTime(QDate(2015, 8, 10), QTime(23, 52, 20), Qt::UTC)); + QCOMPARE(out.ancestorDepth(), ItemFetchScope::AllAncestors); + QCOMPARE(out.fetch(ItemFetchScope::None), false); + QCOMPARE(out.cacheOnly(), true); + QCOMPARE(out.checkCachedPayloadPartsOnly(), true); + QCOMPARE(out.fullPayload(), fullPayload); + QCOMPARE(out.allAttributes(), true); + QCOMPARE(out.fetchSize(), true); + QCOMPARE(out.fetchMTime(), true); + QCOMPARE(out.fetchRemoteRevision(), true); + QCOMPARE(out.ignoreErrors(), true); + QCOMPARE(out.fetchFlags(), true); + QCOMPARE(out.fetchRemoteId(), true); + QCOMPARE(out.fetchGID(), true); + QCOMPARE(out.fetchRelations(), true); + QCOMPARE(out.fetchVirtualReferences(), true); +} + +void ProtocolTest::testScopeContext_data() +{ + QTest::addColumn("colId"); + QTest::addColumn("colRid"); + QTest::addColumn("tagId"); + QTest::addColumn("tagRid"); + + QTest::newRow("collection - id") << 42LL << QString() << 0LL << QString(); + QTest::newRow("collection - rid") << 0LL << QStringLiteral("rid") << 0LL << QString(); + QTest::newRow("collection - both") << 42LL << QStringLiteral("rid") << 0LL << QString(); + + QTest::newRow("tag - id") << 0LL << QString() << 42LL << QString(); + QTest::newRow("tag - rid") << 0LL << QString() << 0LL << QStringLiteral("rid"); + QTest::newRow("tag - both") << 0LL << QString() << 42LL << QStringLiteral("rid"); + + QTest::newRow("both - id") << 42LL << QString() << 10LL << QString(); + QTest::newRow("both - rid") << 0LL << QStringLiteral("colRid") << 0LL << QStringLiteral("tagRid"); + QTest::newRow("col - id, tag - rid") << 42LL << QString() << 0LL << QStringLiteral("tagRid"); + QTest::newRow("col - rid, tag - id") << 0LL << QStringLiteral("colRid") << 42LL << QString(); + QTest::newRow("both - both") << 42LL << QStringLiteral("colRid") << 10LL << QStringLiteral("tagRid"); +} + +void ProtocolTest::testScopeContext() +{ + QFETCH(qint64, colId); + QFETCH(QString, colRid); + QFETCH(qint64, tagId); + QFETCH(QString, tagRid); + + const bool hasColId = colId > 0; + const bool hasColRid = !colRid.isEmpty(); + const bool hasTagId = tagId > 0; + const bool hasTagRid = !tagRid.isEmpty(); + + ScopeContext in; + QVERIFY(in.isEmpty()); + if (hasColId) { + in.setContext(ScopeContext::Collection, colId); + } + if (hasColRid) { + in.setContext(ScopeContext::Collection, colRid); + } + if (hasTagId) { + in.setContext(ScopeContext::Tag, tagId); + } + if (hasTagRid) { + in.setContext(ScopeContext::Tag, tagRid); + } + + QCOMPARE(in.hasContextId(ScopeContext::Any), false); + QCOMPARE(in.hasContextRID(ScopeContext::Any), false); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(in.hasContextId(ScopeContext::Collection), hasColId); + QCOMPARE(in.hasContextRID(ScopeContext::Collection), hasColRid); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(in.hasContextId(ScopeContext::Tag), hasTagId); + QCOMPARE(in.hasContextRID(ScopeContext::Tag), hasTagRid); + QVERIFY(!in.isEmpty()); + + ScopeContext out = serializeAndDeserialize(in); + QCOMPARE(out.isEmpty(), false); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Collection), hasColId); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Collection), colId); + QCOMPARE(out.hasContextRID(ScopeContext::Collection), hasColRid); + QCOMPARE(out.contextRID(ScopeContext::Collection), colRid); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Tag), hasTagId); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Tag), tagId); + QCOMPARE(out.hasContextRID(ScopeContext::Tag), hasTagRid); + QCOMPARE(out.contextRID(ScopeContext::Tag), tagRid); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); + + // Clearing "any" should not do anything + out.clearContext(ScopeContext::Any); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Collection), hasColId); + QEXPECT_FAIL("collection - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Collection), colId); + QCOMPARE(out.hasContextRID(ScopeContext::Collection), hasColRid); + QCOMPARE(out.contextRID(ScopeContext::Collection), colRid); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.hasContextId(ScopeContext::Tag), hasTagId); + QEXPECT_FAIL("tag - both", "Cannot set both ID and RID context", Continue); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(out.contextId(ScopeContext::Tag), tagId); + QCOMPARE(out.hasContextRID(ScopeContext::Tag), hasTagRid); + QCOMPARE(out.contextRID(ScopeContext::Tag), tagRid); + + if (hasColId || hasColRid) { + ScopeContext clear = out; + clear.clearContext(ScopeContext::Collection); + QCOMPARE(clear.hasContextId(ScopeContext::Collection), false); + QCOMPARE(clear.hasContextRID(ScopeContext::Collection), false); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(clear.hasContextId(ScopeContext::Tag), hasTagId); + QCOMPARE(clear.hasContextRID(ScopeContext::Tag), hasTagRid); + } + if (hasTagId || hasTagRid) { + ScopeContext clear = out; + clear.clearContext(ScopeContext::Tag); + QEXPECT_FAIL("both - both", "Cannot set both ID and RID context", Continue); + QCOMPARE(clear.hasContextId(ScopeContext::Collection), hasColId); + QCOMPARE(clear.hasContextRID(ScopeContext::Collection), hasColRid); + QCOMPARE(clear.hasContextId(ScopeContext::Tag), false); + QCOMPARE(clear.hasContextRID(ScopeContext::Tag), false); + } + + out.clearContext(ScopeContext::Collection); + out.clearContext(ScopeContext::Tag); + QVERIFY(out.isEmpty()); +} + +void ProtocolTest::testPartMetaData() +{ + PartMetaData in; + in.setName("PLD:HEAD"); + in.setSize(42); + in.setVersion(1); + in.setStorageType(PartMetaData::External); + + const PartMetaData out = serializeAndDeserialize(in); + QCOMPARE(out.name(), QByteArray("PLD:HEAD")); + QCOMPARE(out.size(), 42); + QCOMPARE(out.version(), 1); + QCOMPARE(out.storageType(), PartMetaData::External); + QCOMPARE(out, in); + const bool notEquals = (in != out); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCachePolicy() +{ + CachePolicy in; + in.setInherit(true); + in.setCheckInterval(42); + in.setCacheTimeout(10); + in.setSyncOnDemand(true); + in.setLocalParts({QStringLiteral("PLD:HEAD"), QStringLiteral("PLD:ENVELOPE")}); + + const CachePolicy out = serializeAndDeserialize(in); + QCOMPARE(out.inherit(), true); + QCOMPARE(out.checkInterval(), 42); + QCOMPARE(out.cacheTimeout(), 10); + QCOMPARE(out.syncOnDemand(), true); + QCOMPARE(out.localParts(), QStringList() << QStringLiteral("PLD:HEAD") << QStringLiteral("PLD:ENVELOPE")); + QCOMPARE(out, in); + const bool notEquals = (out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testHelloResponse() +{ + HelloResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setServerName(QStringLiteral("AkonadiTest")); + in.setMessage(QStringLiteral("Oh, hello there!")); + in.setProtocolVersion(42); + in.setError(10, QStringLiteral("Ooops")); + + const auto out = serializeAndDeserialize(HelloResponsePtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(out->isResponse()); + QVERIFY(out->isError()); + QCOMPARE(out->errorCode(), 10); + QCOMPARE(out->errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(out->serverName(), QStringLiteral("AkonadiTest")); + QCOMPARE(out->message(), QStringLiteral("Oh, hello there!")); + QCOMPARE(out->protocolVersion(), 42); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLoginCommand() +{ + LoginCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + in.setSessionId("MySession-123-notifications"); + + const auto out = serializeAndDeserialize(LoginCommandPtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(!out->isResponse()); + QCOMPARE(out->sessionId(), QByteArray("MySession-123-notifications")); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLoginResponse() +{ + LoginResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const auto out = serializeAndDeserialize(LoginResponsePtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(out->isResponse()); + QVERIFY(out->isError()); + QCOMPARE(out->errorCode(), 42); + QCOMPARE(out->errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLogoutCommand() +{ + LogoutCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + + const auto out = serializeAndDeserialize(LogoutCommandPtr::create(in)); + QVERIFY(!out->isResponse()); + QVERIFY(out->isValid()); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testLogoutResponse() +{ + LogoutResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const auto out = serializeAndDeserialize(LogoutResponsePtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(out->isResponse()); + QVERIFY(out->isError()); + QCOMPARE(out->errorCode(), 42); + QCOMPARE(out->errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testTransactionCommand() +{ + TransactionCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + in.setMode(TransactionCommand::Begin); + + const auto out = serializeAndDeserialize(TransactionCommandPtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(!out->isResponse()); + QCOMPARE(out->mode(), TransactionCommand::Begin); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testTransactionResponse() +{ + TransactionResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const auto out = serializeAndDeserialize(TransactionResponsePtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(out->isResponse()); + QVERIFY(out->isError()); + QCOMPARE(out->errorCode(), 42); + QCOMPARE(out->errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCreateItemCommand() +{ + Scope addedTags(QVector{3, 4}); + Scope removedTags(QVector{5, 6}); + Attributes attrs{{"ATTR1", "MyAttr"}, {"ATTR2", "Můj chlupaťoučký kůň"}}; + QSet parts{"PLD:HEAD", "PLD:ENVELOPE"}; + + CreateItemCommand in; + QVERIFY(!in.isResponse()); + QVERIFY(in.isValid()); + QCOMPARE(in.mergeModes(), CreateItemCommand::None); + in.setMergeModes(CreateItemCommand::MergeModes(CreateItemCommand::GID | CreateItemCommand::RemoteID)); + in.setCollection(Scope(1)); + in.setItemSize(100); + in.setMimeType(QStringLiteral("text/directory")); + in.setGid(QStringLiteral("GID")); + in.setRemoteId(QStringLiteral("RID")); + in.setRemoteRevision(QStringLiteral("RREV")); + in.setDateTime(QDateTime(QDate(2015, 8, 11), QTime(14, 32, 21), Qt::UTC)); + in.setFlags({"\\SEEN", "FLAG"}); + in.setFlagsOverwritten(true); + in.setAddedFlags({"FLAG2"}); + in.setRemovedFlags({"FLAG3"}); + in.setTags(Scope(2)); + in.setAddedTags(addedTags); + in.setRemovedTags(removedTags); + in.setAttributes(attrs); + in.setParts(parts); + + const auto out = serializeAndDeserialize(CreateItemCommandPtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(!out->isResponse()); + QCOMPARE(out->mergeModes(), CreateItemCommand::GID | CreateItemCommand::RemoteID); + QCOMPARE(out->collection(), Scope(1)); + QCOMPARE(out->itemSize(), 100); + QCOMPARE(out->mimeType(), QStringLiteral("text/directory")); + QCOMPARE(out->gid(), QStringLiteral("GID")); + QCOMPARE(out->remoteId(), QStringLiteral("RID")); + QCOMPARE(out->remoteRevision(), QStringLiteral("RREV")); + QCOMPARE(out->dateTime(), QDateTime(QDate(2015, 8, 11), QTime(14, 32, 21), Qt::UTC)); + QCOMPARE(out->flags(), + QSet() << "\\SEEN" + << "FLAG"); + QCOMPARE(out->flagsOverwritten(), true); + QCOMPARE(out->addedFlags(), QSet{"FLAG2"}); + QCOMPARE(out->removedFlags(), QSet{"FLAG3"}); + QCOMPARE(out->tags(), Scope(2)); + QCOMPARE(out->addedTags(), addedTags); + QCOMPARE(out->removedTags(), removedTags); + QCOMPARE(out->attributes(), attrs); + QCOMPARE(out->parts(), parts); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCreateItemResponse() +{ + CreateItemResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const auto out = serializeAndDeserialize(CreateItemResponsePtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(out->isResponse()); + QVERIFY(out->isError()); + QCOMPARE(out->errorCode(), 42); + QCOMPARE(out->errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCopyItemsCommand() +{ + const Scope items(QVector{1, 2, 3, 10}); + + CopyItemsCommand in; + QVERIFY(in.isValid()); + QVERIFY(!in.isResponse()); + in.setItems(items); + in.setDestination(Scope(42)); + + const auto out = serializeAndDeserialize(CopyItemsCommandPtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(!out->isResponse()); + QCOMPARE(out->items(), items); + QCOMPARE(out->destination(), Scope(42)); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +void ProtocolTest::testCopyItemsResponse() +{ + CopyItemsResponse in; + QVERIFY(in.isResponse()); + QVERIFY(in.isValid()); + QVERIFY(!in.isError()); + in.setError(42, QStringLiteral("Ooops")); + + const auto out = serializeAndDeserialize(CopyItemsResponsePtr::create(in)); + QVERIFY(out->isValid()); + QVERIFY(out->isResponse()); + QVERIFY(out->isError()); + QCOMPARE(out->errorCode(), 42); + QCOMPARE(out->errorMessage(), QStringLiteral("Ooops")); + QCOMPARE(*out, in); + const bool notEquals = (*out != in); + QVERIFY(!notEquals); +} + +QTEST_MAIN(ProtocolTest) diff --git a/autotests/private/protocoltest.h b/autotests/private/protocoltest.h new file mode 100644 index 0000000..16a9176 --- /dev/null +++ b/autotests/private/protocoltest.h @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include +#include + +#include +#include + +#include + +class ProtocolTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testFactory_data(); + void testFactory(); + + void testCommand(); + void testResponse_data(); + void testResponse(); + void testAncestor(); + void testFetchScope_data(); + void testFetchScope(); + void testScopeContext_data(); + void testScopeContext(); + void testPartMetaData(); + void testCachePolicy(); + + void testHelloResponse(); + void testLoginCommand(); + void testLoginResponse(); + void testLogoutCommand(); + void testLogoutResponse(); + void testTransactionCommand(); + void testTransactionResponse(); + void testCreateItemCommand(); + void testCreateItemResponse(); + void testCopyItemsCommand(); + void testCopyItemsResponse(); + +private: + template + typename std::enable_if::value, QSharedPointer>::type serializeAndDeserialize(const QSharedPointer &in) + { + QBuffer buf; + buf.open(QIODevice::ReadWrite); + Akonadi::Protocol::DataStream stream(&buf); + + Akonadi::Protocol::serialize(stream, in); + stream.flush(); + buf.seek(0); + return Akonadi::Protocol::deserialize(&buf).staticCast(); + } + + template + typename std::enable_if::value == false, T>::type serializeAndDeserialize(const T &in, int * = nullptr) + { + QBuffer buf; + buf.open(QIODevice::ReadWrite); + + Akonadi::Protocol::DataStream stream(&buf); + stream << in; + stream.flush(); + buf.seek(0); + T out; + stream >> out; + + return out; + } +}; + diff --git a/autotests/server/CMakeLists.txt b/autotests/server/CMakeLists.txt new file mode 100644 index 0000000..64904fe --- /dev/null +++ b/autotests/server/CMakeLists.txt @@ -0,0 +1,103 @@ +########### next target ############### + +# QTEST_MAIN is using QApplication when QT_GUI_LIB is defined +remove_definitions(-DQT_GUI_LIB) + +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +akonadi_run_xsltproc( + XSL ${Akonadi_SOURCE_DIR}/src/server/storage/schema.xsl + XML ${CMAKE_CURRENT_SOURCE_DIR}/dbtest_data/unittest_schema.xml + BASENAME unittestschema + CLASSNAME UnitTestSchema +) +akonadi_run_xsltproc( + XSL ${CMAKE_CURRENT_SOURCE_DIR}/dbpopulator.xsl + XML ${CMAKE_CURRENT_SOURCE_DIR}/dbtest_data/dbdata.xml + BASENAME dbpopulator +) + +set_property(SOURCE ${CMAKE_CURRENT_BINARY_DIR}/dbpopulator.cpp PROPERTY SKIP_AUTOMOC TRUE) + +add_library(akonadi_unittest_common STATIC) +target_sources(akonadi_unittest_common PRIVATE + unittestschema.cpp + fakeconnection.cpp + fakeintervalcheck.cpp + fakedatastore.cpp + fakeclient.cpp + fakeakonadiserver.cpp + fakesearchmanager.cpp + fakeitemretrievalmanager.cpp + dbinitializer.cpp + inspectablenotificationcollector.cpp + ${CMAKE_CURRENT_BINARY_DIR}/dbpopulator.cpp +) + + +target_link_libraries(akonadi_unittest_common + KF5AkonadiPrivate + libakonadiserver + Qt::Core + Qt::DBus + Qt::Test + Qt::Sql + Qt::Network +) + +macro(add_server_test _source) + set(_test ${_source} ../../src/server/akonadiserver_debug.cpp ../../src/server/akonadiserver_search_debug.cpp dbtest_data/dbtest_data.qrc) + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} NAME_PREFIX "AkonadiServer-" ${_test}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiServer-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + set_tests_properties(AkonadiServer-${_name} PROPERTIES + ENVIRONMENT "QT_HASH_SEED=0;QT_NO_CPU_FEATURE=sse4.2" + ) + target_link_libraries(${_name} + akonadi_shared + akonadi_unittest_common + libakonadiserver + KF5AkonadiPrivate + Qt::Core + Qt::DBus + Qt::Test + Qt::Sql + Qt::Network + ${CMAKE_SHARED_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_server_test(dbdeadlockcatchertest.cpp) +add_server_test(dbtypetest.cpp) +add_server_test(dbintrospectortest.cpp) +add_server_test(querybuildertest.cpp) +add_server_test(dbinitializertest.cpp) +add_server_test(dbupdatertest.cpp) +add_server_test(handlertest.cpp) +add_server_test(dbconfigtest.cpp) +add_server_test(parthelpertest.cpp) +add_server_test(itemretrievertest.cpp) +add_server_test(notificationsubscribertest.cpp) +add_server_test(notificationmanagertest.cpp) +add_server_test(parttypehelpertest.cpp) +add_server_test(collectionstatisticstest.cpp) +add_server_test(aggregatedfetchscopetest.cpp) +add_server_test(collectionschedulertest.cpp) + +if (SQLITE_FOUND) # tests using the fake server need the QSQLITE3 plugin +add_server_test(partstreamertest.cpp) +add_server_test(itemcreatehandlertest.cpp) +add_server_test(itemlinkhandlertest.cpp) +add_server_test(itemmovehandlertest.cpp) +add_server_test(collectioncreatehandlertest.cpp) +add_server_test(collectionfetchhandlertest.cpp) +add_server_test(collectionmodifyhandlertest.cpp) +add_server_test(searchtest.cpp akonadiprivate) +add_server_test(relationhandlertest.cpp akonadiprivate) +add_server_test(taghandlertest.cpp akonadiprivate) +add_server_test(fetchhandlertest.cpp akonadiprivate) +endif() diff --git a/autotests/server/aggregatedfetchscopetest.cpp b/autotests/server/aggregatedfetchscopetest.cpp new file mode 100644 index 0000000..a744112 --- /dev/null +++ b/autotests/server/aggregatedfetchscopetest.cpp @@ -0,0 +1,158 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "aggregatedfetchscope.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class AggregatedFetchScopeTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void testTagApply() + { + AggregatedTagFetchScope scope; + + // first subscriber, A + scope.addSubscriber(); + Protocol::TagFetchScope oldTagScope; + Protocol::TagFetchScope tagScopeA; + QSet attrs = {"FOO"}; + tagScopeA.setAttributes(attrs); + tagScopeA.setFetchIdOnly(true); + tagScopeA.setFetchAllAttributes(false); + scope.apply(oldTagScope, tagScopeA); + QCOMPARE(scope.attributes(), attrs); + QVERIFY(scope.fetchIdOnly()); + QVERIFY(!scope.fetchAllAttributes()); + + // second subscriber, B + Protocol::TagFetchScope tagScopeB = tagScopeA; + tagScopeB.setFetchIdOnly(false); + tagScopeB.setFetchAllAttributes(true); + scope.addSubscriber(); + scope.apply(oldTagScope, tagScopeB); + QCOMPARE(scope.attributes(), attrs); + QVERIFY(!scope.fetchIdOnly()); + QVERIFY(scope.fetchAllAttributes()); + + // then B goes away + scope.apply(tagScopeB, oldTagScope); + scope.removeSubscriber(); + QCOMPARE(scope.attributes(), attrs); + QVERIFY(scope.fetchIdOnly()); + QVERIFY(!scope.fetchAllAttributes()); + + // A goes away + scope.apply(tagScopeA, oldTagScope); + scope.removeSubscriber(); + QCOMPARE(scope.attributes(), QSet()); + } + + void testCollectionApply() + { + AggregatedCollectionFetchScope scope; + + // first subscriber, A + scope.addSubscriber(); + Protocol::CollectionFetchScope oldCollectionScope; + Protocol::CollectionFetchScope collectionScopeA; + QSet attrs = {"FOO"}; + collectionScopeA.setAttributes(attrs); + collectionScopeA.setFetchIdOnly(true); + scope.apply(oldCollectionScope, collectionScopeA); + QCOMPARE(scope.attributes(), attrs); + QVERIFY(scope.fetchIdOnly()); + + // second subscriber, B + Protocol::CollectionFetchScope collectionScopeB = collectionScopeA; + collectionScopeB.setFetchIdOnly(false); + scope.addSubscriber(); + scope.apply(oldCollectionScope, collectionScopeB); + QCOMPARE(scope.attributes(), attrs); + QVERIFY(!scope.fetchIdOnly()); + + // then B goes away + scope.apply(collectionScopeB, oldCollectionScope); + scope.removeSubscriber(); + QCOMPARE(scope.attributes(), attrs); + QVERIFY(scope.fetchIdOnly()); + + // A goes away + scope.apply(collectionScopeA, oldCollectionScope); + scope.removeSubscriber(); + QCOMPARE(scope.attributes(), QSet()); + } + + void testItemApply() + { + AggregatedItemFetchScope scope; + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::NoAncestor); + + // first subscriber, A + scope.addSubscriber(); + Protocol::ItemFetchScope oldItemScope; + Protocol::ItemFetchScope itemScopeA; + QVector parts = {"FOO"}; + QSet partsSet = {"FOO"}; + itemScopeA.setRequestedParts(parts); + itemScopeA.setAncestorDepth(Protocol::ItemFetchScope::ParentAncestor); + itemScopeA.setFetch(Protocol::ItemFetchScope::CacheOnly); + itemScopeA.setFetch(Protocol::ItemFetchScope::IgnoreErrors); + scope.apply(oldItemScope, itemScopeA); + QCOMPARE(scope.requestedParts(), partsSet); + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::ParentAncestor); + QVERIFY(scope.cacheOnly()); + QVERIFY(scope.ignoreErrors()); + + // second subscriber, B + Protocol::ItemFetchScope itemScopeB = itemScopeA; + itemScopeB.setAncestorDepth(Protocol::ItemFetchScope::AllAncestors); + scope.addSubscriber(); + QVERIFY(!scope.cacheOnly()); // they don't agree so: false + QVERIFY(!scope.ignoreErrors()); + scope.apply(oldItemScope, itemScopeB); + QCOMPARE(scope.requestedParts(), partsSet); + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::AllAncestors); + + // subscriber C with ParentAncestor - but that won't make change it + Protocol::ItemFetchScope itemScopeC = itemScopeA; + scope.addSubscriber(); + scope.apply(oldItemScope, itemScopeC); + QCOMPARE(scope.requestedParts(), partsSet); + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::AllAncestors); // no change + + // then C goes away + scope.apply(itemScopeC, oldItemScope); + scope.removeSubscriber(); + QCOMPARE(scope.requestedParts(), partsSet); + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::AllAncestors); + + // then B goes away + scope.apply(itemScopeB, oldItemScope); + scope.removeSubscriber(); + QCOMPARE(scope.requestedParts(), partsSet); + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::ParentAncestor); + + // A goes away + scope.apply(itemScopeA, oldItemScope); + scope.removeSubscriber(); + QCOMPARE(scope.requestedParts(), QSet()); + QCOMPARE(scope.ancestorDepth(), Protocol::ItemFetchScope::NoAncestor); + } +}; + +QTEST_MAIN(AggregatedFetchScopeTest) + +#include "aggregatedfetchscopetest.moc" diff --git a/autotests/server/collectioncreatehandlertest.cpp b/autotests/server/collectioncreatehandlertest.cpp new file mode 100644 index 0000000..0a9f25b --- /dev/null +++ b/autotests/server/collectioncreatehandlertest.cpp @@ -0,0 +1,161 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include + +#include + +#include "aktest.h" +#include "dbinitializer.h" +#include "entities.h" +#include "fakeakonadiserver.h" + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class CollectionCreateHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + CollectionCreateHandlerTest() + { + mAkonadi.init(); + } + +private Q_SLOTS: + void testCreate_data() + { + DbInitializer dbInitializer; + + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + + auto notificationTemplate = Protocol::CollectionChangeNotificationPtr::create(); + notificationTemplate->setOperation(Protocol::CollectionChangeNotification::Add); + notificationTemplate->setParentCollection(3); + notificationTemplate->setResource("akonadi_fake_resource_0"); + notificationTemplate->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + { + auto cmd = Protocol::CreateCollectionCommandPtr::create(); + cmd->setName(QStringLiteral("New Name")); + cmd->setParent(Scope(3)); + cmd->setAttributes({{"MYRANDOMATTRIBUTE", ""}}); + + auto resp = Protocol::FetchCollectionsResponsePtr::create(8); + resp->setName(QStringLiteral("New Name")); + resp->setParentId(3); + resp->setAttributes({{"MYRANDOMATTRIBUTE", ""}}); + resp->setResource(QStringLiteral("akonadi_fake_resource_0")); + resp->cachePolicy().setLocalParts({QLatin1String("ALL")}); + resp->setMimeTypes({QLatin1String("application/octet-stream"), QLatin1String("inode/directory")}); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateCollectionResponsePtr::create()); + + Protocol::FetchCollectionsResponse collection(*resp); + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setCollection(std::move(collection)); + + QTest::newRow("create collection") << scenarios << notification; + } + { + auto cmd = Protocol::CreateCollectionCommandPtr::create(); + cmd->setName(QStringLiteral("Name 2")); + cmd->setParent(Scope(3)); + cmd->setEnabled(false); + cmd->setDisplayPref(Tristate::True); + cmd->setSyncPref(Tristate::True); + cmd->setIndexPref(Tristate::True); + + auto resp = Protocol::FetchCollectionsResponsePtr::create(9); + resp->setName(QStringLiteral("Name 2")); + resp->setParentId(3); + resp->setEnabled(false); + resp->setDisplayPref(Tristate::True); + resp->setSyncPref(Tristate::True); + resp->setIndexPref(Tristate::True); + resp->setResource(QStringLiteral("akonadi_fake_resource_0")); + resp->cachePolicy().setLocalParts({QLatin1String("ALL")}); + resp->setMimeTypes({QLatin1String("application/octet-stream"), QLatin1String("inode/directory")}); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateCollectionResponsePtr::create()); + + Protocol::FetchCollectionsResponse collection(*resp); + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setCollection(std::move(collection)); + + QTest::newRow("create collection with local override") << scenarios << notification; + } + + { + auto cmd = Protocol::CreateCollectionCommandPtr::create(); + cmd->setName(QStringLiteral("TopLevel")); + cmd->setParent(Scope(0)); + cmd->setMimeTypes({QLatin1String("inode/directory")}); + + auto resp = Protocol::FetchCollectionsResponsePtr::create(10); + resp->setName(QStringLiteral("TopLevel")); + resp->setParentId(0); + resp->setEnabled(true); + resp->setMimeTypes({QLatin1String("inode/directory")}); + resp->cachePolicy().setLocalParts({QLatin1String("ALL")}); + resp->setResource(QStringLiteral("akonadi_fake_resource_0")); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario("akonadi_fake_resource_0") << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateCollectionResponsePtr::create()); + + Protocol::FetchCollectionsResponse collection(*resp); + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setSessionId("akonadi_fake_resource_0"); + notification->setParentCollection(0); + notification->setCollection(std::move(collection)); + + QTest::newRow("create top-level collection") << scenarios << notification; + } + } + + void testCreate() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::CollectionChangeNotificationPtr, notification); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + auto notificationSpy = mAkonadi.notificationSpy(); + if (notification->operation() != Protocol::CollectionChangeNotification::InvalidOp) { + QCOMPARE(notificationSpy->count(), 1); + const auto notifications = notificationSpy->takeFirst().first().value(); + QCOMPARE(notifications.count(), 1); + const auto actualNtf = notifications.first().staticCast(); + if (*actualNtf != *notification) { + qDebug() << "Actual: " << Protocol::debugString(actualNtf); + qDebug() << "Expected:" << Protocol::debugString(notification); + } + QCOMPARE(*notifications.first().staticCast(), *notification); + } else { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } + } +}; + +AKTEST_FAKESERVER_MAIN(CollectionCreateHandlerTest) + +#include "collectioncreatehandlertest.moc" diff --git a/autotests/server/collectionfetchhandlertest.cpp b/autotests/server/collectionfetchhandlertest.cpp new file mode 100644 index 0000000..77314a5 --- /dev/null +++ b/autotests/server/collectionfetchhandlertest.cpp @@ -0,0 +1,577 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include + +#include "aktest.h" +#include "dbinitializer.h" +#include "entities.h" +#include "fakeakonadiserver.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class CollectionFetchHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + CollectionFetchHandlerTest() + : QObject() + { + mAkonadi.setPopulateDb(false); + mAkonadi.init(); + { + MimeType mt(QStringLiteral("mimetype1")); + mt.insert(); + } + { + MimeType mt(QStringLiteral("mimetype2")); + mt.insert(); + } + { + MimeType mt(QStringLiteral("mimetype3")); + mt.insert(); + } + { + MimeType mt(QStringLiteral("mimetype4")); + mt.insert(); + } + } + + Protocol::FetchCollectionsCommandPtr + createCommand(const Scope &scope, + Protocol::FetchCollectionsCommand::Depth depth = Akonadi::Protocol::FetchCollectionsCommand::BaseCollection, + Protocol::Ancestor::Depth ancDepth = Protocol::Ancestor::NoAncestor, + const QStringList &mimeTypes = QStringList(), + const QString &resource = QString()) + { + auto cmd = Protocol::FetchCollectionsCommandPtr::create(scope); + cmd->setDepth(depth); + cmd->setAncestorsDepth(ancDepth); + cmd->setMimeTypes(mimeTypes); + cmd->setResource(resource); + return cmd; + } + + QScopedPointer initializer; +private Q_SLOTS: + + void testList_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + Collection col2 = initializer->createCollection("col2", col1); + Collection col3 = initializer->createCollection("col3", col2); + Collection col4 = initializer->createCollection("col4"); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id())) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("base list") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id(), Protocol::FetchCollectionsCommand::ParentCollection)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("first level list") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id(), Protocol::FetchCollectionsCommand::AllCollections)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list that filters collection") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(col2.id(), Protocol::FetchCollectionsCommand::BaseCollection, Protocol::Ancestor::AllAncestors)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("base ancestors") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(col2.id(), Protocol::FetchCollectionsCommand::BaseCollection, Protocol::Ancestor::AllAncestors)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("first level ancestors") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(col1.id(), Protocol::FetchCollectionsCommand::AllCollections, Protocol::Ancestor::AllAncestors)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive ancestors") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(Scope(), Protocol::FetchCollectionsCommand::ParentCollection)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("first level root list") << scenarios; + } + } + + void testList() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testListFiltered_data() + { + initializer.reset(new DbInitializer); + + MimeType mtCalendar(QStringLiteral("text/calendar")); + mtCalendar.insert(); + + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + col1.update(); + Collection col2 = initializer->createCollection("col2", col1); + col2.addMimeType(mtCalendar); + col2.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(Scope(), + Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, + {QLatin1String("text/calendar")})) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1, false, false)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list to display including local override") << scenarios; + } + } + + void testListFiltered() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testListFilterByResource() + { + initializer.reset(new DbInitializer); + + Resource res2; + res2.setName(QStringLiteral("testresource2")); + QVERIFY(res2.insert()); + + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + + Collection col2; + col2.setName(QStringLiteral("col2")); + col2.setRemoteId(QStringLiteral("col2")); + col2.setResource(res2); + QVERIFY(col2.insert()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(Scope(), + Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, + {}, + QStringLiteral("testresource"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + col2.remove(); + res2.remove(); + } + + void testListEnabled_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + Collection col2 = initializer->createCollection("col2", col1); + col2.setEnabled(false); + col2.setSyncPref(Collection::True); + col2.setDisplayPref(Collection::True); + col2.setIndexPref(Collection::True); + col2.update(); + Collection col3 = initializer->createCollection("col3", col2); + col3.setEnabled(true); + col3.setSyncPref(Collection::False); + col3.setDisplayPref(Collection::False); + col3.setIndexPref(Collection::False); + col3.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + + auto cmd = createCommand(col3.id()); + cmd->setDisplayPref(true); + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + // Listing a disabled collection should still work for base listing + QTest::newRow("list base of disabled collection") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd->setDisplayPref(true); + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list to display including local override") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd->setSyncPref(true); + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list to sync including local override") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd->setIndexPref(true); + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list to index including local override") << scenarios; + } + { + TestScenario::List scenarios; + + auto cmd = createCommand(Scope(), Protocol::FetchCollectionsCommand::AllCollections); + cmd->setEnabled(true); + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(initializer->collection("Search"))) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("recursive list of enabled") << scenarios; + } + } + + void testListEnabled() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testListAttribute_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + Collection col2 = initializer->createCollection("col2"); + + CollectionAttribute attr1; + attr1.setType("type"); + attr1.setValue("value"); + attr1.setCollection(col1); + attr1.insert(); + + CollectionAttribute attr2; + attr2.setType("type"); + attr2.setValue(QStringLiteral("Umlautäöü").toUtf8()); + attr2.setCollection(col2); + attr2.insert(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col1.id())) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1, false, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("list attribute") << scenarios; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(col2.id())) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, false, true)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("list attribute") << scenarios; + } + } + + void testListAttribute() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testListAncestorAttributes_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + + CollectionAttribute attr1; + attr1.setType("type"); + attr1.setValue("value"); + attr1.setCollection(col1); + attr1.insert(); + + Collection col2 = initializer->createCollection("col2", col1); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + + auto cmd = createCommand(col2.id(), Protocol::FetchCollectionsCommand::BaseCollection, Protocol::Ancestor::AllAncestors); + cmd->setAncestorsAttributes({"type"}); + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2, true, true, {QLatin1String("type")})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("list ancestor attribute with fetch scope") << scenarios; + } + } + + void testListAncestorAttributes() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testIncludeAncestors_data() + { + // The collection we are querying contains a load of disabled collections (typical scenario with many shared folders) + // The collection we are NOT querying contains a reasonable amount of enabled collections (to test the performance impact of the manually filtering by + // tree) + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + + MimeType mtDirectory = MimeType::retrieveByName(QStringLiteral("mimetype1")); + + Collection col1 = initializer->createCollection("col1"); + col1.addMimeType(mtDirectory); + col1.update(); + Collection col2 = initializer->createCollection("col2", col1); + Collection col3 = initializer->createCollection("col3", col2); + Collection col4 = initializer->createCollection("col4", col3); + col4.addMimeType(mtDirectory); + col4.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(Scope(), + Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, + {QLatin1String("mimetype1")})) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col1)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + QTest::newRow("ensure filtered grandparent is included") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(col1.id(), + Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::NoAncestor, + {QLatin1String("mimetype1")})) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col2)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col3)) + << TestScenario::create(5, TestScenario::ServerCmd, initializer->listResponse(col4)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponsePtr::create()); + // This also ensures col1 is excluded although it matches the mimetype filter + QTest::newRow("ensure filtered grandparent is included with specified parent") << scenarios; + } + } + + void testIncludeAncestors() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + +// No point in running the benchmark every time +#if 0 + + void testListEnabledBenchmark_data() + { + //The collection we are quering contains a load of disabled collections (typical scenario with many shared folders) + //The collection we are NOT querying contains a reasonable amount of enabled collections (to test the performance impact of the manually filtering by tree) + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + + Collection toplevel = initializer->createCollection("toplevel"); + + Collection col1 = initializer->createCollection("col1", toplevel); + Collection col2 = initializer->createCollection("col2", col1); + Collection col3 = initializer->createCollection("col3", col2); + + Collection col4 = initializer->createCollection("col4", toplevel); + Collection col5 = initializer->createCollection("col5", col4); + col5.setEnabled(false); + col5.update(); + Collection col6 = initializer->createCollection("col6", col5); + col5.setEnabled(false); + col5.update(); + + MimeType mt1 = MimeType::retrieveByName(QLatin1String("mimetype1")); + MimeType mt2 = MimeType::retrieveByName(QLatin1String("mimetype2")); + MimeType mt3 = MimeType::retrieveByName(QLatin1String("mimetype3")); + MimeType mt4 = MimeType::retrieveByName(QLatin1String("mimetype4")); + + QTime t; + t.start(); + for (int i = 0; i < 100000; i++) { + QByteArray name = QString::fromLatin1("col%1").arg(i+4).toLatin1(); + Collection col = initializer->createCollection(name.data(), col3); + col.setEnabled(false); + col.addMimeType(mt1); + col.addMimeType(mt2); + col.addMimeType(mt3); + col.addMimeType(mt4); + col.update(); + } + for (int i = 0; i < 1000; i++) { + QByteArray name = QString::fromLatin1("col%1").arg(i+100004).toLatin1(); + Collection col = initializer->createCollection(name.data(), col5); + col.addMimeType(mt1); + col.addMimeType(mt2); + col.update(); + } + + qDebug() << "Created 100000 collections in" << t.elapsed() << "msecs"; + + QTest::addColumn("scenarios"); + + // { + // QList scenario; + // scenario << FakeAkonadiServer::defaultScenario() + // << "C: 2 LIST " + QByteArray::number(toplevel.id()) + " INF (ENABLED ) ()" + // << "S: IGNORE 1006" + // << "S: 2 OK List completed"; + // QTest::newRow("recursive list of enabled") << scenario; + // } + // { + // QList scenario; + // scenario << FakeAkonadiServer::defaultScenario() + // << "C: 2 LIST " + QByteArray::number(toplevel.id()) + " INF (MIMETYPE (mimetype1) RESOURCE \"testresource\") ()" + // // << "C: 2 LIST " + QByteArray::number(0) + " INF (RESOURCE \"testresource\") ()" + // << "S: IGNORE 101005" + // << "S: 2 OK List completed"; + // QTest::newRow("recursive list filtered by mimetype") << scenario; + // } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(toplevel.id(), Protocol::FetchCollectionsCommand::AllCollections, + Protocol::Ancestor::AllAncestors, { QLatin1String("mimetype1") }, QLatin1String("testresource"))) + << TestScenario::ignore(101005) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchCollectionsResponse()); + QTest::newRow("recursive list filtered by mimetype with ancestors") << scenarios; + } + } + + void testListEnabledBenchmark() + { + QFETCH(TestScenario::List, scenarios); + // StorageDebugger::instance()->enableSQLDebugging(true); + // StorageDebugger::instance()->writeToFile(QLatin1String("sqllog.txt")); + + QBENCHMARK { + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + } + +#endif +}; + +AKTEST_FAKESERVER_MAIN(CollectionFetchHandlerTest) + +#include "collectionfetchhandlertest.moc" diff --git a/autotests/server/collectionmodifyhandlertest.cpp b/autotests/server/collectionmodifyhandlertest.cpp new file mode 100644 index 0000000..0f57310 --- /dev/null +++ b/autotests/server/collectionmodifyhandlertest.cpp @@ -0,0 +1,186 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include + +#include + +#include "aktest.h" +#include "entities.h" +#include "fakeakonadiserver.h" + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class CollectionModifyHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + CollectionModifyHandlerTest() + { + mAkonadi.init(); + } + +private Q_SLOTS: + void testModify_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("expectedNotifications"); + QTest::addColumn("newValue"); + + auto notificationTemplate = Protocol::CollectionChangeNotificationPtr::create(); + notificationTemplate->setOperation(Protocol::CollectionChangeNotification::Modify); + notificationTemplate->setParentCollection(4); + notificationTemplate->setResource("akonadi_fake_resource_0"); + notificationTemplate->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + auto collectionTemplate = Protocol::FetchCollectionsResponsePtr::create(); + collectionTemplate->setId(5); + collectionTemplate->setRemoteId(QStringLiteral("ColD")); + collectionTemplate->setRemoteRevision(QStringLiteral("")); + collectionTemplate->setName(QStringLiteral("New Name")); + collectionTemplate->setParentId(4); + collectionTemplate->setResource(QStringLiteral("akonadi_fake_resource_0")); + + { + auto cmd = Protocol::ModifyCollectionCommandPtr::create(5); + cmd->setName(QStringLiteral("New Name")); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponsePtr::create()); + + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setChangedParts(QSet() << "NAME"); + notification->setCollection(*collectionTemplate); + QTest::newRow("modify collection") << scenarios << Protocol::ChangeNotificationList{notification} + << QVariant::fromValue(QStringLiteral("New Name")); + } + { + auto cmd = Protocol::ModifyCollectionCommandPtr::create(5); + cmd->setEnabled(false); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponsePtr::create()); + + Protocol::FetchCollectionsResponse collection(*collectionTemplate); + collection.setEnabled(false); + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setChangedParts(QSet() << "ENABLED"); + notification->setCollection(collection); + auto unsubscribeNotification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + unsubscribeNotification->setOperation(Protocol::CollectionChangeNotification::Unsubscribe); + unsubscribeNotification->setCollection(std::move(collection)); + + QTest::newRow("disable collection") << scenarios << Protocol::ChangeNotificationList{notification, unsubscribeNotification} + << QVariant::fromValue(false); + } + { + auto cmd = Protocol::ModifyCollectionCommandPtr::create(5); + cmd->setEnabled(true); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponsePtr::create()); + + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setChangedParts(QSet() << "ENABLED"); + notification->setCollection(*collectionTemplate); + auto subscribeNotification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + subscribeNotification->setOperation(Protocol::CollectionChangeNotification::Subscribe); + subscribeNotification->setCollection(*collectionTemplate); + + QTest::newRow("enable collection") << scenarios << Protocol::ChangeNotificationList{notification, subscribeNotification} + << QVariant::fromValue(true); + } + { + auto cmd = Protocol::ModifyCollectionCommandPtr::create(5); + cmd->setEnabled(false); + cmd->setSyncPref(Tristate::True); + cmd->setDisplayPref(Tristate::True); + cmd->setIndexPref(Tristate::True); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyCollectionResponsePtr::create()); + + Protocol::FetchCollectionsResponse collection(*collectionTemplate); + collection.setEnabled(false); + collection.setSyncPref(Tristate::True); + collection.setDisplayPref(Tristate::True); + collection.setIndexPref(Tristate::True); + auto notification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + notification->setChangedParts(QSet() << "ENABLED" + << "SYNC" + << "DISPLAY" + << "INDEX"); + notification->setCollection(collection); + auto unsubscribeNotification = Protocol::CollectionChangeNotificationPtr::create(*notificationTemplate); + unsubscribeNotification->setOperation(Protocol::CollectionChangeNotification::Unsubscribe); + unsubscribeNotification->setCollection(std::move(collection)); + + QTest::newRow("local override enable") << scenarios << Protocol::ChangeNotificationList{notification, unsubscribeNotification} + << QVariant::fromValue(true); + } + } + + void testModify() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + QFETCH(QVariant, newValue); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + auto notificationSpy = mAkonadi.notificationSpy(); + if (expectedNotifications.isEmpty()) { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + return; + } + QTRY_COMPARE(notificationSpy->count(), 1); + // Only one notify call + QCOMPARE(notificationSpy->first().count(), 1); + const auto receivedNotifications = notificationSpy->first().first().value(); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + + for (int i = 0; i < expectedNotifications.size(); i++) { + const auto recvNtf = receivedNotifications.at(i).staticCast(); + const auto expNtf = expectedNotifications.at(i).staticCast(); + if (*recvNtf != *expNtf) { + qDebug() << "Actual: " << Protocol::debugString(recvNtf); + qDebug() << "Expected:" << Protocol::debugString(expNtf); + } + QCOMPARE(*recvNtf, *expNtf); + const auto notification = receivedNotifications.at(i).staticCast(); + if (notification->changedParts().contains("NAME")) { + Collection col = Collection::retrieveById(notification->collection().id()); + QCOMPARE(col.name(), newValue.toString()); + } + if (!notification->changedParts().intersects({"ENABLED", "SYNC", "DISPLAY", "INDEX"})) { + Collection col = Collection::retrieveById(notification->collection().id()); + const bool sync = col.syncPref() == Collection::Undefined ? col.enabled() : col.syncPref() == Collection::True; + QCOMPARE(sync, newValue.toBool()); + const bool display = col.displayPref() == Collection::Undefined ? col.enabled() : col.displayPref() == Collection::True; + QCOMPARE(display, newValue.toBool()); + const bool index = col.indexPref() == Collection::Undefined ? col.enabled() : col.indexPref() == Collection::True; + QCOMPARE(index, newValue.toBool()); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(CollectionModifyHandlerTest) + +#include "collectionmodifyhandlertest.moc" diff --git a/autotests/server/collectionschedulertest.cpp b/autotests/server/collectionschedulertest.cpp new file mode 100644 index 0000000..2898ca3 --- /dev/null +++ b/autotests/server/collectionschedulertest.cpp @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "fakeakonadiserver.h" +#include "fakeintervalcheck.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using TimePoint = CollectionScheduler::TimePoint; + +using namespace std::literals::chrono_literals; + +class CollectionSchedulerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; +private Q_SLOTS: + void initTestCase() + { + mAkonadi.init(); + } + + void shouldInitializeSyncIntervals() + { + // WHEN + FakeIntervalCheck sched(mAkonadi.itemRetrievalManager()); + sched.waitForInit(); + const TimePoint now(std::chrono::steady_clock::now()); + // THEN + // Collections root (1), ColA (2), ColB (3), ColD (5), virtual (6) and virtual2 (7) + // should have a check scheduled in 5 minutes (default value) + for (qint64 collectionId : {1, 2, 3, 5, 6, 7}) { + QVERIFY2(sched.nextScheduledTime(collectionId) > now + 4min, qPrintable(QString::number(collectionId))); + QVERIFY(sched.nextScheduledTime(collectionId) < now + 6min); + } + QCOMPARE(sched.nextScheduledTime(4).time_since_epoch(), TimePoint::duration::zero()); // ColC is skipped because syncPref=false + QCOMPARE(sched.nextScheduledTime(314).time_since_epoch(), TimePoint::duration::zero()); // no such collection + QVERIFY(sched.currentTimerInterval() > 4min); + QVERIFY(sched.currentTimerInterval() < 6min); + } + + // (not that this feature is really used right now, it defaults to 5 and CacheCleaner sets it to 5) + void shouldObeyMinimumInterval() + { + // GIVEN + FakeIntervalCheck sched(mAkonadi.itemRetrievalManager()); + // WHEN + sched.setMinimumInterval(10); + sched.waitForInit(); + // THEN + const TimePoint now(std::chrono::steady_clock::now()); + QTRY_VERIFY(sched.nextScheduledTime(2).time_since_epoch() > TimePoint::duration::zero()); + QVERIFY(sched.nextScheduledTime(2) > now + 9min); + QVERIFY(sched.nextScheduledTime(2) < now + 11min); + QVERIFY(sched.currentTimerInterval() > 9min); + QVERIFY(sched.currentTimerInterval() < 11min); + } + + void shouldRemoveAndAddCollectionFromSchedule() + { + // GIVEN + FakeIntervalCheck sched(mAkonadi.itemRetrievalManager()); + sched.waitForInit(); + const auto timeForRoot = sched.nextScheduledTime(1); + const auto timeForColB = sched.nextScheduledTime(3); + QVERIFY(sched.nextScheduledTime(2) <= timeForColB); + // WHEN + sched.collectionRemoved(2); + // THEN + QTRY_COMPARE(sched.nextScheduledTime(2).time_since_epoch(), TimePoint::duration::zero()); + QCOMPARE(sched.nextScheduledTime(1), timeForRoot); // unchanged + QCOMPARE(sched.nextScheduledTime(3), timeForColB); // unchanged + QVERIFY(sched.currentTimerInterval() > 4min); // unchanged + QVERIFY(sched.currentTimerInterval() < 6min); // unchanged + + // AND WHEN re-adding the collection + QTest::qWait(1000); // we only have precision to the second... + sched.collectionAdded(2); + // THEN + QTRY_VERIFY(sched.nextScheduledTime(2).time_since_epoch() > TimePoint::duration::zero()); + // This is unchanged, even though it would normally have been 1s later. See "minor optimization" in scheduler. + QCOMPARE(sched.nextScheduledTime(2), timeForColB); + QCOMPARE(sched.nextScheduledTime(1), timeForRoot); // unchanged + QCOMPARE(sched.nextScheduledTime(3), timeForColB); // unchanged + QVERIFY(sched.currentTimerInterval() > 4min); // unchanged + QVERIFY(sched.currentTimerInterval() < 6min); // unchanged + } + + void shouldHonourIntervalChange() + { + // GIVEN + FakeIntervalCheck sched(mAkonadi.itemRetrievalManager()); + sched.waitForInit(); + const auto timeForColB = sched.nextScheduledTime(3); + Collection colA = Collection::retrieveByName(QStringLiteral("Collection A")); + QCOMPARE(colA.id(), 2); + QVERIFY(sched.nextScheduledTime(2) <= timeForColB); + // WHEN + colA.setCachePolicyInherit(false); + colA.setCachePolicyCheckInterval(20); // in minutes + QVERIFY(colA.update()); + sched.collectionChanged(2); + // THEN + // "in 20 minutes" is 15 minutes later than "in 5 minutes" + QTRY_VERIFY(sched.nextScheduledTime(2) >= timeForColB + 14min); + QVERIFY(sched.nextScheduledTime(2) <= timeForColB + 16min); + QVERIFY(sched.currentTimerInterval() > 4min); // unchanged + QVERIFY(sched.currentTimerInterval() < 6min); // unchanged + } +}; + +AKTEST_FAKESERVER_MAIN(CollectionSchedulerTest) + +#include "collectionschedulertest.moc" diff --git a/autotests/server/collectionstatisticstest.cpp b/autotests/server/collectionstatisticstest.cpp new file mode 100644 index 0000000..b058517 --- /dev/null +++ b/autotests/server/collectionstatisticstest.cpp @@ -0,0 +1,184 @@ +/* + * SPDX-FileCopyrightText: 2016 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include + +#include "aktest.h" +#include "dbinitializer.h" +#include "fakeakonadiserver.h" +#include "storage/collectionstatistics.h" + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::Collection) + +class IntrospectableCollectionStatistics : public CollectionStatistics +{ +public: + explicit IntrospectableCollectionStatistics(bool prefetch) + : CollectionStatistics(prefetch) + , mCalculationsCount(0) + { + } + ~IntrospectableCollectionStatistics() override + { + } + + int calculationsCount() const + { + return mCalculationsCount; + } + +protected: + Statistics calculateCollectionStatistics(const Collection &col) override + { + ++mCalculationsCount; + return CollectionStatistics::calculateCollectionStatistics(col); + } + +private: + int mCalculationsCount; +}; + +class CollectionStatisticsTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + std::unique_ptr dbInitializer; + +public: + CollectionStatisticsTest() + { + qRegisterMetaType(); + + mAkonadi.setPopulateDb(false); + mAkonadi.init(); + + dbInitializer = std::make_unique(); + } + +private Q_SLOTS: + void testPrefetch_data() + { + dbInitializer->createResource("testresource"); + auto col1 = dbInitializer->createCollection("col1"); + dbInitializer->createItem("item1", col1); + dbInitializer->createItem("item2", col1); + auto col2 = dbInitializer->createCollection("col2"); + // empty + auto col3 = dbInitializer->createCollection("col3"); + dbInitializer->createItem("item3", col3); + + QTest::addColumn("collection"); + QTest::addColumn("calculationsCount"); + QTest::addColumn("count"); + QTest::addColumn("read"); + QTest::addColumn("size"); + + QTest::newRow("col1") << col1 << 0 << 2LL << 0LL << 0LL; + QTest::newRow("col2") << col2 << 0 << 0LL << 0LL << 0LL; + QTest::newRow("col3") << col3 << 0 << 1LL << 0LL << 0LL; + } + + void testPrefetch() + { + QFETCH(Collection, collection); + QFETCH(int, calculationsCount); + QFETCH(qint64, count); + QFETCH(qint64, read); + QFETCH(qint64, size); + + IntrospectableCollectionStatistics cs(true); + auto stats = cs.statistics(collection); + QCOMPARE(cs.calculationsCount(), calculationsCount); + QCOMPARE(stats.count, count); + QCOMPARE(stats.read, read); + QCOMPARE(stats.size, size); + } + + void testCalculateStats() + { + dbInitializer->cleanup(); + dbInitializer->createResource("testresource"); + auto col = dbInitializer->createCollection("col1"); + dbInitializer->createItem("item1", col); + dbInitializer->createItem("item2", col); + dbInitializer->createItem("item3", col); + + IntrospectableCollectionStatistics cs(false); + auto stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 3); + QCOMPARE(stats.read, 0); + QCOMPARE(stats.size, 0); + } + + void testSeenChanged() + { + dbInitializer->cleanup(); + dbInitializer->createResource("testresource"); + auto col = dbInitializer->createCollection("col1"); + dbInitializer->createItem("item1", col); + dbInitializer->createItem("item2", col); + dbInitializer->createItem("item3", col); + + IntrospectableCollectionStatistics cs(false); + auto stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 3); + QCOMPARE(stats.read, 0); + QCOMPARE(stats.size, 0); + + cs.itemsSeenChanged(col, 2); + stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 3); + QCOMPARE(stats.read, 2); + QCOMPARE(stats.size, 0); + + cs.itemsSeenChanged(col, -1); + stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 3); + QCOMPARE(stats.read, 1); + QCOMPARE(stats.size, 0); + } + + void testItemAdded() + { + dbInitializer->cleanup(); + dbInitializer->createResource("testresource"); + auto col = dbInitializer->createCollection("col1"); + dbInitializer->createItem("item1", col); + + IntrospectableCollectionStatistics cs(false); + auto stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 1); + QCOMPARE(stats.read, 0); + QCOMPARE(stats.size, 0); + + cs.itemAdded(col, 5, true); + stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 2); + QCOMPARE(stats.read, 1); + QCOMPARE(stats.size, 5); + + cs.itemAdded(col, 3, false); + stats = cs.statistics(col); + QCOMPARE(cs.calculationsCount(), 1); + QCOMPARE(stats.count, 3); + QCOMPARE(stats.read, 1); + QCOMPARE(stats.size, 8); + } +}; + +AKTEST_MAIN(CollectionStatisticsTest) + +#include "collectionstatisticstest.moc" diff --git a/autotests/server/collectiontreecachetest.cpp b/autotests/server/collectiontreecachetest.cpp new file mode 100644 index 0000000..b9f6a00 --- /dev/null +++ b/autotests/server/collectiontreecachetest.cpp @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "storage/collectiontreecache.h" +#include "aktest.h" +#include "dbinitializer.h" +#include "fakeakonadiserver.h" +#include "private/scope_p.h" +#include "storage/selectquerybuilder.h" + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class InspectableCollectionTreeCache : public CollectionTreeCache +{ + Q_OBJECT +public: + InspectableCollectionTreeCache() + : CollectionTreeCache() + , mCachePopulated(0) + { + } + + bool waitForCachePopulated() + { + QSignalSpy spy(this, &InspectableCollectionTreeCache::cachePopulated); + return mCachePopulated == 1 || spy.wait(5000); + } + +Q_SIGNALS: + void cachePopulated(); + +protected: + void init() Q_DECL_OVERRIDE + { + CollectionTreeCache::init(); + mCachePopulated = 1; + Q_EMIT cachePopulated(); + } + + void quit() Q_DECL_OVERRIDE + { + } + +private: + QAtomicInt mCachePopulated; +}; + +class CollectionTreeCacheTest : public QObject +{ + Q_OBJECT + +public: + CollectionTreeCacheTest() + { + try { + FakeAkonadiServer::instance()->init(); + } catch (const FakeAkonadiServerException &e) { + qWarning() << "Server exception: " << e.what(); + qFatal("Fake Akonadi Server failed to start up, aborting test"); + } + } + + ~CollectionTreeCacheTest() + { + FakeAkonadiServer::instance()->quit(); + } + +private: + void populateDb(DbInitializer &db) + { + // ResA + // |- Col A1 + // |- Col A2 + // | |- Col A3 + // | |- Col A7 + // | |- Col A5 + // | |- Col A8 + // |- Col A6 + // | |- Col A10 + // |- Col A9 + auto res = db.createResource("TestResource"); + auto resA = db.createCollection("Res A", Collection()); + auto colA1 = db.createCollection("Col A1", resA); + auto colA2 = db.createCollection("Col A2", resA); + auto colA3 = db.createCollection("Col A3", colA2); + auto colA5 = db.createCollection("Col A5", colA2); + auto colA6 = db.createCollection("Col A6", resA); + auto colA7 = db.createCollection("Col A7", colA2); + auto colA8 = db.createCollection("Col A8", colA7); + auto colA9 = db.createCollection("Col A9", resA); + auto colA10 = db.createCollection("Col A10", colA6); + + // Move the collection to the final parent + colA5.setParent(colA7); + colA5.update(); + } + +private Q_SLOTS: + void populateTest() + { + DbInitializer db; + populateDb(db); + + InspectableCollectionTreeCache treeCache; + QVERIFY(treeCache.waitForCachePopulated()); + + auto allCols = treeCache.retrieveCollections(Scope(), std::numeric_limits::max(), 1); + + SelectQueryBuilder qb; + QVERIFY(qb.exec()); + auto expCols = qb.result(); + + const auto sort = [](const Collection &l, const Collection &r) { + return l.id() < r.id(); + }; + std::sort(allCols.begin(), allCols.end(), sort); + std::sort(expCols.begin(), expCols.end(), sort); + + QCOMPARE(allCols.size(), expCols.size()); + QCOMPARE(allCols, expCols); + } +}; + +AKTEST_FAKESERVER_MAIN(CollectionTreeCacheTest) + +#include "collectiontreecachetest.moc" diff --git a/autotests/server/dbconfigtest.cpp b/autotests/server/dbconfigtest.cpp new file mode 100644 index 0000000..fc88945 --- /dev/null +++ b/autotests/server/dbconfigtest.cpp @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#define QL1S(x) QStringLiteral(x) + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +class TestableDbConfigPostgresql : public DbConfigPostgresql +{ +public: + QStringList postgresSearchPaths(const QTemporaryDir &dir) + { + return DbConfigPostgresql::postgresSearchPaths(dir.path()); + } +}; + +class DbConfigTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDbConfig() + { + // doesn't work, DbConfig has an internal singleton-like cache... + // QFETCH( QString, driverName ); + const QString driverName(QL1S(AKONADI_DATABASE_BACKEND)); + + // isolated config file to not conflict with a running instance + akTestSetInstanceIdentifier(QL1S("unit-test")); + + { + QSettings s(StandardDirs::serverConfigFile(StandardDirs::WriteOnly)); + s.setValue(QL1S("General/Driver"), driverName); + } + + QScopedPointer cfg(DbConfig::configuredDatabase()); + + QVERIFY(!cfg.isNull()); + QCOMPARE(cfg->driverName(), driverName); + QCOMPARE(cfg->databaseName(), QL1S("akonadi")); + QCOMPARE(cfg->useInternalServer(), true); + QCOMPARE(cfg->sizeThreshold(), 4096LL); + } + + void testPostgresVersionedLookup() + { + QTemporaryDir dir; + QVERIFY(dir.isValid()); + + const QStringList versions{QStringLiteral("10.2"), + QStringLiteral("10.0"), + QStringLiteral("9.5"), + QStringLiteral("12.4"), + QStringLiteral("8.0"), + QStringLiteral("12.0")}; + for (const auto &version : versions) { + QVERIFY(QDir(dir.path()).mkdir(version)); + } + + TestableDbConfigPostgresql dbConfig; + const auto paths = dbConfig.postgresSearchPaths(dir) | Views::filter([&dir](const auto &path) { + return path.startsWith(dir.path()); + }) + | Views::transform([&dir](const auto &path) { + return QString(path).remove(dir.path() + QStringLiteral("/")).remove(QStringLiteral("/bin")); + }) + | Actions::toQList; + + const QStringList expected{QStringLiteral("12.4"), + QStringLiteral("12.0"), + QStringLiteral("10.2"), + QStringLiteral("10.0"), + QStringLiteral("9.5"), + QStringLiteral("8.0")}; + QCOMPARE(paths, expected); + } +}; + +AKTEST_MAIN(DbConfigTest) + +#include "dbconfigtest.moc" diff --git a/autotests/server/dbdeadlockcatchertest.cpp b/autotests/server/dbdeadlockcatchertest.cpp new file mode 100644 index 0000000..91cd3ba --- /dev/null +++ b/autotests/server/dbdeadlockcatchertest.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include + +#include "storage/dbdeadlockcatcher.h" + +#include + +using namespace Akonadi::Server; + +class DbDeadlockCatcherTest : public QObject +{ + Q_OBJECT + +private: + int m_myFuncCalled = 0; + void myFunc(int maxRecursion) + { + ++m_myFuncCalled; + if (m_myFuncCalled <= maxRecursion) { + throw DbDeadlockException(QSqlQuery()); + } + } + +private Q_SLOTS: + void testRecurseOnce() + { + m_myFuncCalled = 0; + DbDeadlockCatcher catcher([this]() { + myFunc(1); + }); + QCOMPARE(m_myFuncCalled, 2); + } + + void testRecurseTwice() + { + m_myFuncCalled = 0; + DbDeadlockCatcher catcher([this]() { + myFunc(2); + }); + QCOMPARE(m_myFuncCalled, 3); + } + + void testHitRecursionLimit() + { + m_myFuncCalled = 0; + QVERIFY_EXCEPTION_THROWN(DbDeadlockCatcher catcher([this]() { + myFunc(10); + }), + DbDeadlockException); + QCOMPARE(m_myFuncCalled, 6); + } +}; + +AKTEST_MAIN(DbDeadlockCatcherTest) + +#include "dbdeadlockcatchertest.moc" diff --git a/autotests/server/dbinitializer.cpp b/autotests/server/dbinitializer.cpp new file mode 100644 index 0000000..89294e5 --- /dev/null +++ b/autotests/server/dbinitializer.cpp @@ -0,0 +1,221 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "dbinitializer.h" +#include "akonadiserver_debug.h" + +#include +#include +#include + +#include "shared/akranges.h" + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +DbInitializer::~DbInitializer() +{ + cleanup(); +} + +Resource DbInitializer::createResource(const char *name) +{ + Resource res; + qint64 id = -1; + res.setName(QLatin1String(name)); + const bool ret = res.insert(&id); + Q_ASSERT(ret); + Q_UNUSED(ret) + mResource = res; + return res; +} + +Collection DbInitializer::createCollection(const char *name, const Collection &parent) +{ + Collection col; + if (parent.isValid()) { + col.setParent(parent); + } + col.setName(QLatin1String(name)); + col.setRemoteId(QLatin1String(name)); + col.setResource(mResource); + const bool ret = col.insert(); + Q_ASSERT(ret); + Q_UNUSED(ret) + return col; +} + +PimItem DbInitializer::createItem(const char *name, const Collection &parent) +{ + PimItem item; + MimeType mimeType = MimeType::retrieveByName(QStringLiteral("test")); + if (!mimeType.isValid()) { + MimeType mt; + mt.setName(QStringLiteral("test")); + mt.insert(); + mimeType = mt; + } + item.setMimeType(mimeType); + item.setCollection(parent); + item.setRemoteId(QLatin1String(name)); + const bool ret = item.insert(); + Q_ASSERT(ret); + Q_UNUSED(ret) + return item; +} + +Part DbInitializer::createPart(qint64 pimItem, const QByteArray &partName, const QByteArray &partData) +{ + auto partType = PartTypeHelper::parseFqName(QString::fromLatin1(partName)); + PartType type = PartType::retrieveByFQNameOrCreate(partType.first, partType.second); + + Part part; + part.setPimItemId(pimItem); + part.setPartTypeId(type.id()); + part.setData(partData); + part.setDatasize(partData.size()); + const bool ret = part.insert(); + Q_ASSERT(ret); + Q_UNUSED(ret) + return part; +} + +QByteArray DbInitializer::toByteArray(bool enabled) +{ + if (enabled) { + return "TRUE"; + } + return "FALSE"; +} + +QByteArray DbInitializer::toByteArray(Collection::Tristate tristate) +{ + switch (tristate) { + case Collection::True: + return "TRUE"; + case Collection::False: + return "FALSE"; + case Collection::Undefined: + default: + break; + } + return "DEFAULT"; +} + +Akonadi::Protocol::FetchCollectionsResponsePtr +DbInitializer::listResponse(const Collection &col, bool ancestors, bool mimetypes, const QStringList &ancestorFetchScope) +{ + auto resp = Akonadi::Protocol::FetchCollectionsResponsePtr::create(col.id()); + resp->setParentId(col.parentId()); + resp->setName(col.name()); + if (mimetypes) { + resp->setMimeTypes(col.mimeTypes() | Views::transform(&MimeType::name) | Actions::toQList); + } + resp->setRemoteId(col.remoteId()); + resp->setRemoteRevision(col.remoteRevision()); + resp->setResource(col.resource().name()); + resp->setIsVirtual(col.isVirtual()); + Akonadi::Protocol::CachePolicy cp; + cp.setInherit(true); + cp.setLocalParts({QLatin1String("ALL")}); + resp->setCachePolicy(cp); + if (ancestors) { + QVector ancs; + Collection parent = col.parent(); + while (parent.isValid()) { + Akonadi::Protocol::Ancestor anc; + anc.setId(parent.id()); + anc.setRemoteId(parent.remoteId()); + anc.setName(parent.name()); + if (!ancestorFetchScope.isEmpty()) { + anc.setRemoteId(parent.remoteId()); + Akonadi::Protocol::Attributes attrs; + Q_FOREACH (const CollectionAttribute &attr, parent.attributes()) { + if (ancestorFetchScope.contains(QString::fromLatin1(attr.type()))) { + attrs.insert(attr.type(), attr.value()); + } + } + attrs.insert("ENABLED", parent.enabled() ? "TRUE" : "FALSE"); + anc.setAttributes(attrs); + } + parent = parent.parent(); + ancs.push_back(anc); + } + // Root + ancs.push_back(Akonadi::Protocol::Ancestor(0)); + resp->setAncestors(ancs); + } + resp->setEnabled(col.enabled()); + resp->setDisplayPref(static_cast(col.displayPref())); + resp->setSyncPref(static_cast(col.syncPref())); + resp->setIndexPref(static_cast(col.indexPref())); + + Akonadi::Protocol::Attributes attrs; + Q_FOREACH (const CollectionAttribute &attr, col.attributes()) { + attrs.insert(attr.type(), attr.value()); + } + resp->setAttributes(attrs); + return resp; +} + +Akonadi::Protocol::FetchItemsResponsePtr DbInitializer::fetchResponse(const PimItem &item) +{ + auto resp = Akonadi::Protocol::FetchItemsResponsePtr::create(); + resp->setId(item.id()); + resp->setRevision(item.rev()); + resp->setMimeType(item.mimeType().name()); + resp->setRemoteId(item.remoteId()); + resp->setParentId(item.collectionId()); + resp->setSize(item.size()); + resp->setMTime(item.datetime()); + resp->setRemoteRevision(item.remoteRevision()); + resp->setGid(item.gid()); + const auto flags = item.flags(); + QVector flagNames; + for (const auto &flag : flags) { + flagNames.push_back(flag.name().toUtf8()); + } + resp->setFlags(flagNames); + + return resp; +} + +Collection DbInitializer::collection(const char *name) +{ + return Collection::retrieveByName(QLatin1String(name)); +} + +void DbInitializer::cleanup() +{ + Q_FOREACH (Collection col, mResource.collections()) { // krazy:exclude=foreach + if (!col.isVirtual()) { + col.remove(); + } + } + mResource.remove(); + + if (DataStore::self()->database().isOpen()) { + { + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.exec(); + } + { + QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete); + qb.exec(); + } + { + QueryBuilder qb(TagType::tableName(), QueryBuilder::Delete); + qb.exec(); + } + } + + Q_FOREACH (Part part, Part::retrieveAll()) { // krazy:exclude=foreach + part.remove(); + } + Q_FOREACH (PimItem item, PimItem::retrieveAll()) { // krazy:exclude=foreach + item.remove(); + } +} diff --git a/autotests/server/dbinitializer.h b/autotests/server/dbinitializer.h new file mode 100644 index 0000000..b945d2b --- /dev/null +++ b/autotests/server/dbinitializer.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include "entities.h" +#include + +class DbInitializer +{ +public: + ~DbInitializer(); + Akonadi::Server::Resource createResource(const char *name); + Akonadi::Server::Collection createCollection(const char *name, const Akonadi::Server::Collection &parent = Akonadi::Server::Collection()); + Akonadi::Server::PimItem createItem(const char *name, const Akonadi::Server::Collection &parent); + Akonadi::Server::Part createPart(qint64 pimitemId, const QByteArray &partname, const QByteArray &data); + QByteArray toByteArray(bool enabled); + QByteArray toByteArray(Akonadi::Server::Collection::Tristate tristate); + Akonadi::Protocol::FetchCollectionsResponsePtr + listResponse(const Akonadi::Server::Collection &col, bool ancestors = false, bool mimetypes = true, const QStringList &ancestorFetchScope = QStringList()); + Akonadi::Protocol::FetchItemsResponsePtr fetchResponse(const Akonadi::Server::PimItem &item); + Akonadi::Server::Collection collection(const char *name); + + void cleanup(); + +private: + Akonadi::Server::Resource mResource; +}; + diff --git a/autotests/server/dbinitializertest.cpp b/autotests/server/dbinitializertest.cpp new file mode 100644 index 0000000..b1b9bd1 --- /dev/null +++ b/autotests/server/dbinitializertest.cpp @@ -0,0 +1,177 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbinitializertest.h" +#include "unittestschema.h" +#include + +#define DBINITIALIZER_UNITTEST +#include "storage/dbinitializer.cpp" +#undef DBINITIALIZER_UNITTEST + +#include + +#define QL1S(x) QLatin1String(x) + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QVector) + +class StatementCollector : public TestInterface +{ +public: + void execStatement(const QString &statement) override + { + statements.push_back(statement); + } + + QStringList statements; +}; + +class DbFakeIntrospector : public DbIntrospector +{ +public: + explicit DbFakeIntrospector(const QSqlDatabase &database) + : DbIntrospector(database) + , m_hasTable(false) + , m_hasIndex(false) + , m_tableEmpty(true) + { + } + + bool hasTable(const QString &tableName) override + { + Q_UNUSED(tableName) + return m_hasTable; + } + bool hasIndex(const QString &tableName, const QString &indexName) override + { + Q_UNUSED(tableName) + Q_UNUSED(indexName) + return m_hasIndex; + } + bool hasColumn(const QString &tableName, const QString &columnName) override + { + Q_UNUSED(tableName) + Q_UNUSED(columnName) + return false; + } + bool isTableEmpty(const QString &tableName) override + { + Q_UNUSED(tableName) + return m_tableEmpty; + } + QVector foreignKeyConstraints(const QString &tableName) override + { + Q_UNUSED(tableName) + return m_foreignKeys; + } + + QVector m_foreignKeys; + bool m_hasTable; + bool m_hasIndex; + bool m_tableEmpty; +}; + +void DbInitializerTest::initTestCase() +{ + Q_INIT_RESOURCE(akonadidb); +} + +void DbInitializerTest::testRun_data() +{ + QTest::addColumn("driverName"); + QTest::addColumn("filename"); + QTest::addColumn("hasTable"); + QTest::addColumn>("fks"); + QTest::addColumn("hasFks"); + + QVector fks; + + QTest::newRow("mysql") << "QMYSQL" + << ":dbinit_mysql" << false << fks << true; + QTest::newRow("sqlite") << "QSQLITE" + << ":dbinit_sqlite" << false << fks << true; + QTest::newRow("psql") << "QPSQL" + << ":dbinit_psql" << false << fks << true; + + DbIntrospector::ForeignKey fk; + fk.name = QL1S("myForeignKeyIdentifier"); + fk.column = QL1S("collectionId"); + fk.refTable = QL1S("CollectionTable"); + fk.refColumn = QL1S("id"); + fk.onUpdate = QL1S("RESTRICT"); + fk.onDelete = QL1S("CASCADE"); + fks.push_back(fk); + + QTest::newRow("mysql (incremental)") << "QMYSQL" + << ":dbinit_mysql_incremental" << true << fks << true; + QTest::newRow("sqlite (incremental)") << "QSQLITE" + << ":dbinit_sqlite_incremental" << true << fks << true; + QTest::newRow("psql (incremental)") << "QPSQL" + << ":dbinit_psql_incremental" << true << fks << true; +} + +void DbInitializerTest::testRun() +{ + QFETCH(QString, driverName); + QFETCH(QString, filename); + QFETCH(bool, hasTable); + QFETCH(QVector, fks); + QFETCH(bool, hasFks); + + QFile file(filename); + QVERIFY(file.open(QFile::ReadOnly)); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName); + UnitTestSchema schema; + DbInitializer::Ptr initializer = DbInitializer::createInstance(db, &schema); + QVERIFY(bool(initializer)); + + StatementCollector collector; + initializer->setTestInterface(&collector); + auto introspector = new DbFakeIntrospector(db); + introspector->m_hasTable = hasTable; + introspector->m_hasIndex = hasTable; + introspector->m_tableEmpty = !hasTable; + introspector->m_foreignKeys = fks; + initializer->setIntrospector(DbIntrospector::Ptr(introspector)); + + QVERIFY(initializer->run()); + QVERIFY(initializer->updateIndexesAndConstraints()); + QVERIFY(!collector.statements.isEmpty()); + + Q_FOREACH (const QString &statement, collector.statements) { + const QString expected = readNextStatement(&file).simplified(); + + QString normalized = statement.simplified(); + normalized.replace(QLatin1String(" ,"), QLatin1String(",")); + normalized.replace(QLatin1String(" )"), QLatin1String(")")); + QCOMPARE(normalized, expected); + } + + QVERIFY(initializer->errorMsg().isEmpty()); + QCOMPARE(initializer->hasForeignKeyConstraints(), hasFks); + } +} + +QString DbInitializerTest::readNextStatement(QIODevice *io) +{ + QString statement; + while (!io->atEnd()) { + const QString line = QString::fromUtf8(io->readLine()); + if (line.trimmed().isEmpty() && !statement.isEmpty()) { + return statement; + } + statement += line; + } + + return statement; +} + +AKTEST_MAIN(DbInitializerTest) diff --git a/autotests/server/dbinitializertest.h b/autotests/server/dbinitializertest.h new file mode 100644 index 0000000..0287460 --- /dev/null +++ b/autotests/server/dbinitializertest.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +class QIODevice; +class DbInitializerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void testRun_data(); + void testRun(); + +private: + static QString readNextStatement(QIODevice *io); +}; + diff --git a/autotests/server/dbintrospectortest.cpp b/autotests/server/dbintrospectortest.cpp new file mode 100644 index 0000000..1cac7fc --- /dev/null +++ b/autotests/server/dbintrospectortest.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include + +#include +#include +#include + +#define QL1S(x) QLatin1String(x) + +using namespace Akonadi::Server; + +class DbIntrospectorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testHasIndexQuery_data() + { + QTest::addColumn("driverName"); + QTest::addColumn("indexQuery"); + + QTest::newRow("mysql") << "QMYSQL" + << "SHOW INDEXES FROM myTable WHERE `Key_name` = 'myIndex'"; + QTest::newRow("sqlite") << "QSQLITE" + << "SELECT * FROM sqlite_master WHERE type='index' AND tbl_name='myTable' AND name='myIndex';"; + QTest::newRow("psql") << "QPSQL" + << "SELECT indexname FROM pg_catalog.pg_indexes WHERE tablename ilike 'myTable' AND indexname ilike 'myIndex' UNION SELECT " + "conname FROM pg_catalog.pg_constraint WHERE conname ilike 'myIndex'"; + } + + void testHasIndexQuery() + { + QFETCH(QString, driverName); + QFETCH(QString, indexQuery); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName); + DbIntrospector::Ptr introspector = DbIntrospector::createInstance(db); + QVERIFY(introspector); + QCOMPARE(introspector->hasIndexQuery(QL1S("myTable"), QL1S("myIndex")), indexQuery); + } + } + + void testHasIndex_data() + { + QTest::addColumn("driverName"); + QTest::addColumn("shouldThrow"); + + QTest::newRow("mysql") << "QMYSQL" << true; + QTest::newRow("sqlite") << "QSQLITE" << true; + QTest::newRow("psql") << "QPSQL" << true; + } + + void testHasIndex() + { + QFETCH(QString, driverName); + QFETCH(bool, shouldThrow); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName + QL1S("hasIndex")); + DbIntrospector::Ptr introspector = DbIntrospector::createInstance(db); + QVERIFY(introspector); + + bool didThrow = false; + try { + QVERIFY(introspector->hasIndex(QL1S("myTable"), QL1S("myIndex"))); + } catch (const DbException &e) { + didThrow = true; + qDebug() << Q_FUNC_INFO << e.what(); + } + QCOMPARE(didThrow, shouldThrow); + } + } +}; + +AKTEST_MAIN(DbIntrospectorTest) + +#include "dbintrospectortest.moc" diff --git a/autotests/server/dbpopulator.xsl b/autotests/server/dbpopulator.xsl new file mode 100644 index 0000000..b24c23d --- /dev/null +++ b/autotests/server/dbpopulator.xsl @@ -0,0 +1,417 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + set + + + + + + + + :: + + + + + + + + + + + + collection.addMimeType(mimeType); + + + + + + + + + + pimItem.addFlag(flag); + + + + + + + + + + pimItem.addTag(tag); + + + + + + + + + + + + + + + + + + + + + + + + ; + + + + + + + + + + + + + + + + + + + + + partType.id() + + + + + mimeType.id() + + + + + + + .id() + + + + NULL + + + + + + + . + ( + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + QString() + + + QStringLiteral("") + + + + + "" + + + QDateTime::fromString(QStringLiteral(""), Qt::ISODate) + + + ); + + + + + + if (!.insert()) { + qWarning() << "Failed to insert into database"; + qWarning() << "DB Error:" << FakeDataStore::self()->database().lastError().text(); + return false; + } + qDebug() << " ' + + + + + + + + : + + + + + + + + ' inserted with id" << .id(); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#ifndef AKONADI_SERVER_DBPOPULATOR_H +#define AKONADI_SERVER_DBPOPULATOR_H + +namespace Akonadi { +namespace Server { + +class DbPopulator +{ +public: + DbPopulator(); + ~DbPopulator(); + + bool run(); + +}; + +} +} +#endif + + + + +/* + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#include "dbpopulator.h" +#include "entities.h" +#include "fakedatastore.h" + +#include <QtSql/QSqlDatabase> +#include <QtSql/QSqlQuery> +#include <QtSql/QSqlError> + +#include <QtCore/QString> +#include <QtCore/QVariant> + +using namespace Akonadi::Server; + +DbPopulator::DbPopulator() +{ +} + +DbPopulator::~DbPopulator() +{ +} + + + +bool DbPopulator::run() +{ + + + + + + MimeType + mimeType + + + + + Flag + flag + + + + + PartType + partType + + + + + Tag + tag + + + + + + + + + + + + + + + + + + + + qDebug() << "Database successfully populated"; + return true; +} + + + + + + diff --git a/autotests/server/dbtest_data/dbdata.xml b/autotests/server/dbtest_data/dbdata.xml new file mode 100644 index 0000000..fbfbe76 --- /dev/null +++ b/autotests/server/dbtest_data/dbdata.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autotests/server/dbtest_data/dbinit_mysql b/autotests/server/dbtest_data/dbinit_mysql new file mode 100644 index 0000000..8c27f7f --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_mysql @@ -0,0 +1,271 @@ +CREATE TABLE SchemaVersionTable (version INTEGER NOT NULL DEFAULT 0, + generation INTEGER NOT NULL DEFAULT 0) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO SchemaVersionTable (version) VALUES (36) + +CREATE TABLE ResourceTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE, + isVirtual BOOL DEFAULT false) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO ResourceTable (isVirtual,name) VALUES (true,'akonadi_search_resource') + +CREATE TABLE CollectionTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + remoteId VARBINARY(255), + remoteRevision VARBINARY(255), + name VARBINARY(255) NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + enabled BOOL NOT NULL DEFAULT true, + syncPref TINYINT DEFAULT 2, + displayPref TINYINT DEFAULT 2, + indexPref TINYINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT false, + cachePolicyInherit BOOL NOT NULL DEFAULT true, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false, + cachePolicyLocalParts VARBINARY(255), + queryString VARBINARY(32768), + queryAttributes VARBINARY(255), + queryCollections VARBINARY(255), + isVirtual BOOL DEFAULT false, + FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC + +INSERT INTO CollectionTable (isVirtual,name,parentId,resourceId) VALUES (true,'Search',NULL,1) + +CREATE TABLE MimeTypeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE PimItemTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + rev INTEGER NOT NULL DEFAULT 0, + remoteId VARBINARY(255), + remoteRevision VARBINARY(255), + gid VARBINARY(255), + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE FlagTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE PartTypeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL, + ns VARBINARY(255) NOT NULL) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE PartTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + pimItemId BIGINT NOT NULL, + partTypeId BIGINT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + storage TINYINT DEFAULT 0, + FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE CollectionAttributeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE TagTypeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO TagTypeTable (name) VALUES ('PLAIN') + +CREATE TABLE TagTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + gid VARBINARY(255) NOT NULL, + parentId BIGINT, + typeId BIGINT DEFAULT 1, + FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE TagAttributeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + tagId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId BIGINT NOT NULL, + resourceId BIGINT NOT NULL, + remoteId VARBINARY(255) NOT NULL, + FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE RelationTypeTable (id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARBINARY(255) NOT NULL UNIQUE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +INSERT INTO RelationTypeTable (name) VALUES ('GENERIC') + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId VARBINARY(255), + FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE) COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE PimItemTagRelation (PimItem_id BIGINT NOT NULL, + Tag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id), + FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE) + COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE) COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE) COLLATE=utf8_general_ci DEFAULT CHARSET=utf8 + +CREATE UNIQUE INDEX CollectionTable_parentAndNameIndex ON CollectionTable (parentId,name) + +CREATE INDEX CollectionTable_parentIndex ON CollectionTable (parentId) + +CREATE INDEX CollectionTable_resourceIndex ON CollectionTable (resourceId) + +CREATE INDEX CollectionTable_enabledIndex ON CollectionTable (enabled) + +CREATE INDEX CollectionTable_syncPrefIndex ON CollectionTable (syncPref) + +CREATE INDEX CollectionTable_displayPrefIndex ON CollectionTable (displayPref) + +CREATE INDEX CollectionTable_indexPrefIndex ON CollectionTable (indexPref) + +CREATE INDEX PimItemTable_collectionIndex ON PimItemTable (collectionId) + +CREATE INDEX PimItemTable_mimeTypeIndex ON PimItemTable (mimeTypeId) + +CREATE INDEX PimItemTable_gidIndex ON PimItemTable (gid) + +CREATE INDEX PimItemTable_ridIndex ON PimItemTable (remoteId) + +CREATE INDEX PimItemTable_idSortIndex ON PimItemTable (id DESC) + +CREATE UNIQUE INDEX PartTypeTable_partTypeNameIndex ON PartTypeTable (ns,name) + +CREATE UNIQUE INDEX PartTable_pimItemIdTypeIndex ON PartTable (pimItemId,partTypeId) + +CREATE INDEX PartTable_pimItemIdSortIndex ON PartTable (pimItemId DESC) + +CREATE INDEX PartTable_partTypeIndex ON PartTable (partTypeId) + +CREATE INDEX CollectionAttributeTable_collectionIndex ON CollectionAttributeTable (collectionId) + +CREATE INDEX TagTable_parentIndex ON TagTable (parentId) + +CREATE INDEX TagTable_typeIndex ON TagTable (typeId) + +CREATE INDEX TagAttributeTable_tagIndex ON TagAttributeTable (tagId) + +CREATE UNIQUE INDEX TagRemoteIdResourceRelationTable_TagAndResourceIndex ON TagRemoteIdResourceRelationTable (tagId,resourceId) + +CREATE INDEX TagRemoteIdResourceRelationTable_tagIndex ON TagRemoteIdResourceRelationTable (tagId) + +CREATE INDEX TagRemoteIdResourceRelationTable_resourceIndex ON TagRemoteIdResourceRelationTable (resourceId) + +CREATE UNIQUE INDEX RelationTable_RelationIndex ON RelationTable (leftId,rightId,typeId) + +CREATE INDEX RelationTable_leftIndex ON RelationTable (leftId) + +CREATE INDEX RelationTable_rightIndex ON RelationTable (rightId) + +CREATE INDEX RelationTable_typeIndex ON RelationTable (typeId) + +CREATE INDEX PimItemFlagRelation_PimItem_idIndex ON PimItemFlagRelation (PimItem_id) + +CREATE INDEX PimItemFlagRelation_Flag_idIndex ON PimItemFlagRelation (Flag_id) + +CREATE INDEX PimItemFlagRelation_pimItemIdSortIndex ON PimItemFlagRelation (pimitem_id DESC) + +CREATE INDEX PimItemTagRelation_PimItem_idIndex ON PimItemTagRelation (PimItem_id) + +CREATE INDEX PimItemTagRelation_Tag_idIndex ON PimItemTagRelation (Tag_id) + +CREATE INDEX CollectionMimeTypeRelation_Collection_idIndex ON CollectionMimeTypeRelation (Collection_id) + +CREATE INDEX CollectionMimeTypeRelation_MimeType_idIndex ON CollectionMimeTypeRelation (MimeType_id) + +CREATE INDEX CollectionPimItemRelation_Collection_idIndex ON CollectionPimItemRelation (Collection_id) + +CREATE INDEX CollectionPimItemRelation_PimItem_idIndex ON CollectionPimItemRelation (PimItem_id) + +ALTER TABLE CollectionTable ADD FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionTable ADD FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PartTable ADD FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PartTable ADD FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionAttributeTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagTable ADD FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagTable ADD FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE TagAttributeTable ADD FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagRemoteIdResourceRelationTable ADD FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagRemoteIdResourceRelationTable ADD FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE RelationTable ADD FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE RelationTable ADD FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE RelationTable ADD FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTagRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTagRelation ADD FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + diff --git a/autotests/server/dbtest_data/dbinit_mysql_incremental b/autotests/server/dbtest_data/dbinit_mysql_incremental new file mode 100644 index 0000000..1980ad3 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_mysql_incremental @@ -0,0 +1,209 @@ +ALTER TABLE SchemaVersionTable ADD COLUMN version INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE SchemaVersionTable ADD COLUMN generation INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE ResourceTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE ResourceTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE ResourceTable ADD COLUMN isVirtual BOOL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE CollectionTable ADD COLUMN remoteId VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN remoteRevision VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN name VARBINARY(255) NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN parentId BIGINT + +ALTER TABLE CollectionTable ADD COLUMN resourceId BIGINT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN enabled BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN syncPref TINYINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN displayPref TINYINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN indexPref TINYINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN referenced BOOL NOT NULL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyInherit BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyLocalParts VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN queryString VARBINARY(32768) + +ALTER TABLE CollectionTable ADD COLUMN queryAttributes VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN queryCollections VARBINARY(255) + +ALTER TABLE CollectionTable ADD COLUMN isVirtual BOOL DEFAULT false + +ALTER TABLE MimeTypeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE MimeTypeTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE PimItemTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE PimItemTable ADD COLUMN rev INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE PimItemTable ADD COLUMN remoteId VARBINARY(255) + +ALTER TABLE PimItemTable ADD COLUMN remoteRevision VARBINARY(255) + +ALTER TABLE PimItemTable ADD COLUMN gid VARBINARY(255) + +ALTER TABLE PimItemTable ADD COLUMN collectionId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN mimeTypeId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN dirty BOOL + +ALTER TABLE PimItemTable ADD COLUMN size BIGINT NOT NULL DEFAULT 0 + +ALTER TABLE FlagTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE FlagTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE PartTypeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE PartTypeTable ADD COLUMN name VARBINARY(255) NOT NULL + +ALTER TABLE PartTypeTable ADD COLUMN ns VARBINARY(255) NOT NULL + +ALTER TABLE PartTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE PartTable ADD COLUMN pimItemId BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN data LONGBLOB + +ALTER TABLE PartTable ADD COLUMN datasize BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN version INTEGER DEFAULT 0 + +ALTER TABLE PartTable ADD COLUMN storage TINYINT DEFAULT 0 + +ALTER TABLE CollectionAttributeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE CollectionAttributeTable ADD COLUMN collectionId BIGINT NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN type LONGBLOB NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN value LONGBLOB + +ALTER TABLE TagTypeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE TagTypeTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE TagTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE TagTable ADD COLUMN gid VARBINARY(255) NOT NULL + +ALTER TABLE TagTable ADD COLUMN parentId BIGINT + +ALTER TABLE TagTable ADD COLUMN typeId BIGINT DEFAULT 1 + +ALTER TABLE TagAttributeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE TagAttributeTable ADD COLUMN tagId BIGINT NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN type LONGBLOB NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN value LONGBLOB + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN tagId BIGINT NOT NULL + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN resourceId BIGINT NOT NULL + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN remoteId VARBINARY(255) NOT NULL + +ALTER TABLE RelationTypeTable ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY + +ALTER TABLE RelationTypeTable ADD COLUMN name VARBINARY(255) NOT NULL UNIQUE + +ALTER TABLE RelationTable ADD COLUMN leftId BIGINT NOT NULL + +ALTER TABLE RelationTable ADD COLUMN rightId BIGINT NOT NULL + +ALTER TABLE RelationTable ADD COLUMN typeId BIGINT DEFAULT 1 + +ALTER TABLE RelationTable ADD COLUMN remoteId VARBINARY(255) + +ALTER TABLE PimItemFlagRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemFlagRelation ADD COLUMN Flag_id BIGINT NOT NULL + +ALTER TABLE PimItemTagRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemTagRelation ADD COLUMN Tag_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN MimeType_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemTable DROP FOREIGN KEY myForeignKeyIdentifier + +ALTER TABLE CollectionAttributeTable DROP FOREIGN KEY myForeignKeyIdentifier + +ALTER TABLE CollectionTable ADD FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionTable ADD FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTable ADD FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PartTable ADD FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PartTable ADD FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionAttributeTable ADD FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagTable ADD FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagTable ADD FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE TagAttributeTable ADD FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagRemoteIdResourceRelationTable ADD FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE TagRemoteIdResourceRelationTable ADD FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE RelationTable ADD FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE RelationTable ADD FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE RelationTable ADD FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemFlagRelation ADD FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTagRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE PimItemTagRelation ADD FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionMimeTypeRelation ADD FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE + +ALTER TABLE CollectionPimItemRelation ADD FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE diff --git a/autotests/server/dbtest_data/dbinit_psql b/autotests/server/dbtest_data/dbinit_psql new file mode 100644 index 0000000..96fc714 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_psql @@ -0,0 +1,232 @@ +CREATE TABLE SchemaVersionTable (version INTEGER NOT NULL DEFAULT 0, + generation INTEGER NOT NULL DEFAULT 0) + +INSERT INTO SchemaVersionTable (version) VALUES (36) + +CREATE TABLE ResourceTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + isVirtual BOOL DEFAULT false) + +INSERT INTO ResourceTable (isVirtual,name) VALUES (true,'akonadi_search_resource') + +CREATE TABLE CollectionTable (id SERIAL PRIMARY KEY, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId int8, + resourceId int8 NOT NULL, + enabled BOOL NOT NULL DEFAULT true, + syncPref SMALLINT DEFAULT 2, + displayPref SMALLINT DEFAULT 2, + indexPref SMALLINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT false, + cachePolicyInherit BOOL NOT NULL DEFAULT true, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryAttributes TEXT, + queryCollections TEXT, + isVirtual BOOL DEFAULT false) + +INSERT INTO CollectionTable (isVirtual,name,parentId,resourceId) VALUES (true,'Search',NULL,1) + +CREATE TABLE MimeTypeTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL) + +CREATE TABLE PimItemTable (id SERIAL PRIMARY KEY, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId int8, + mimeTypeId int8, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size int8 NOT NULL DEFAULT 0) + +CREATE TABLE FlagTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL) + +CREATE TABLE PartTypeTable (id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + ns TEXT NOT NULL) + +CREATE TABLE PartTable (id SERIAL PRIMARY KEY, + pimItemId int8 NOT NULL, + partTypeId int8 NOT NULL, + data BYTEA, + datasize int8 NOT NULL, + version INTEGER DEFAULT 0, + storage SMALLINT DEFAULT 0) + +CREATE TABLE CollectionAttributeTable (id SERIAL PRIMARY KEY, + collectionId int8 NOT NULL, + type BYTEA NOT NULL, + value BYTEA) + +CREATE TABLE TagTypeTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL) + +INSERT INTO TagTypeTable (name) VALUES ('PLAIN') + +CREATE TABLE TagTable (id SERIAL PRIMARY KEY, + gid TEXT NOT NULL, + parentId int8, + typeId int8 DEFAULT 1) + +CREATE TABLE TagAttributeTable (id SERIAL PRIMARY KEY, + tagId int8 NOT NULL, + type BYTEA NOT NULL, + value BYTEA) + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId int8 NOT NULL, + resourceId int8 NOT NULL, + remoteId TEXT NOT NULL) + +CREATE TABLE RelationTypeTable (id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL) + +INSERT INTO RelationTypeTable (name) VALUES ('GENERIC') + +CREATE TABLE RelationTable (leftId int8 NOT NULL, + rightId int8 NOT NULL, + typeId int8 DEFAULT 1, + remoteId TEXT) + +CREATE TABLE PimItemFlagRelation (PimItem_id int8 NOT NULL, + Flag_id int8 NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id)) + +CREATE TABLE PimItemTagRelation (PimItem_id int8 NOT NULL, + Tag_id int8 NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id)) + +CREATE TABLE CollectionMimeTypeRelation (Collection_id int8 NOT NULL, + MimeType_id int8 NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id)) + +CREATE TABLE CollectionPimItemRelation (Collection_id int8 NOT NULL, + PimItem_id int8 NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id)) + +CREATE UNIQUE INDEX CollectionTable_parentAndNameIndex ON CollectionTable (parentId,name) + +CREATE INDEX CollectionTable_parentIndex ON CollectionTable (parentId) + +CREATE INDEX CollectionTable_resourceIndex ON CollectionTable (resourceId) + +CREATE INDEX CollectionTable_enabledIndex ON CollectionTable (enabled) + +CREATE INDEX CollectionTable_syncPrefIndex ON CollectionTable (syncPref) + +CREATE INDEX CollectionTable_displayPrefIndex ON CollectionTable (displayPref) + +CREATE INDEX CollectionTable_indexPrefIndex ON CollectionTable (indexPref) + +CREATE INDEX PimItemTable_collectionIndex ON PimItemTable (collectionId) + +CREATE INDEX PimItemTable_mimeTypeIndex ON PimItemTable (mimeTypeId) + +CREATE INDEX PimItemTable_gidIndex ON PimItemTable (gid) + +CREATE INDEX PimItemTable_ridIndex ON PimItemTable (remoteId) + +CREATE INDEX PimItemTable_idSortIndex ON PimItemTable (id DESC) + +CREATE UNIQUE INDEX PartTypeTable_partTypeNameIndex ON PartTypeTable (ns,name) + +CREATE UNIQUE INDEX PartTable_pimItemIdTypeIndex ON PartTable (pimItemId,partTypeId) + +CREATE INDEX PartTable_pimItemIdSortIndex ON PartTable (pimItemId DESC) + +CREATE INDEX PartTable_partTypeIndex ON PartTable (partTypeId) + +CREATE INDEX CollectionAttributeTable_collectionIndex ON CollectionAttributeTable (collectionId) + +CREATE INDEX TagTable_parentIndex ON TagTable (parentId) + +CREATE INDEX TagTable_typeIndex ON TagTable (typeId) + +CREATE INDEX TagAttributeTable_tagIndex ON TagAttributeTable (tagId) + +CREATE UNIQUE INDEX TagRemoteIdResourceRelationTable_TagAndResourceIndex ON TagRemoteIdResourceRelationTable (tagId,resourceId) + +CREATE INDEX TagRemoteIdResourceRelationTable_tagIndex ON TagRemoteIdResourceRelationTable (tagId) + +CREATE INDEX TagRemoteIdResourceRelationTable_resourceIndex ON TagRemoteIdResourceRelationTable (resourceId) + +CREATE UNIQUE INDEX RelationTable_RelationIndex ON RelationTable (leftId,rightId,typeId) + +CREATE INDEX RelationTable_leftIndex ON RelationTable (leftId) + +CREATE INDEX RelationTable_rightIndex ON RelationTable (rightId) + +CREATE INDEX RelationTable_typeIndex ON RelationTable (typeId) + +CREATE INDEX PimItemFlagRelation_PimItem_idIndex ON PimItemFlagRelation (PimItem_id) + +CREATE INDEX PimItemFlagRelation_Flag_idIndex ON PimItemFlagRelation (Flag_id) + +CREATE INDEX PimItemFlagRelation_pimItemIdSortIndex ON PimItemFlagRelation (pimitem_id DESC) + +CREATE INDEX PimItemTagRelation_PimItem_idIndex ON PimItemTagRelation (PimItem_id) + +CREATE INDEX PimItemTagRelation_Tag_idIndex ON PimItemTagRelation (Tag_id) + +CREATE INDEX CollectionMimeTypeRelation_Collection_idIndex ON CollectionMimeTypeRelation (Collection_id) + +CREATE INDEX CollectionMimeTypeRelation_MimeType_idIndex ON CollectionMimeTypeRelation (MimeType_id) + +CREATE INDEX CollectionPimItemRelation_Collection_idIndex ON CollectionPimItemRelation (Collection_id) + +CREATE INDEX CollectionPimItemRelation_PimItem_idIndex ON CollectionPimItemRelation (PimItem_id) + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PartTable ADD CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PartTable ADD CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionAttributeTable ADD CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagTable ADD CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagTable ADD CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagAttributeTable ADD CONSTRAINT TagAttributeTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagRemoteIdResourceRelationTable ADD CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagRemoteIdResourceRelationTable ADD CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE RelationTable ADD CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE RelationTable ADD CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE RelationTable ADD CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTagRelation ADD CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTagRelation ADD CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + diff --git a/autotests/server/dbtest_data/dbinit_psql_incremental b/autotests/server/dbtest_data/dbinit_psql_incremental new file mode 100644 index 0000000..6f7156b --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_psql_incremental @@ -0,0 +1,210 @@ +ALTER TABLE SchemaVersionTable ADD COLUMN version INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE SchemaVersionTable ADD COLUMN generation INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE ResourceTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE ResourceTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE ResourceTable ADD COLUMN isVirtual BOOL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE CollectionTable ADD COLUMN remoteId TEXT + +ALTER TABLE CollectionTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE CollectionTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN parentId int8 + +ALTER TABLE CollectionTable ADD COLUMN resourceId int8 NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN enabled BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN syncPref SMALLINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN displayPref SMALLINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN indexPref SMALLINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN referenced BOOL NOT NULL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyInherit BOOL NOT NULL DEFAULT true + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicySyncOnDemand BOOL NOT NULL DEFAULT false + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyLocalParts TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryString TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryAttributes TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryCollections TEXT + +ALTER TABLE CollectionTable ADD COLUMN isVirtual BOOL DEFAULT false + +ALTER TABLE MimeTypeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE MimeTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PimItemTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE PimItemTable ADD COLUMN rev INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE PimItemTable ADD COLUMN remoteId TEXT + +ALTER TABLE PimItemTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE PimItemTable ADD COLUMN gid TEXT + +ALTER TABLE PimItemTable ADD COLUMN collectionId int8 + +ALTER TABLE PimItemTable ADD COLUMN mimeTypeId int8 + +ALTER TABLE PimItemTable ADD COLUMN datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN dirty BOOL + +ALTER TABLE PimItemTable ADD COLUMN size int8 NOT NULL DEFAULT 0 + +ALTER TABLE FlagTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE FlagTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PartTypeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE PartTypeTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE PartTypeTable ADD COLUMN ns TEXT NOT NULL + +ALTER TABLE PartTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE PartTable ADD COLUMN pimItemId int8 NOT NULL + +ALTER TABLE PartTable ADD COLUMN data BYTEA + +ALTER TABLE PartTable ADD COLUMN datasize int8 NOT NULL + +ALTER TABLE PartTable ADD COLUMN version INTEGER DEFAULT 0 + +ALTER TABLE PartTable ADD COLUMN storage SMALLINT DEFAULT 0 + +ALTER TABLE CollectionAttributeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE CollectionAttributeTable ADD COLUMN collectionId int8 NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN type BYTEA NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN value BYTEA + +ALTER TABLE TagTypeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE TagTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE TagTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE TagTable ADD COLUMN gid TEXT NOT NULL + +ALTER TABLE TagTable ADD COLUMN parentId int8 + +ALTER TABLE TagTable ADD COLUMN typeId int8 DEFAULT 1 + +ALTER TABLE TagAttributeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE TagAttributeTable ADD COLUMN tagId int8 NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN type BYTEA NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN value BYTEA + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN tagId int8 NOT NULL + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN resourceId int8 NOT NULL + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN remoteId TEXT NOT NULL + +ALTER TABLE RelationTypeTable ADD COLUMN id SERIAL PRIMARY KEY + +ALTER TABLE RelationTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE RelationTable ADD COLUMN leftId int8 NOT NULL + +ALTER TABLE RelationTable ADD COLUMN rightId int8 NOT NULL + +ALTER TABLE RelationTable ADD COLUMN typeId int8 DEFAULT 1 + +ALTER TABLE RelationTable ADD COLUMN remoteId TEXT + +ALTER TABLE PimItemFlagRelation ADD COLUMN PimItem_id int8 NOT NULL + +ALTER TABLE PimItemFlagRelation ADD COLUMN Flag_id int8 NOT NULL + +ALTER TABLE PimItemTagRelation ADD COLUMN PimItem_id int8 NOT NULL + +ALTER TABLE PimItemTagRelation ADD COLUMN Tag_id int8 NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN Collection_id int8 NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN MimeType_id int8 NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN Collection_id int8 NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN PimItem_id int8 NOT NULL + +ALTER TABLE PimItemTable DROP CONSTRAINT myForeignKeyIdentifier + +ALTER TABLE CollectionAttributeTable DROP CONSTRAINT myForeignKeyIdentifier + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionTable ADD CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTable ADD CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PartTable ADD CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PartTable ADD CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionAttributeTable ADD CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagTable ADD CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagTable ADD CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagAttributeTable ADD CONSTRAINT TagAttributeTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagRemoteIdResourceRelationTable ADD CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE TagRemoteIdResourceRelationTable ADD CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE RelationTable ADD CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE RelationTable ADD CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE RelationTable ADD CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemFlagRelation ADD CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTagRelation ADD CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE PimItemTagRelation ADD CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionMimeTypeRelation ADD CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + +ALTER TABLE CollectionPimItemRelation ADD CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED + diff --git a/autotests/server/dbtest_data/dbinit_sqlite b/autotests/server/dbtest_data/dbinit_sqlite new file mode 100644 index 0000000..95ba537 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_sqlite @@ -0,0 +1,748 @@ +CREATE TABLE SchemaVersionTable (version INTEGER NOT NULL DEFAULT 0, + generation INTEGER NOT NULL DEFAULT 0) + +INSERT INTO SchemaVersionTable (version) VALUES (36) + +CREATE TABLE ResourceTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL, + isVirtual BOOL DEFAULT 0) + +INSERT INTO ResourceTable (isVirtual,name) VALUES (1,'akonadi_search_resource') + +CREATE TABLE CollectionTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + enabled BOOL NOT NULL DEFAULT 1, + syncPref TINYINT DEFAULT 2, + displayPref TINYINT DEFAULT 2, + indexPref TINYINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT 0, + cachePolicyInherit BOOL NOT NULL DEFAULT 1, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryAttributes TEXT, + queryCollections TEXT, + isVirtual BOOL DEFAULT 0, + CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionTable (isVirtual,name,parentId,resourceId) VALUES (1,'Search',NULL,1) + +CREATE TABLE MimeTypeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL) + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE FlagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL) + +CREATE TABLE PartTypeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL, + ns TEXT NOT NULL) + +CREATE TABLE PartTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pimItemId BIGINT NOT NULL, + partTypeId BIGINT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + storage TINYINT DEFAULT 0, + CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +CREATE TABLE CollectionAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE TagTypeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL) + +INSERT INTO TagTypeTable (name) VALUES ('PLAIN') + +CREATE TABLE TagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + gid TEXT NOT NULL, + parentId BIGINT, + typeId BIGINT DEFAULT 1, + CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE TagAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + tagId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT TagAttributeTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId BIGINT NOT NULL, + resourceId BIGINT NOT NULL, + remoteId TEXT NOT NULL, + CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE RelationTypeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT UNIQUE NOT NULL) + +INSERT INTO RelationTypeTable (name) VALUES ('GENERIC') + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE PimItemTagRelation (PimItem_id BIGINT NOT NULL, + Tag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id), + CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +CREATE UNIQUE INDEX CollectionTable_parentAndNameIndex ON CollectionTable (parentId,name) + +CREATE INDEX CollectionTable_parentIndex ON CollectionTable (parentId) + +CREATE INDEX CollectionTable_resourceIndex ON CollectionTable (resourceId) + +CREATE INDEX CollectionTable_enabledIndex ON CollectionTable (enabled) + +CREATE INDEX CollectionTable_syncPrefIndex ON CollectionTable (syncPref) + +CREATE INDEX CollectionTable_displayPrefIndex ON CollectionTable (displayPref) + +CREATE INDEX CollectionTable_indexPrefIndex ON CollectionTable (indexPref) + +CREATE INDEX PimItemTable_collectionIndex ON PimItemTable (collectionId) + +CREATE INDEX PimItemTable_mimeTypeIndex ON PimItemTable (mimeTypeId) + +CREATE INDEX PimItemTable_gidIndex ON PimItemTable (gid) + +CREATE INDEX PimItemTable_ridIndex ON PimItemTable (remoteId) + +CREATE INDEX PimItemTable_idSortIndex ON PimItemTable (id DESC) + +CREATE UNIQUE INDEX PartTypeTable_partTypeNameIndex ON PartTypeTable (ns,name) + +CREATE UNIQUE INDEX PartTable_pimItemIdTypeIndex ON PartTable (pimItemId,partTypeId) + +CREATE INDEX PartTable_pimItemIdSortIndex ON PartTable (pimItemId DESC) + +CREATE INDEX PartTable_partTypeIndex ON PartTable (partTypeId) + +CREATE INDEX CollectionAttributeTable_collectionIndex ON CollectionAttributeTable (collectionId) + +CREATE INDEX TagTable_parentIndex ON TagTable (parentId) + +CREATE INDEX TagTable_typeIndex ON TagTable (typeId) + +CREATE INDEX TagAttributeTable_tagIndex ON TagAttributeTable (tagId) + +CREATE UNIQUE INDEX TagRemoteIdResourceRelationTable_TagAndResourceIndex ON TagRemoteIdResourceRelationTable (tagId,resourceId) + +CREATE INDEX TagRemoteIdResourceRelationTable_tagIndex ON TagRemoteIdResourceRelationTable (tagId) + +CREATE INDEX TagRemoteIdResourceRelationTable_resourceIndex ON TagRemoteIdResourceRelationTable (resourceId) + +CREATE UNIQUE INDEX RelationTable_RelationIndex ON RelationTable (leftId,rightId,typeId) + +CREATE INDEX RelationTable_leftIndex ON RelationTable (leftId) + +CREATE INDEX RelationTable_rightIndex ON RelationTable (rightId) + +CREATE INDEX RelationTable_typeIndex ON RelationTable (typeId) + +CREATE INDEX PimItemFlagRelation_PimItem_idIndex ON PimItemFlagRelation (PimItem_id) + +CREATE INDEX PimItemFlagRelation_Flag_idIndex ON PimItemFlagRelation (Flag_id) + +CREATE INDEX PimItemFlagRelation_pimItemIdSortIndex ON PimItemFlagRelation (pimitem_id DESC) + +CREATE INDEX PimItemTagRelation_PimItem_idIndex ON PimItemTagRelation (PimItem_id) + +CREATE INDEX PimItemTagRelation_Tag_idIndex ON PimItemTagRelation (Tag_id) + +CREATE INDEX CollectionMimeTypeRelation_Collection_idIndex ON CollectionMimeTypeRelation (Collection_id) + +CREATE INDEX CollectionMimeTypeRelation_MimeType_idIndex ON CollectionMimeTypeRelation (MimeType_id) + +CREATE INDEX CollectionPimItemRelation_Collection_idIndex ON CollectionPimItemRelation (Collection_id) + +CREATE INDEX CollectionPimItemRelation_PimItem_idIndex ON CollectionPimItemRelation (PimItem_id) + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionTable RENAME TO CollectionTable_old + +CREATE TABLE CollectionTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + enabled BOOL NOT NULL DEFAULT 1, + syncPref TINYINT DEFAULT 2, + displayPref TINYINT DEFAULT 2, + indexPref TINYINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT 0, + cachePolicyInherit BOOL NOT NULL DEFAULT 1, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryAttributes TEXT, + queryCollections TEXT, + isVirtual BOOL DEFAULT 0, + CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionTable SELECT * FROM CollectionTable_old + +DROP TABLE CollectionTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionTable RENAME TO CollectionTable_old + +CREATE TABLE CollectionTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + enabled BOOL NOT NULL DEFAULT 1, + syncPref TINYINT DEFAULT 2, + displayPref TINYINT DEFAULT 2, + indexPref TINYINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT 0, + cachePolicyInherit BOOL NOT NULL DEFAULT 1, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryAttributes TEXT, + queryCollections TEXT, + isVirtual BOOL DEFAULT 0, + CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionTable SELECT * FROM CollectionTable_old + +DROP TABLE CollectionTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTable RENAME TO PimItemTable_old + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTable SELECT * FROM PimItemTable_old + +DROP TABLE PimItemTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTable RENAME TO PimItemTable_old + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTable SELECT * FROM PimItemTable_old + +DROP TABLE PimItemTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PartTable RENAME TO PartTable_old + +CREATE TABLE PartTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pimItemId BIGINT NOT NULL, + partTypeId BIGINT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + storage TINYINT DEFAULT 0, + CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PartTable SELECT * FROM PartTable_old + +DROP TABLE PartTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PartTable RENAME TO PartTable_old + +CREATE TABLE PartTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pimItemId BIGINT NOT NULL, + partTypeId BIGINT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + storage TINYINT DEFAULT 0, + CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PartTable SELECT * FROM PartTable_old + +DROP TABLE PartTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionAttributeTable RENAME TO CollectionAttributeTable_old + +CREATE TABLE CollectionAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionAttributeTable SELECT * FROM CollectionAttributeTable_old + +DROP TABLE CollectionAttributeTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagTable RENAME TO TagTable_old + +CREATE TABLE TagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + gid TEXT NOT NULL, + parentId BIGINT, + typeId BIGINT DEFAULT 1, + CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagTable SELECT * FROM TagTable_old + +DROP TABLE TagTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagTable RENAME TO TagTable_old + +CREATE TABLE TagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + gid TEXT NOT NULL, + parentId BIGINT, + typeId BIGINT DEFAULT 1, + CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagTable SELECT * FROM TagTable_old + +DROP TABLE TagTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagAttributeTable RENAME TO TagAttributeTable_old + + +CREATE TABLE TagAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + tagId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT TagAttributeTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagAttributeTable SELECT * FROM TagAttributeTable_old + +DROP TABLE TagAttributeTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagRemoteIdResourceRelationTable RENAME TO TagRemoteIdResourceRelationTable_old + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId BIGINT NOT NULL, + resourceId BIGINT NOT NULL, + remoteId TEXT NOT NULL, + CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagRemoteIdResourceRelationTable SELECT * FROM TagRemoteIdResourceRelationTable_old + +DROP TABLE TagRemoteIdResourceRelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagRemoteIdResourceRelationTable RENAME TO TagRemoteIdResourceRelationTable_old + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId BIGINT NOT NULL, + resourceId BIGINT NOT NULL, + remoteId TEXT NOT NULL, + CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagRemoteIdResourceRelationTable SELECT * FROM TagRemoteIdResourceRelationTable_old + +DROP TABLE TagRemoteIdResourceRelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE RelationTable RENAME TO RelationTable_old + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO RelationTable SELECT * FROM RelationTable_old + +DROP TABLE RelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE RelationTable RENAME TO RelationTable_old + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO RelationTable SELECT * FROM RelationTable_old + +DROP TABLE RelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE RelationTable RENAME TO RelationTable_old + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO RelationTable SELECT * FROM RelationTable_old + +DROP TABLE RelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemFlagRelation RENAME TO PimItemFlagRelation_old + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PimItemFlagRelation SELECT * FROM PimItemFlagRelation_old + +DROP TABLE PimItemFlagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemFlagRelation RENAME TO PimItemFlagRelation_old + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PimItemFlagRelation SELECT * FROM PimItemFlagRelation_old + +DROP TABLE PimItemFlagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTagRelation RENAME TO PimItemTagRelation_old + +CREATE TABLE PimItemTagRelation (PimItem_id BIGINT NOT NULL, + Tag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id), + CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTagRelation SELECT * FROM PimItemTagRelation_old + +DROP TABLE PimItemTagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTagRelation RENAME TO PimItemTagRelation_old + +CREATE TABLE PimItemTagRelation (PimItem_id BIGINT NOT NULL, + Tag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id), + CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTagRelation SELECT * FROM PimItemTagRelation_old + +DROP TABLE PimItemTagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionMimeTypeRelation RENAME TO CollectionMimeTypeRelation_old + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionMimeTypeRelation SELECT * FROM CollectionMimeTypeRelation_old + +DROP TABLE CollectionMimeTypeRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionMimeTypeRelation RENAME TO CollectionMimeTypeRelation_old + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionMimeTypeRelation SELECT * FROM CollectionMimeTypeRelation_old + +DROP TABLE CollectionMimeTypeRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionPimItemRelation RENAME TO CollectionPimItemRelation_old + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionPimItemRelation SELECT * FROM CollectionPimItemRelation_old + +DROP TABLE CollectionPimItemRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionPimItemRelation RENAME TO CollectionPimItemRelation_old + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionPimItemRelation SELECT * FROM CollectionPimItemRelation_old + +DROP TABLE CollectionPimItemRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF diff --git a/autotests/server/dbtest_data/dbinit_sqlite_incremental b/autotests/server/dbtest_data/dbinit_sqlite_incremental new file mode 100644 index 0000000..24ef4b4 --- /dev/null +++ b/autotests/server/dbtest_data/dbinit_sqlite_incremental @@ -0,0 +1,745 @@ +ALTER TABLE SchemaVersionTable ADD COLUMN version INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE SchemaVersionTable ADD COLUMN generation INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE ResourceTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE ResourceTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE ResourceTable ADD COLUMN isVirtual BOOL DEFAULT 0 + +ALTER TABLE CollectionTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN remoteId TEXT + +ALTER TABLE CollectionTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE CollectionTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN parentId BIGINT + +ALTER TABLE CollectionTable ADD COLUMN resourceId BIGINT NOT NULL + +ALTER TABLE CollectionTable ADD COLUMN enabled BOOL NOT NULL DEFAULT 1 + +ALTER TABLE CollectionTable ADD COLUMN syncPref TINYINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN displayPref TINYINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN indexPref TINYINT DEFAULT 2 + +ALTER TABLE CollectionTable ADD COLUMN referenced BOOL NOT NULL DEFAULT 0 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyInherit BOOL NOT NULL DEFAULT 1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0 + +ALTER TABLE CollectionTable ADD COLUMN cachePolicyLocalParts TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryString TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryAttributes TEXT + +ALTER TABLE CollectionTable ADD COLUMN queryCollections TEXT + +ALTER TABLE CollectionTable ADD COLUMN isVirtual BOOL DEFAULT 0 + +ALTER TABLE MimeTypeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE MimeTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PimItemTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE PimItemTable ADD COLUMN rev INTEGER NOT NULL DEFAULT 0 + +ALTER TABLE PimItemTable ADD COLUMN remoteId TEXT + +ALTER TABLE PimItemTable ADD COLUMN remoteRevision TEXT + +ALTER TABLE PimItemTable ADD COLUMN gid TEXT + +ALTER TABLE PimItemTable ADD COLUMN collectionId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN mimeTypeId BIGINT + +ALTER TABLE PimItemTable ADD COLUMN datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP + +ALTER TABLE PimItemTable ADD COLUMN dirty BOOL + +ALTER TABLE PimItemTable ADD COLUMN size BIGINT NOT NULL DEFAULT 0 + +ALTER TABLE FlagTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE FlagTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE PartTypeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE PartTypeTable ADD COLUMN name TEXT NOT NULL + +ALTER TABLE PartTypeTable ADD COLUMN ns TEXT NOT NULL + +ALTER TABLE PartTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE PartTable ADD COLUMN pimItemId BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN data LONGBLOB + +ALTER TABLE PartTable ADD COLUMN datasize BIGINT NOT NULL + +ALTER TABLE PartTable ADD COLUMN version INTEGER DEFAULT 0 + +ALTER TABLE PartTable ADD COLUMN storage TINYINT DEFAULT 0 + +ALTER TABLE CollectionAttributeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN collectionId BIGINT NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN type LONGBLOB NOT NULL + +ALTER TABLE CollectionAttributeTable ADD COLUMN value LONGBLOB + +ALTER TABLE TagTypeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE TagTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE TagTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE TagTable ADD COLUMN gid TEXT NOT NULL + +ALTER TABLE TagTable ADD COLUMN parentId BIGINT + +ALTER TABLE TagTable ADD COLUMN typeId BIGINT DEFAULT 1 + +ALTER TABLE TagAttributeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN tagId BIGINT NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN type LONGBLOB NOT NULL + +ALTER TABLE TagAttributeTable ADD COLUMN value LONGBLOB + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN tagId BIGINT NOT NULL + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN resourceId BIGINT NOT NULL + +ALTER TABLE TagRemoteIdResourceRelationTable ADD COLUMN remoteId TEXT NOT NULL + +ALTER TABLE RelationTypeTable ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + +ALTER TABLE RelationTypeTable ADD COLUMN name TEXT UNIQUE NOT NULL + +ALTER TABLE RelationTable ADD COLUMN leftId BIGINT NOT NULL + +ALTER TABLE RelationTable ADD COLUMN rightId BIGINT NOT NULL + +ALTER TABLE RelationTable ADD COLUMN typeId BIGINT DEFAULT 1 + +ALTER TABLE RelationTable ADD COLUMN remoteId TEXT + +ALTER TABLE PimItemFlagRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemFlagRelation ADD COLUMN Flag_id BIGINT NOT NULL + +ALTER TABLE PimItemTagRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +ALTER TABLE PimItemTagRelation ADD COLUMN Tag_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionMimeTypeRelation ADD COLUMN MimeType_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN Collection_id BIGINT NOT NULL + +ALTER TABLE CollectionPimItemRelation ADD COLUMN PimItem_id BIGINT NOT NULL + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTable RENAME TO PimItemTable_old + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTable SELECT * FROM PimItemTable_old + +DROP TABLE PimItemTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionAttributeTable RENAME TO CollectionAttributeTable_old + +CREATE TABLE CollectionAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionAttributeTable SELECT * FROM CollectionAttributeTable_old + +DROP TABLE CollectionAttributeTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionTable RENAME TO CollectionTable_old + +CREATE TABLE CollectionTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + enabled BOOL NOT NULL DEFAULT 1, + syncPref TINYINT DEFAULT 2, + displayPref TINYINT DEFAULT 2, + indexPref TINYINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT 0, + cachePolicyInherit BOOL NOT NULL DEFAULT 1, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryAttributes TEXT, + queryCollections TEXT, + isVirtual BOOL DEFAULT 0, + CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionTable SELECT * FROM CollectionTable_old + +DROP TABLE CollectionTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionTable RENAME TO CollectionTable_old + +CREATE TABLE CollectionTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + remoteId TEXT, + remoteRevision TEXT, + name TEXT NOT NULL, + parentId BIGINT, + resourceId BIGINT NOT NULL, + enabled BOOL NOT NULL DEFAULT 1, + syncPref TINYINT DEFAULT 2, + displayPref TINYINT DEFAULT 2, + indexPref TINYINT DEFAULT 2, + referenced BOOL NOT NULL DEFAULT 0, + cachePolicyInherit BOOL NOT NULL DEFAULT 1, + cachePolicyCheckInterval INTEGER NOT NULL DEFAULT -1, + cachePolicyCacheTimeout INTEGER NOT NULL DEFAULT -1, + cachePolicySyncOnDemand BOOL NOT NULL DEFAULT 0, + cachePolicyLocalParts TEXT, + queryString TEXT, + queryAttributes TEXT, + queryCollections TEXT, + isVirtual BOOL DEFAULT 0, + CONSTRAINT CollectionTableparentId_Collectionid_fk FOREIGN KEY (parentId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionTable SELECT * FROM CollectionTable_old + +DROP TABLE CollectionTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTable RENAME TO PimItemTable_old + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTable SELECT * FROM PimItemTable_old + +DROP TABLE PimItemTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTable RENAME TO PimItemTable_old + +CREATE TABLE PimItemTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + rev INTEGER NOT NULL DEFAULT 0, + remoteId TEXT, + remoteRevision TEXT, + gid TEXT, + collectionId BIGINT, + mimeTypeId BIGINT, + datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + atime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + dirty BOOL, + size BIGINT NOT NULL DEFAULT 0, + CONSTRAINT PimItemTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTablemimeTypeId_MimeTypeid_fk FOREIGN KEY (mimeTypeId) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTable SELECT * FROM PimItemTable_old + +DROP TABLE PimItemTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PartTable RENAME TO PartTable_old + +CREATE TABLE PartTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pimItemId BIGINT NOT NULL, + partTypeId BIGINT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + storage TINYINT DEFAULT 0, + CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PartTable SELECT * FROM PartTable_old + +DROP TABLE PartTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PartTable RENAME TO PartTable_old + +CREATE TABLE PartTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + pimItemId BIGINT NOT NULL, + partTypeId BIGINT NOT NULL, + data LONGBLOB, + datasize BIGINT NOT NULL, + version INTEGER DEFAULT 0, + storage TINYINT DEFAULT 0, + CONSTRAINT PartTablepimItemId_PimItemid_fk FOREIGN KEY (pimItemId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PartTablepartTypeId_PartTypeid_fk FOREIGN KEY (partTypeId) REFERENCES PartTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PartTable SELECT * FROM PartTable_old + +DROP TABLE PartTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionAttributeTable RENAME TO CollectionAttributeTable_old + +CREATE TABLE CollectionAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + collectionId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT CollectionAttributeTablecollectionId_Collectionid_fk FOREIGN KEY (collectionId) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionAttributeTable SELECT * FROM CollectionAttributeTable_old + +DROP TABLE CollectionAttributeTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagTable RENAME TO TagTable_old + +CREATE TABLE TagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + gid TEXT NOT NULL, + parentId BIGINT, + typeId BIGINT DEFAULT 1, + CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagTable SELECT * FROM TagTable_old + +DROP TABLE TagTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagTable RENAME TO TagTable_old + +CREATE TABLE TagTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + gid TEXT NOT NULL, + parentId BIGINT, + typeId BIGINT DEFAULT 1, + CONSTRAINT TagTableparentId_Tagid_fk FOREIGN KEY (parentId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagTabletypeId_TagTypeid_fk FOREIGN KEY (typeId) REFERENCES TagTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagTable SELECT * FROM TagTable_old + +DROP TABLE TagTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagAttributeTable RENAME TO TagAttributeTable_old + + +CREATE TABLE TagAttributeTable (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + tagId BIGINT NOT NULL, + type LONGBLOB NOT NULL, + value LONGBLOB, + CONSTRAINT TagAttributeTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagAttributeTable SELECT * FROM TagAttributeTable_old + +DROP TABLE TagAttributeTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagRemoteIdResourceRelationTable RENAME TO TagRemoteIdResourceRelationTable_old + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId BIGINT NOT NULL, + resourceId BIGINT NOT NULL, + remoteId TEXT NOT NULL, + CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagRemoteIdResourceRelationTable SELECT * FROM TagRemoteIdResourceRelationTable_old + +DROP TABLE TagRemoteIdResourceRelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE TagRemoteIdResourceRelationTable RENAME TO TagRemoteIdResourceRelationTable_old + +CREATE TABLE TagRemoteIdResourceRelationTable (tagId BIGINT NOT NULL, + resourceId BIGINT NOT NULL, + remoteId TEXT NOT NULL, + CONSTRAINT TagRemoteIdResourceRelationTabletagId_Tagid_fk FOREIGN KEY (tagId) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT TagRemoteIdResourceRelationTableresourceId_Resourceid_fk FOREIGN KEY (resourceId) REFERENCES ResourceTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO TagRemoteIdResourceRelationTable SELECT * FROM TagRemoteIdResourceRelationTable_old + +DROP TABLE TagRemoteIdResourceRelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE RelationTable RENAME TO RelationTable_old + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO RelationTable SELECT * FROM RelationTable_old + +DROP TABLE RelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE RelationTable RENAME TO RelationTable_old + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO RelationTable SELECT * FROM RelationTable_old + +DROP TABLE RelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE RelationTable RENAME TO RelationTable_old + +CREATE TABLE RelationTable (leftId BIGINT NOT NULL, + rightId BIGINT NOT NULL, + typeId BIGINT DEFAULT 1, + remoteId TEXT, + CONSTRAINT RelationTableleftId_PimItemid_fk FOREIGN KEY (leftId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTablerightId_PimItemid_fk FOREIGN KEY (rightId) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT RelationTabletypeId_RelationTypeid_fk FOREIGN KEY (typeId) REFERENCES RelationTypeTable(id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO RelationTable SELECT * FROM RelationTable_old + +DROP TABLE RelationTable_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemFlagRelation RENAME TO PimItemFlagRelation_old + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PimItemFlagRelation SELECT * FROM PimItemFlagRelation_old + +DROP TABLE PimItemFlagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemFlagRelation RENAME TO PimItemFlagRelation_old + +CREATE TABLE PimItemFlagRelation (PimItem_id BIGINT NOT NULL, + Flag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Flag_id), + CONSTRAINT PimItemFlagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemFlagRelationFlag_id_Flagid_fk FOREIGN KEY (Flag_id) REFERENCES FlagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + + +INSERT INTO PimItemFlagRelation SELECT * FROM PimItemFlagRelation_old + +DROP TABLE PimItemFlagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTagRelation RENAME TO PimItemTagRelation_old + +CREATE TABLE PimItemTagRelation (PimItem_id BIGINT NOT NULL, + Tag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id), + CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTagRelation SELECT * FROM PimItemTagRelation_old + +DROP TABLE PimItemTagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE PimItemTagRelation RENAME TO PimItemTagRelation_old + +CREATE TABLE PimItemTagRelation (PimItem_id BIGINT NOT NULL, + Tag_id BIGINT NOT NULL, + PRIMARY KEY (PimItem_id, Tag_id), + CONSTRAINT PimItemTagRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT PimItemTagRelationTag_id_Tagid_fk FOREIGN KEY (Tag_id) REFERENCES TagTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO PimItemTagRelation SELECT * FROM PimItemTagRelation_old + +DROP TABLE PimItemTagRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionMimeTypeRelation RENAME TO CollectionMimeTypeRelation_old + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionMimeTypeRelation SELECT * FROM CollectionMimeTypeRelation_old + +DROP TABLE CollectionMimeTypeRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionMimeTypeRelation RENAME TO CollectionMimeTypeRelation_old + +CREATE TABLE CollectionMimeTypeRelation (Collection_id BIGINT NOT NULL, + MimeType_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, MimeType_id), + CONSTRAINT CollectionMimeTypeRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionMimeTypeRelationMimeType_id_MimeTypeid_fk FOREIGN KEY (MimeType_id) REFERENCES MimeTypeTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionMimeTypeRelation SELECT * FROM CollectionMimeTypeRelation_old + +DROP TABLE CollectionMimeTypeRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionPimItemRelation RENAME TO CollectionPimItemRelation_old + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionPimItemRelation SELECT * FROM CollectionPimItemRelation_old + +DROP TABLE CollectionPimItemRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF + +PRAGMA defer_foreign_keys=ON + +BEGIN TRANSACTION + +ALTER TABLE CollectionPimItemRelation RENAME TO CollectionPimItemRelation_old + +CREATE TABLE CollectionPimItemRelation (Collection_id BIGINT NOT NULL, + PimItem_id BIGINT NOT NULL, + PRIMARY KEY (Collection_id, PimItem_id), + CONSTRAINT CollectionPimItemRelationCollection_id_Collectionid_fk FOREIGN KEY (Collection_id) REFERENCES CollectionTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT CollectionPimItemRelationPimItem_id_PimItemid_fk FOREIGN KEY (PimItem_id) REFERENCES PimItemTable(id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED) + +INSERT INTO CollectionPimItemRelation SELECT * FROM CollectionPimItemRelation_old + +DROP TABLE CollectionPimItemRelation_old + +COMMIT + +PRAGMA defer_foreign_keys=OFF diff --git a/autotests/server/dbtest_data/dbtest_data.qrc b/autotests/server/dbtest_data/dbtest_data.qrc new file mode 100644 index 0000000..9d4fa2f --- /dev/null +++ b/autotests/server/dbtest_data/dbtest_data.qrc @@ -0,0 +1,14 @@ + + + unittest_dbupdate.xml + unittest_schema.xml + + dbinit_mysql + dbinit_psql + dbinit_sqlite + + dbinit_mysql_incremental + dbinit_psql_incremental + dbinit_sqlite_incremental + + diff --git a/autotests/server/dbtest_data/unittest_dbupdate.xml b/autotests/server/dbtest_data/unittest_dbupdate.xml new file mode 100644 index 0000000..1bc3945 --- /dev/null +++ b/autotests/server/dbtest_data/unittest_dbupdate.xml @@ -0,0 +1,318 @@ + + + + + + + + + ALTER TABLE LocationTable DROP COLUMN existCount; + ALTER TABLE LocationTable DROP COLUMN recentCount; + ALTER TABLE LocationTable DROP COLUMN unseenCount; + ALTER TABLE LocationTable DROP COLUMN firstUnseen; + + + + UPDATE LocationTable SET subscribed = true; + + + + ALTER TABLE LocationTable DROP COLUMN cachePolicyId; + ALTER TABLE ResourceTable DROP COLUMN cachePolicyId; + DROP TABLE CachePolicyTable; + + + + UPDATE PartTable SET name = 'PLD:ENVELOPE' WHERE name = 'ENVELOPE'; + UPDATE PartTable SET name = 'PLD:RFC822' WHERE name = 'RFC822'; + UPDATE PartTable SET name = 'PLD:HEAD' WHERE name = 'HEAD'; + UPDATE PartTable SET name = concat( 'ATR:', name ) WHERE substr( name, 1, 4 ) != 'PLD:'; + + + + + DROP TABLE CollectionTable; + ALTER TABLE LocationTable RENAME TO CollectionTable; + ALTER TABLE PimItemTable DROP COLUMN collectionId; + ALTER TABLE PimItemTable CHANGE locationId collectionId BIGINT; + DROP TABLE CollectionAttributeTable; + ALTER TABLE LocationAttributeTable CHANGE locationId collectionId BIGINT; + ALTER TABLE LocationAttributeTable RENAME TO CollectionAttributeTable; + DROP TABLE CollectionMimeTypeRelation; + ALTER TABLE LocationMimeTypeRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationMimeTypeRelation RENAME TO CollectionMimeTypeRelation; + DROP TABLE CollectionPimItemRelation; + ALTER TABLE LocationPimItemRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationPimItemRelation RENAME TO CollectionPimItemRelation; + + + + ALTER TABLE PartTable CHANGE datasize datasize BIGINT; + + + + UPDATE CollectionTable SET parentId = NULL WHERE parentId = 0; + ALTER TABLE CollectionTable CHANGE parentId parentId BIGINT DEFAULT NULL; + + + + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_nepomuktag_resource'; + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_search_resource'; + + + + UPDATE CollectionTable SET queryString = remoteId WHERE resourceId = 1 AND parentId IS NOT NULL; + UPDATE CollectionTable SET queryLanguage = 'SPARQL' WHERE resourceId = 1 AND parentId IS NOT NULL; + + + + ALTER TABLE CollectionAttributeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionMimeTypeRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionPimItemRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE FlagTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE MimeTypeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PartTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimItemFlagRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimitemTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE ResourceTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE SchemaVersionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + + + + + ALTER TABLE ResourceTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE name name VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryString queryString VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARCHAR(255) BINARY; + ALTER TABLE MimeTypeTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE FlagTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PartTable CHANGE name name VARCHAR(255) BINARY; + + + + + ALTER TABLE ResourceTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE CollectionTable CHANGE name name VARBINARY(255); + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARBINARY(255); + ALTER TABLE MimeTypeTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE FlagTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PartTable CHANGE name name VARBINARY(255); + + + UPDATE PimItemFlagRelation SET Flag_id=(SELECT id FROM FlagTable WHERE name='\\SEEN') WHERE Flag_id=(SELECT id FROM FlagTable WHERE name='\\Seen'); + DELETE FROM FlagTable WHERE name='\\Seen'; + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(1024); + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(32768); + + + + + ALTER TABLE PimItemFlagRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + ALTER TABLE PimItemFlagRelation CHANGE Flag_id Flag_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE MimeType_id MimeType_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + + + + UPDATE CollectionTable SET isVirtual = true WHERE resourceId IN (SELECT id FROM ResourceTable WHERE isVirtual = true) + UPDATE CollectionTable SET isVirtual = 1 WHERE resourceId IN (SELECT id FROM ResourceTable WHERE isVirtual = 1) + + + + ALTER TABLE CollectionTable ALTER remoteId TYPE text USING convert_from(remoteId,'utf8'); + ALTER TABLE CollectionTable ALTER remoteRevision TYPE text USING convert_from(remoteRevision,'utf8'); + ALTER TABLE CollectionTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE CollectionTable ALTER cachePolicyLocalParts TYPE text USING convert_from(cachePolicyLocalParts,'utf8'); + ALTER TABLE CollectionTable ALTER queryString TYPE text USING convert_from(queryString,'utf8'); + ALTER TABLE CollectionTable ALTER queryLanguage TYPE text USING convert_from(queryLanguage,'utf8'); + ALTER TABLE FlagTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE MimeTypeTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE PartTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE PimItemTable ALTER remoteId TYPE text USING convert_from(remoteId,'utf8'); + ALTER TABLE PimItemTable ALTER remoteRevision TYPE text USING convert_from(remoteRevision,'utf8'); + ALTER TABLE ResourceTable ALTER name TYPE text USING convert_from(name,'utf8'); + + + + + + + + + UPDATE CollectionTable SET queryAttributes = 'QUERYLANGUAGE SPARQL' WHERE queryLanguage = 'SPARQL'; + ALTER TABLE CollectionTable DROP COLUMN queryLanguage; + + + + UPDATE CollectionTable SET enabled = subscribed; + ALTER TABLE CollectionTable DROP COLUMN subscribed; + + + + + + DELETE FROM PimItemFlagRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM PimItemFlagRelation + LEFT JOIN PimItemTable ON PimItemFlagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + DELETE FROM PimItemFlagRelation WHERE flag_id IN ( + SELECT flag_id FROM PimItemFlagRelation + LEFT JOIN FlagTable ON PimItemFlagRelation.flag_id = FlagTable.id + WHERE FlagTable.id IS NULL) + + + DELETE FROM PimItemTagRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM PimItemTagRelation + LEFT JOIN PimItemTable ON PimItemTagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + DELETE FROM PimItemTagRelation WHERE tag_id IN ( + SELECT tag_id FROM PimItemTagRelation + LEFT JOIN TagTable ON PimItemTagRelation.tag_id = TagTable.id + WHERE TagTable.id IS NULL) + + + DELETE FROM CollectionMimeTypeRelation WHERE collection_id IN ( + SELECT collection_id FROM CollectionMimeTypeRelation + LEFT JOIN CollectionTable ON CollectionMimeTypeRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) + + DELETE FROM CollectionMimeTypeRelation WHERE mimeType_id IN ( + SELECT mimeType_id FROM CollectionMimeTypeRelation + LEFT JOIN MimeTypeTable ON CollectionMimeTypeRelation.mimeType_id = MimeTypeTable.id + WHERE MimeTypeTable.id IS NULL) + + + DELETE FROM CollectionPimItemRelation WHERE collection_id IN ( + SELECT collection_id FROM CollectionPimItemRelation + LEFT JOIN CollectionTable ON CollectionPimItemRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) + + DELETE FROM CollectionPimItemRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM CollectionPimItemRelation + LEFT JOIN PimItemTable ON CollectionPimItemRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + + + + + DELETE FROM PimItemFlagRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM PimItemFlagRelation + LEFT JOIN PimItemTable ON PimItemFlagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + DELETE FROM PimItemFlagRelation WHERE flag_id IN ( + SELECT id FROM ( + SELECT flag_id AS id FROM PimItemFlagRelation + LEFT JOIN FlagTable ON PimItemFlagRelation.flag_id = FlagTable.id + WHERE FlagTable.id IS NULL) x) + + + DELETE FROM PimItemTagRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM PimItemTagRelation + LEFT JOIN PimItemTable ON PimItemTagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + DELETE FROM PimItemTagRelation WHERE tag_id IN ( + SELECT id FROM ( + SELECT tag_id AS id FROM PimItemTagRelation + LEFT JOIN TagTable ON PimItemTagRelation.tag_id = TagTable.id + WHERE TagTable.id IS NULL) x) + + + DELETE FROM CollectionMimeTypeRelation WHERE collection_id IN ( + SELECT id FROM ( + SELECT collection_id AS id FROM CollectionMimeTypeRelation + LEFT JOIN CollectionTable ON CollectionMimeTypeRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) x) + + DELETE FROM CollectionMimeTypeRelation WHERE mimeType_id IN ( + SELECT id FROM ( + SELECT mimeType_id AS id FROM CollectionMimeTypeRelation + LEFT JOIN MimeTypeTable ON CollectionMimeTypeRelation.mimeType_id = MimeTypeTable.id + WHERE MimeTypeTable.id IS NULL) x) + + + DELETE FROM CollectionPimItemRelation WHERE collection_id IN ( + SELECT id FROM ( + SELECT collection_id AS id FROM CollectionPimItemRelation + LEFT JOIN CollectionTable ON CollectionPimItemRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) x) + + DELETE FROM CollectionPimItemRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM CollectionPimItemRelation + LEFT JOIN PimItemTable ON CollectionPimItemRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + + + SELECT setval('tagtypetable_id_seq', (SELECT max(id) FROM TagTypeTable)) + SELECT setval('relationtypetable_id_seq', (SELECT max(id) FROM RelationTypeTable)) + + + + UPDATE PartTable SET storage = external; + ALTER TABLE PartTable DROP COLUMN external; + + UPDATE PartTable SET storage = cast(external as integer); + ALTER TABLE PartTable DROP COLUMN external; + + diff --git a/autotests/server/dbtest_data/unittest_schema.xml b/autotests/server/dbtest_data/unittest_schema.xml new file mode 100644 index 0000000..84e9904 --- /dev/null +++ b/autotests/server/dbtest_data/unittest_schema.xml @@ -0,0 +1,246 @@ + + + + + + + + + Contains the schema version of the database. + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + +
+ + + + + + + + + + + create/modified time + + + read access time + + + Indicates that this item has unsaved changes. + + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + +
+ + + Table containing item part types. + + + Part name, without namespace. + + + Part namespace. + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + + +
+ + + + + + + + + Specifies allowed MimeType for a Collection + + + + Used to associate items with search folders. + +
diff --git a/autotests/server/dbtypetest.cpp b/autotests/server/dbtypetest.cpp new file mode 100644 index 0000000..673b4ee --- /dev/null +++ b/autotests/server/dbtypetest.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include +#include + +#define QL1S(x) QLatin1String(x) + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(DbType::Type) + +class DbTypeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDriverName_data() + { + QTest::addColumn("driverName"); + QTest::addColumn("dbType"); + + QTest::newRow("mysql") << "QMYSQL" << DbType::MySQL; + QTest::newRow("sqlite") << "QSQLITE" << DbType::Sqlite; + QTest::newRow("sqlite3") << "QSQLITE3" << DbType::Sqlite; + QTest::newRow("psql") << "QPSQL" << DbType::PostgreSQL; + } + + void testDriverName() + { + QFETCH(QString, driverName); + QFETCH(DbType::Type, dbType); + + QCOMPARE(DbType::typeForDriverName(driverName), dbType); + + if (QSqlDatabase::drivers().contains(driverName)) { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName, driverName); + QCOMPARE(DbType::type(db), dbType); + } + } +}; + +AKTEST_MAIN(DbTypeTest) + +#include "dbtypetest.moc" diff --git a/autotests/server/dbupdatertest.cpp b/autotests/server/dbupdatertest.cpp new file mode 100644 index 0000000..f663126 --- /dev/null +++ b/autotests/server/dbupdatertest.cpp @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbupdatertest.h" + +#include "storage/dbupdater.h" +#include + +using namespace Akonadi::Server; + +void DbUpdaterTest::initTestCase() +{ + Q_INIT_RESOURCE(akonadidb); +} + +void DbUpdaterTest::testMysqlUpdateStatements() +{ + const QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL")); + DbUpdater updater(db, QStringLiteral(":unittest_dbupdate.xml")); + + { + UpdateSet::Map updateSets; + QVERIFY(updater.parseUpdateSets(1, updateSets)); + const auto expectedSets = {2, 3, 4, 8, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 28, 31, 35}; + for (const auto expected : expectedSets) { + QVERIFY(updateSets.contains(expected)); + } + QCOMPARE(std::size_t(updateSets.count()), expectedSets.size()); + } + + { + UpdateSet::Map updateSets; + QVERIFY(updater.parseUpdateSets(13, updateSets)); + const auto expectedSets = {14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 26, 28, 31, 35}; + for (int i = 0; i < 13; ++i) { + QVERIFY(!updateSets.contains(i)); + } + for (const auto expected : expectedSets) { + QVERIFY(updateSets.contains(expected)); + } + QCOMPARE(std::size_t(updateSets.count()), expectedSets.size()); + + QCOMPARE(updateSets.value(14).statements.count(), 2); + QCOMPARE(updateSets.value(16).statements.count(), 11); + QCOMPARE(updateSets.value(22).statements.count(), 6); + } +} + +void DbUpdaterTest::testPsqlUpdateStatements() +{ + const QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL")); + DbUpdater updater(db, QStringLiteral(":unittest_dbupdate.xml")); + + { + UpdateSet::Map updateSets; + QVERIFY(updater.parseUpdateSets(1, updateSets)); + const auto expectedSets = {2, 3, 4, 8, 10, 12, 13, 14, 15, 16, 19, 23, 24, 25, 26, 28, 30, 33, 35}; + for (const auto expected : expectedSets) { + QVERIFY(updateSets.contains(expected)); + } + QCOMPARE(std::size_t(updateSets.count()), expectedSets.size()); + } + + { + UpdateSet::Map updateSets; + QVERIFY(updater.parseUpdateSets(13, updateSets)); + const auto expectedSets = {14, 15, 16, 19, 23, 24, 25, 26, 28, 30, 33, 35}; + for (int i = 0; i < 13; ++i) { + QVERIFY(!updateSets.contains(i)); + } + for (const auto expected : expectedSets) { + QVERIFY(updateSets.contains(expected)); + } + QCOMPARE(std::size_t(updateSets.count()), expectedSets.size()); + + QCOMPARE(updateSets.value(14).statements.count(), 2); + QCOMPARE(updateSets.value(16).statements.count(), 11); + QCOMPARE(updateSets.value(17).statements.count(), 0); + QCOMPARE(updateSets.value(22).statements.count(), 0); + } +} + +void DbUpdaterTest::cleanupTestCase() +{ + Q_CLEANUP_RESOURCE(akonadidb); +} + +QTEST_MAIN(DbUpdaterTest) diff --git a/autotests/server/dbupdatertest.h b/autotests/server/dbupdatertest.h new file mode 100644 index 0000000..a2bfc07 --- /dev/null +++ b/autotests/server/dbupdatertest.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +class DbUpdaterTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testMysqlUpdateStatements(); + void testPsqlUpdateStatements(); + void cleanupTestCase(); +}; + diff --git a/autotests/server/fakeakonadiserver.cpp b/autotests/server/fakeakonadiserver.cpp new file mode 100644 index 0000000..4d0dc61 --- /dev/null +++ b/autotests/server/fakeakonadiserver.cpp @@ -0,0 +1,324 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "fakeakonadiserver.h" +#include "cachecleaner.h" +#include "debuginterface.h" +#include "fakeclient.h" +#include "fakeconnection.h" +#include "fakedatastore.h" +#include "fakeintervalcheck.h" +#include "fakeitemretrievalmanager.h" +#include "fakesearchmanager.h" +#include "inspectablenotificationcollector.h" +#include "resourcemanager.h" +#include "search/searchtaskmanager.h" +#include "storage/collectionstatistics.h" +#include "storagejanitor.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "aklocalserver.h" +#include "preprocessormanager.h" +#include "search/searchmanager.h" +#include "storage/datastore.h" +#include "storage/dbconfig.h" +#include "utils.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::InspectableNotificationCollector *) + +TestScenario TestScenario::create(qint64 tag, TestScenario::Action action, const Protocol::CommandPtr &response) +{ + TestScenario sc; + sc.action = action; + + QBuffer buffer(&sc.data); + buffer.open(QIODevice::ReadWrite); + { + Protocol::DataStream stream(&buffer); + stream << tag; + Protocol::serialize(stream, response); + stream.flush(); + } + + { + buffer.seek(0); + Protocol::DataStream os(&buffer); + qint64 cmpTag; + os >> cmpTag; + Q_ASSERT(cmpTag == tag); + Protocol::CommandPtr cmpResp = Protocol::deserialize(os.device()); + + bool ok = false; + [cmpTag, tag, cmpResp, response, &ok]() { + QCOMPARE(cmpTag, tag); + QCOMPARE(cmpResp->type(), response->type()); + QCOMPARE(cmpResp->isResponse(), response->isResponse()); + QCOMPARE(Protocol::debugString(cmpResp), Protocol::debugString(response)); + QCOMPARE(*cmpResp, *response); + ok = true; + }(); + if (!ok) { + sc.data.clear(); + return sc; + } + } + return sc; +} + +FakeAkonadiServer::FakeAkonadiServer() +{ + qputenv("AKONADI_INSTANCE", qPrintable(instanceName())); + qputenv("XDG_DATA_HOME", qPrintable(QString(basePath() + QLatin1String("/local")))); + qputenv("XDG_CONFIG_HOME", qPrintable(QString(basePath() + QLatin1String("/config")))); + qputenv("HOME", qPrintable(basePath())); + qputenv("KDEHOME", qPrintable(basePath() + QLatin1String("/kdehome"))); + + mClient = std::make_unique(); + + DataStore::setFactory(std::make_unique(*this)); +} + +FakeAkonadiServer::~FakeAkonadiServer() +{ + FakeAkonadiServer::quit(); +} + +QString FakeAkonadiServer::basePath() +{ + return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + + QStringLiteral("/akonadiserver-test-%1").arg(QCoreApplication::instance()->applicationPid()); +} + +QString FakeAkonadiServer::socketFile() +{ + return basePath() % QStringLiteral("/local/share/akonadi/akonadiserver.socket"); +} + +QString FakeAkonadiServer::instanceName() +{ + return QStringLiteral("akonadiserver-test-%1").arg(QCoreApplication::instance()->applicationPid()); +} + +TestScenario::List FakeAkonadiServer::loginScenario(const QByteArray &sessionId) +{ + SchemaVersion schema = SchemaVersion::retrieveAll().at(0); + + auto hello = Protocol::HelloResponsePtr::create(); + hello->setServerName(QStringLiteral("Akonadi")); + hello->setMessage(QStringLiteral("Not Really IMAP server")); + hello->setProtocolVersion(Protocol::version()); + hello->setGeneration(schema.generation()); + + return {TestScenario::create(0, TestScenario::ServerCmd, hello), + TestScenario::create(1, TestScenario::ClientCmd, Protocol::LoginCommandPtr::create(sessionId.isEmpty() ? instanceName().toLatin1() : sessionId)), + TestScenario::create(1, TestScenario::ServerCmd, Protocol::LoginResponsePtr::create())}; +} + +TestScenario::List FakeAkonadiServer::selectResourceScenario(const QString &name) +{ + const Resource resource = Resource::retrieveByName(name); + return {TestScenario::create(3, TestScenario::ClientCmd, Protocol::SelectResourceCommandPtr::create(resource.name())), + TestScenario::create(3, TestScenario::ServerCmd, Protocol::SelectResourceResponsePtr::create())}; +} + +void FakeAkonadiServer::disableItemRetrievalManager() +{ + mDisableItemRetrievalManager = true; +} + +bool FakeAkonadiServer::init() +{ + try { + initFake(); + } catch (const FakeAkonadiServerException &e) { + qWarning() << "Server exception: " << e.what(); + qFatal("Fake Akonadi Server failed to start up, aborting test"); + } + return true; +} + +void FakeAkonadiServer::initFake() +{ + qDebug() << "==== Fake Akonadi Server starting up ===="; + + qputenv("XDG_DATA_HOME", qPrintable(QString(basePath() + QLatin1String("/local")))); + qputenv("XDG_CONFIG_HOME", qPrintable(QString(basePath() + QLatin1String("/config")))); + qputenv("AKONADI_INSTANCE", qPrintable(instanceName())); + QSettings settings(StandardDirs::serverConfigFile(StandardDirs::WriteOnly), QSettings::IniFormat); + settings.beginGroup(QStringLiteral("General")); + settings.setValue(QStringLiteral("Driver"), QLatin1String("QSQLITE3")); + settings.endGroup(); + + settings.beginGroup(QStringLiteral("QSQLITE3")); + settings.setValue(QStringLiteral("Name"), QString(basePath() + QLatin1String("/local/share/akonadi/akonadi.db"))); + settings.endGroup(); + settings.sync(); + + DbConfig *dbConfig = DbConfig::configuredDatabase(); + if (dbConfig->driverName() != QLatin1String("QSQLITE3")) { + throw FakeAkonadiServerException(QLatin1String("Unexpected driver specified. Expected QSQLITE3, got ") + dbConfig->driverName()); + } + + const QLatin1String initCon("initConnection"); + { + QSqlDatabase db = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), initCon); + DbConfig::configuredDatabase()->apply(db); + db.setDatabaseName(DbConfig::configuredDatabase()->databaseName()); + if (!db.isDriverAvailable(DbConfig::configuredDatabase()->driverName())) { + throw FakeAkonadiServerException(QStringLiteral("SQL driver %s not available").arg(db.driverName())); + } + if (!db.isValid()) { + throw FakeAkonadiServerException("Got invalid database"); + } + if (db.open()) { + qWarning() << "Database" << dbConfig->configuredDatabase()->databaseName() << "already exists, the test is not running in a clean environment!"; + } + db.close(); + } + + QSqlDatabase::removeDatabase(initCon); + + dbConfig->setup(); + + mDataStore = static_cast(FakeDataStore::self()); + mDataStore->setPopulateDb(mPopulateDb); + if (!mDataStore->init()) { + throw FakeAkonadiServerException("Failed to initialize datastore"); + } + + mTracer = std::make_unique(); + mCollectionStats = std::make_unique(); + mCacheCleaner = std::make_unique(); + if (!mDisableItemRetrievalManager) { + mItemRetrieval = std::make_unique(); + } + mAgentSearchManager = std::make_unique(); + + mDebugInterface = std::make_unique(*mTracer); + mResourceManager = std::make_unique(*mTracer); + mPreprocessorManager = std::make_unique(*mTracer); + mPreprocessorManager->setEnabled(false); + mIntervalCheck = std::make_unique(*mItemRetrieval); + mSearchManager = std::make_unique(*mAgentSearchManager); + mStorageJanitor = std::make_unique(*this); + + qDebug() << "==== Fake Akonadi Server started ===="; +} + +bool FakeAkonadiServer::quit() +{ + qDebug() << "==== Fake Akonadi Server shutting down ===="; + + // Stop listening for connections + if (mCmdServer) { + mCmdServer->close(); + } + + if (!qEnvironmentVariableIsSet("AKONADI_TEST_NOCLEANUP")) { + bool ok = QDir(basePath()).removeRecursively(); + qDebug() << "Cleaned up" << basePath() << "success=" << ok; + } else { + qDebug() << "Skipping clean up of" << basePath(); + } + + mConnection.reset(); + mClient.reset(); + + mStorageJanitor.reset(); + mSearchManager.reset(); + mIntervalCheck.reset(); + mPreprocessorManager.reset(); + mResourceManager.reset(); + mDebugInterface.reset(); + + mAgentSearchManager.reset(); + mItemRetrieval.reset(); + mCacheCleaner.reset(); + mCollectionStats.reset(); + mTracer.reset(); + + if (mDataStore) { + mDataStore->close(); + } + + qDebug() << "==== Fake Akonadi Server shut down ===="; + return true; +} + +void FakeAkonadiServer::setScenarios(const TestScenario::List &scenarios) +{ + mClient->setScenarios(scenarios); +} + +void FakeAkonadiServer::newCmdConnection(quintptr socketDescriptor) +{ + mConnection = std::make_unique(socketDescriptor, *this); + + // Connection is its own thread, so we have to make sure we get collector + // from DataStore of the Connection's thread, not ours + NotificationCollector *collector = nullptr; + QMetaObject::invokeMethod(mConnection.get(), + "notificationCollector", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(Akonadi::Server::NotificationCollector *, collector)); + mNtfCollector = dynamic_cast(collector); + Q_ASSERT(mNtfCollector); + mNotificationSpy.reset(new QSignalSpy(mNtfCollector, &Server::InspectableNotificationCollector::notifySignal)); + Q_ASSERT(mNotificationSpy->isValid()); +} + +void FakeAkonadiServer::runTest() +{ + mCmdServer = std::make_unique(); + connect(mCmdServer.get(), static_cast(&AkLocalServer::newConnection), this, &FakeAkonadiServer::newCmdConnection); + QVERIFY(mCmdServer->listen(socketFile())); + + QEventLoop serverLoop; + connect(mClient.get(), &QThread::finished, this, [this, &serverLoop]() { // clazy:exclude=lambda-in-connect + disconnect(mClient.get(), &QThread::finished, this, nullptr); + // Flush any pending notifications and wait for them + // before shutting down the event loop + if (mNtfCollector->dispatchNotifications()) { + mNotificationSpy->wait(); + } + + serverLoop.quit(); + }); + + // Start the client: the client will connect to the server and will + // start playing the scenario + mClient->start(); + + // Wait until the client disconnects, i.e. until the scenario is completed. + serverLoop.exec(); + + mCmdServer->close(); +} + +QSharedPointer FakeAkonadiServer::notificationSpy() const +{ + return mNotificationSpy; +} + +void FakeAkonadiServer::setPopulateDb(bool populate) +{ + mPopulateDb = populate; +} diff --git a/autotests/server/fakeakonadiserver.h b/autotests/server/fakeakonadiserver.h new file mode 100644 index 0000000..8246683 --- /dev/null +++ b/autotests/server/fakeakonadiserver.h @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ +#pragma once + +#include "akonadi.h" +#include "exception.h" + +#include +#include + +#include +#include + +#include + +namespace Akonadi +{ +namespace Server +{ +class InspectableNotificationCollector; +class FakeDataStore; +class FakeConnection; +class FakeClient; + +class TestScenario +{ +public: + using List = QList; + + enum Action { + ServerCmd, + ClientCmd, + Wait, + Quit, + Ignore, + }; + + Action action; + QByteArray data; + + static TestScenario create(qint64 tag, Action action, const Protocol::CommandPtr &response); + + static TestScenario wait(int timeout) + { + return TestScenario{Wait, QByteArray::number(timeout)}; + } + + static TestScenario quit() + { + return TestScenario{Quit, QByteArray()}; + } + + static TestScenario ignore(int count) + { + return TestScenario{Ignore, QByteArray::number(count)}; + } +}; + +/** + * A fake server used for testing. Loosely based on KIMAP::FakeServer + */ +class FakeAkonadiServer : public AkonadiServer +{ + Q_OBJECT + +public: + explicit FakeAkonadiServer(); + ~FakeAkonadiServer() override; + + /* Reimpl */ + bool init() override; + + static QString basePath(); + static QString socketFile(); + static QString instanceName(); + + static TestScenario::List loginScenario(const QByteArray &sessionId = QByteArray()); + static TestScenario::List selectCollectionScenario(const QString &name); + static TestScenario::List selectResourceScenario(const QString &name); + + void setScenarios(const TestScenario::List &scenarios); + + void runTest(); + + QSharedPointer notificationSpy() const; + + void setPopulateDb(bool populate); + void disableItemRetrievalManager(); + +protected: + void newCmdConnection(quintptr socketDescriptor) override; + +private: + bool quit() override; + void initFake(); + + FakeDataStore *mDataStore = nullptr; + std::unique_ptr mConnection; + std::unique_ptr mClient; + + InspectableNotificationCollector *mNtfCollector = nullptr; + QSharedPointer mNotificationSpy; + + bool mPopulateDb = true; + bool mDisableItemRetrievalManager = false; +}; + +AKONADI_EXCEPTION_MAKE_INSTANCE(FakeAkonadiServerException); + +} +} + +Q_DECLARE_METATYPE(Akonadi::Server::TestScenario) +Q_DECLARE_METATYPE(Akonadi::Server::TestScenario::List) + diff --git a/autotests/server/fakeclient.cpp b/autotests/server/fakeclient.cpp new file mode 100644 index 0000000..f23199f --- /dev/null +++ b/autotests/server/fakeclient.cpp @@ -0,0 +1,219 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "fakeclient.h" + +#include +#include +#include + +#include +#include +#include +#include + +#define CLIENT_COMPARE(actual, expected, ...) \ + do { \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) { \ + mSocket->disconnectFromServer(); \ + return __VA_ARGS__; \ + } \ + } while (0) + +#define CLIENT_VERIFY(statement, ...) \ + do { \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) { \ + mSocket->disconnectFromServer(); \ + return __VA_ARGS__; \ + } \ + } while (0) + +using namespace Akonadi; +using namespace Akonadi::Server; + +FakeClient::FakeClient(QObject *parent) + : QThread(parent) + , mMutex() +{ + moveToThread(this); +} + +FakeClient::~FakeClient() +{ +} + +void FakeClient::setScenarios(const TestScenario::List &scenarios) +{ + mScenarios = scenarios; +} + +bool FakeClient::isScenarioDone() const +{ + QMutexLocker locker(&mMutex); + return mScenarios.isEmpty(); +} + +bool FakeClient::dataAvailable() +{ + QMutexLocker locker(&mMutex); + + CLIENT_VERIFY(!mScenarios.isEmpty(), false); + + readServerPart(); + writeClientPart(); + + return true; +} + +void FakeClient::readServerPart() +{ + while (!mScenarios.isEmpty() && (mScenarios.at(0).action == TestScenario::ServerCmd || mScenarios.at(0).action == TestScenario::Ignore)) { + TestScenario scenario = mScenarios.takeFirst(); + if (scenario.action == TestScenario::Ignore) { + const int count = scenario.data.toInt(); + + // Read and throw away all "count" responses. Useful for scenarios + // with thousands of responses + qint64 tag; + for (int i = 0; i < count; ++i) { + mStream >> tag; + Protocol::deserialize(mStream.device()); + } + } else { + QBuffer buffer(&scenario.data); + buffer.open(QIODevice::ReadOnly); + Protocol::DataStream expectedStream(&buffer); + qint64 expectedTag; + qint64 actualTag; + + expectedStream >> expectedTag; + const auto expectedCommand = Protocol::deserialize(expectedStream.device()); + try { + while (static_cast(mSocket->bytesAvailable()) < sizeof(qint64)) { + Protocol::DataStream::waitForData(mSocket, 5000); + } + } catch (const ProtocolException &e) { + qDebug() << "ProtocolException:" << e.what(); + qDebug() << "Expected response:" << Protocol::debugString(expectedCommand); + CLIENT_VERIFY(false); + } + + mStream >> actualTag; + CLIENT_COMPARE(actualTag, expectedTag); + + Protocol::CommandPtr actualCommand; + try { + actualCommand = Protocol::deserialize(mStream.device()); + } catch (const ProtocolException &e) { + qDebug() << "Protocol exception:" << e.what(); + qDebug() << "Expected response:" << Protocol::debugString(expectedCommand); + CLIENT_VERIFY(false); + } + + if (actualCommand->type() != expectedCommand->type()) { + qDebug() << "Actual command: " << Protocol::debugString(actualCommand); + qDebug() << "Expected Command:" << Protocol::debugString(expectedCommand); + } + CLIENT_COMPARE(actualCommand->type(), expectedCommand->type()); + CLIENT_COMPARE(actualCommand->isResponse(), expectedCommand->isResponse()); + if (*actualCommand != *expectedCommand) { + qDebug() << "Actual command: " << Protocol::debugString(actualCommand); + qDebug() << "Expected Command:" << Protocol::debugString(expectedCommand); + } + + CLIENT_COMPARE(*actualCommand, *expectedCommand); + } + } + + if (!mScenarios.isEmpty()) { + CLIENT_VERIFY(mScenarios.at(0).action == TestScenario::ClientCmd || mScenarios.at(0).action == TestScenario::Wait + || mScenarios.at(0).action == TestScenario::Quit); + } else { + // Server replied and there's nothing else to send, then quit + mSocket->disconnectFromServer(); + } +} + +void FakeClient::writeClientPart() +{ + while (!mScenarios.isEmpty() && (mScenarios.at(0).action == TestScenario::ClientCmd || mScenarios.at(0).action == TestScenario::Wait)) { + const TestScenario rule = mScenarios.takeFirst(); + + if (rule.action == TestScenario::ClientCmd) { + mSocket->write(rule.data); + CLIENT_VERIFY(mSocket->waitForBytesWritten()); + } else { + const int timeout = rule.data.toInt(); + QTest::qWait(timeout); + } + } + + if (!mScenarios.isEmpty() && mScenarios.at(0).action == TestScenario::Quit) { + mSocket->close(); + } + + if (!mScenarios.isEmpty()) { + CLIENT_VERIFY(mScenarios.at(0).action == TestScenario::ServerCmd || mScenarios.at(0).action == TestScenario::Ignore); + } +} + +void FakeClient::run() +{ + mSocket = new QLocalSocket(); + mSocket->connectToServer(FakeAkonadiServer::socketFile()); + connect(mSocket, &QLocalSocket::disconnected, this, &FakeClient::connectionLost); + connect(mSocket, + &QLocalSocket::errorOccurred, + this, + [this]() { + qWarning() << "Client socket error: " << mSocket->errorString(); + connectionLost(); + QVERIFY(false); + }); + if (!mSocket->waitForConnected()) { + qFatal("Failed to connect to FakeAkonadiServer"); + QVERIFY(false); + return; + } + mStream.setDevice(mSocket); + + Q_FOREVER { + if (mSocket->state() != QLocalSocket::ConnectedState) { + connectionLost(); + break; + } + + { + QEventLoop loop; + connect(mSocket, &QLocalSocket::readyRead, &loop, &QEventLoop::quit); + connect(mSocket, &QLocalSocket::disconnected, &loop, &QEventLoop::quit); + loop.exec(); + } + + while (mSocket->bytesAvailable() > 0) { + if (mSocket->state() != QLocalSocket::ConnectedState) { + connectionLost(); + break; + } + + if (!dataAvailable()) { + break; + } + } + } + + mStream.setDevice(nullptr); + mSocket->close(); + delete mSocket; + mSocket = nullptr; +} + +void FakeClient::connectionLost() +{ + // Otherwise this is an error on server-side, we expected more talking + CLIENT_VERIFY(isScenarioDone()); +} diff --git a/autotests/server/fakeclient.h b/autotests/server/fakeclient.h new file mode 100644 index 0000000..28cc90a --- /dev/null +++ b/autotests/server/fakeclient.h @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include "datastream_p_p.h" +#include +#include + +#include "fakeakonadiserver.h" + +class QLocalSocket; + +namespace Akonadi +{ +namespace Server +{ +class FakeClient : public QThread +{ + Q_OBJECT + +public: + explicit FakeClient(QObject *parent = nullptr); + ~FakeClient() override; + + void setScenarios(const TestScenario::List &scenarios); + + bool isScenarioDone() const; + +protected: + void run() override; + +private Q_SLOTS: + bool dataAvailable(); + void readServerPart(); + void writeClientPart(); + void connectionLost(); + +private: + mutable QRecursiveMutex mMutex; + + TestScenario::List mScenarios; + QLocalSocket *mSocket = nullptr; + Protocol::DataStream mStream; +}; +} +} + diff --git a/autotests/server/fakeconnection.cpp b/autotests/server/fakeconnection.cpp new file mode 100644 index 0000000..148438b --- /dev/null +++ b/autotests/server/fakeconnection.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakeconnection.h" + +#include "fakeakonadiserver.h" +#include "fakedatastore.h" + +using namespace Akonadi::Server; + +FakeConnection::FakeConnection(quintptr socketDescriptor, FakeAkonadiServer &akonadi) + : Connection(socketDescriptor, akonadi) +{ +} + +FakeConnection::FakeConnection(AkonadiServer &akonadi) + : Connection(akonadi) +{ +} + +FakeConnection::~FakeConnection() = default; + +NotificationCollector *FakeConnection::notificationCollector() +{ + return storageBackend()->notificationCollector(); +} diff --git a/autotests/server/fakeconnection.h b/autotests/server/fakeconnection.h new file mode 100644 index 0000000..fd2ba00 --- /dev/null +++ b/autotests/server/fakeconnection.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "connection.h" + +namespace Akonadi +{ +namespace Server +{ +class NotificationCollector; +class FakeAkonadiServer; + +class FakeConnection : public Connection +{ + Q_OBJECT + +public: + explicit FakeConnection(quintptr socketDescriptor, FakeAkonadiServer &akonadi); + explicit FakeConnection(AkonadiServer &akonadi); + ~FakeConnection() override; + +public Q_SLOTS: + Akonadi::Server::NotificationCollector *notificationCollector(); +}; + +} +} + diff --git a/autotests/server/fakedatastore.cpp b/autotests/server/fakedatastore.cpp new file mode 100644 index 0000000..750fcac --- /dev/null +++ b/autotests/server/fakedatastore.cpp @@ -0,0 +1,234 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakedatastore.h" +#include "akonadischema.h" +#include "dbpopulator.h" +#include "fakeakonadiserver.h" +#include "inspectablenotificationcollector.h" +#include "storage/dbconfig.h" +#include "storage/dbinitializer.h" + +#include + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(PimItem) +Q_DECLARE_METATYPE(PimItem::List) +Q_DECLARE_METATYPE(Collection) +Q_DECLARE_METATYPE(Flag) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(Tag) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(MimeType) +Q_DECLARE_METATYPE(QList) + +FakeDataStoreFactory::FakeDataStoreFactory(FakeAkonadiServer &akonadi) + : m_akonadi(akonadi) +{ +} + +DataStore *FakeDataStoreFactory::createStore() +{ + return new FakeDataStore(m_akonadi); +} + +FakeDataStore::FakeDataStore(FakeAkonadiServer &akonadi) + : DataStore(akonadi) + , mPopulateDb(true) +{ + mNotificationCollector = std::make_unique(m_akonadi, this); +} + +FakeDataStore::~FakeDataStore() = default; + +bool FakeDataStore::init() +{ + if (!DataStore::init()) { + return false; + } + + if (mPopulateDb) { + DbPopulator dbPopulator; + if (!dbPopulator.run()) { + qWarning() << "Failed to populate database"; + return false; + } + } + + return true; +} + +bool FakeDataStore::setItemsFlags(const PimItem::List &items, + const QVector *currentFlags, + const QVector &flags, + bool *flagsChanged, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("setItemsFlags"), + QVariantList() << QVariant::fromValue(items) << QVariant::fromValue(flags) << QVariant::fromValue(col) << silent); + return DataStore::setItemsFlags(items, currentFlags, flags, flagsChanged, col, silent); +} + +bool FakeDataStore::appendItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged, + bool checkIfExists, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("appendItemsFlags"), + QVariantList() << QVariant::fromValue(items) << QVariant::fromValue(flags) << checkIfExists << QVariant::fromValue(col) << silent); + return DataStore::appendItemsFlags(items, flags, flagsChanged, checkIfExists, col, silent); +} + +bool FakeDataStore::removeItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, const Collection &col, bool silent) +{ + mChanges.insert(QStringLiteral("removeItemsFlags"), + QVariantList() << QVariant::fromValue(items) << QVariant::fromValue(flags) << QVariant::fromValue(col) << silent); + return DataStore::removeItemsFlags(items, flags, flagsChanged, col, silent); +} + +bool FakeDataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) +{ + mChanges.insert(QStringLiteral("setItemsTags"), QVariantList() << QVariant::fromValue(items) << QVariant::fromValue(tags) << silent); + return DataStore::setItemsTags(items, tags, tagsChanged, silent); +} + +bool FakeDataStore::appendItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged, + bool checkIfExists, + const Collection &col, + bool silent) +{ + mChanges.insert(QStringLiteral("appendItemsTags"), + QVariantList() << QVariant::fromValue(items) << QVariant::fromValue(tags) << checkIfExists << QVariant::fromValue(col) << silent); + return DataStore::appendItemsTags(items, tags, tagsChanged, checkIfExists, col, silent); +} + +bool FakeDataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) +{ + mChanges.insert(QStringLiteral("removeItemsTags"), QVariantList() << QVariant::fromValue(items) << QVariant::fromValue(tags) << silent); + return DataStore::removeItemsTags(items, tags, tagsChanged, silent); +} + +bool FakeDataStore::removeItemParts(const PimItem &item, const QSet &parts) +{ + mChanges.insert(QStringLiteral("remoteItemParts"), QVariantList() << QVariant::fromValue(item) << QVariant::fromValue(parts)); + return DataStore::removeItemParts(item, parts); +} + +bool FakeDataStore::invalidateItemCache(const PimItem &item) +{ + mChanges.insert(QStringLiteral("invalidateItemCache"), QVariantList() << QVariant::fromValue(item)); + return DataStore::invalidateItemCache(item); +} + +bool FakeDataStore::appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap &attributes) +{ + mChanges.insert(QStringLiteral("appendCollection"), QVariantList() << QVariant::fromValue(collection) << mimeTypes << QVariant::fromValue(attributes)); + return DataStore::appendCollection(collection, mimeTypes, attributes); +} + +bool FakeDataStore::cleanupCollection(Collection &collection) +{ + mChanges.insert(QStringLiteral("cleanupCollection"), QVariantList() << QVariant::fromValue(collection)); + return DataStore::cleanupCollection(collection); +} + +bool FakeDataStore::cleanupCollection_slow(Collection &collection) +{ + mChanges.insert(QStringLiteral("cleanupCollection_slow"), QVariantList() << QVariant::fromValue(collection)); + return DataStore::cleanupCollection_slow(collection); +} + +bool FakeDataStore::moveCollection(Collection &collection, const Collection &newParent) +{ + mChanges.insert(QStringLiteral("moveCollection"), QVariantList() << QVariant::fromValue(collection) << QVariant::fromValue(newParent)); + return DataStore::moveCollection(collection, newParent); +} + +bool FakeDataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes) +{ + mChanges.insert(QStringLiteral("appendMimeTypeForCollection"), QVariantList() << collectionId << QVariant::fromValue(mimeTypes)); + return DataStore::appendMimeTypeForCollection(collectionId, mimeTypes); +} + +void FakeDataStore::activeCachePolicy(Collection &col) +{ + mChanges.insert(QStringLiteral("activeCachePolicy"), QVariantList() << QVariant::fromValue(col)); + return DataStore::activeCachePolicy(col); +} + +bool FakeDataStore::appendPimItem(QVector &parts, + const QVector &flags, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem) +{ + mChanges.insert(QStringLiteral("appendPimItem"), + QVariantList() << QVariant::fromValue(mimetype) << QVariant::fromValue(collection) << dateTime << remote_id << remoteRevision << gid); + return DataStore::appendPimItem(parts, flags, mimetype, collection, dateTime, remote_id, remoteRevision, gid, pimItem); +} + +bool FakeDataStore::cleanupPimItems(const PimItem::List &items, bool silent) +{ + mChanges.insert(QStringLiteral("cleanupPimItems"), QVariantList() << QVariant::fromValue(items) << silent); + return DataStore::cleanupPimItems(items, silent); +} + +bool FakeDataStore::unhidePimItem(PimItem &pimItem) +{ + mChanges.insert(QStringLiteral("unhidePimItem"), QVariantList() << QVariant::fromValue(pimItem)); + return DataStore::unhidePimItem(pimItem); +} + +bool FakeDataStore::unhideAllPimItems() +{ + mChanges.insert(QStringLiteral("unhideAllPimItems"), QVariantList()); + return DataStore::unhideAllPimItems(); +} + +bool FakeDataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent) +{ + mChanges.insert(QStringLiteral("addCollectionAttribute"), QVariantList() << QVariant::fromValue(col) << key << value << silent); + return DataStore::addCollectionAttribute(col, key, value, silent); +} + +bool FakeDataStore::removeCollectionAttribute(const Collection &col, const QByteArray &key) +{ + mChanges.insert(QStringLiteral("removeCollectionAttribute"), QVariantList() << QVariant::fromValue(col) << key); + return DataStore::removeCollectionAttribute(col, key); +} + +bool FakeDataStore::beginTransaction(const QString &name) +{ + mChanges.insert(QStringLiteral("beginTransaction"), QVariantList() << name); + return DataStore::beginTransaction(name); +} + +bool FakeDataStore::commitTransaction() +{ + mChanges.insert(QStringLiteral("commitTransaction"), QVariantList()); + return DataStore::commitTransaction(); +} + +bool FakeDataStore::rollbackTransaction() +{ + mChanges.insert(QStringLiteral("rollbackTransaction"), QVariantList()); + return DataStore::rollbackTransaction(); +} + +void FakeDataStore::setPopulateDb(bool populate) +{ + mPopulateDb = populate; +} diff --git a/autotests/server/fakedatastore.h b/autotests/server/fakedatastore.h new file mode 100644 index 0000000..66db8c2 --- /dev/null +++ b/autotests/server/fakedatastore.h @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "storage/datastore.h" + +namespace Akonadi +{ +namespace Server +{ +class FakeAkonadiServer; + +class FakeDataStoreFactory : public DataStoreFactory +{ +public: + FakeDataStoreFactory(FakeAkonadiServer &akonadi); + DataStore *createStore() override; + +private: + FakeAkonadiServer &m_akonadi; +}; + +class FakeDataStore : public DataStore +{ + Q_OBJECT + friend class FakeDataStoreFactory; + +public: + ~FakeDataStore() override; + + bool init() override; + + QMap changes() const + { + return mChanges; + } + + bool setItemsFlags(const PimItem::List &items, + const QVector *currentFlags, + const QVector &flags, + bool *flagsChanged = nullptr, + const Collection &col = Collection(), + bool silent = false) override; + bool appendItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged = nullptr, + bool checkIfExists = true, + const Collection &col = Collection(), + bool silent = false) override; + bool removeItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged = nullptr, + const Collection &col = Collection(), + bool silent = false) override; + + bool setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool silent = false) override; + bool appendItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged = nullptr, + bool checkIfExists = true, + const Collection &col = Collection(), + bool silent = false) override; + bool removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool silent = false) override; + + bool removeItemParts(const PimItem &item, const QSet &parts) override; + + bool invalidateItemCache(const PimItem &item) override; + + bool appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap &attributes) override; + + bool cleanupCollection(Collection &collection) override; + bool cleanupCollection_slow(Collection &collection) override; + + bool moveCollection(Collection &collection, const Collection &newParent) override; + + virtual bool appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes) override; + + void activeCachePolicy(Collection &col) override; + + bool appendPimItem(QVector &parts, + const QVector &flags, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem) override; + + bool cleanupPimItems(const PimItem::List &items, bool silent = false) override; + + bool unhidePimItem(PimItem &pimItem) override; + bool unhideAllPimItems() override; + + bool addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent = false) override; + bool removeCollectionAttribute(const Collection &col, const QByteArray &key) override; + + bool beginTransaction(const QString &name = QString()) override; + bool rollbackTransaction() override; + bool commitTransaction() override; + + void setPopulateDb(bool populate); + +protected: + FakeDataStore(FakeAkonadiServer &akonadi); + + QMap mChanges; + +private: + bool mPopulateDb; +}; + +} +} + diff --git a/autotests/server/fakeentities.h b/autotests/server/fakeentities.h new file mode 100644 index 0000000..d1c68be --- /dev/null +++ b/autotests/server/fakeentities.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" + +namespace Akonadi +{ +namespace Server +{ +class FakePart : public Part +{ +public: + FakePart() + : Part() + { + } + + void setPartType(const PartType &partType) + { + m_partType = partType; + Part::setPartType(partType); + } + + PartType partType() const + { + return m_partType; + } + +private: + PartType m_partType; +}; + +class FakeTag : public Tag +{ +public: + FakeTag() + : Tag() + { + } + + void setTagType(const TagType &tagType) + { + m_tagType = tagType; + Tag::setTagType(tagType); + } + + TagType tagType() const + { + return m_tagType; + } + + void setRemoteId(const QString &remoteId) + { + m_remoteId = remoteId; + } + + QString remoteId() const + { + return m_remoteId; + } + +private: + TagType m_tagType; + QString m_remoteId; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/autotests/server/fakeintervalcheck.cpp b/autotests/server/fakeintervalcheck.cpp new file mode 100644 index 0000000..d5d8ae3 --- /dev/null +++ b/autotests/server/fakeintervalcheck.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakeintervalcheck.h" + +using namespace Akonadi::Server; + +FakeIntervalCheck::FakeIntervalCheck(ItemRetrievalManager &retrievalManager) + : IntervalCheck(retrievalManager) +{ +} + +void FakeIntervalCheck::waitForInit() +{ + m_initCalled.acquire(); +} + +void FakeIntervalCheck::init() +{ + IntervalCheck::init(); + m_initCalled.release(); +} + +bool FakeIntervalCheck::shouldScheduleCollection(const Collection &collection) +{ + return (collection.syncPref() == Collection::True) || ((collection.syncPref() == Collection::Undefined) && collection.enabled()); +} + +bool FakeIntervalCheck::hasChanged(const Collection &collection, const Collection &changed) +{ + Q_ASSERT(collection.id() == changed.id()); + return collection.cachePolicyCheckInterval() != changed.cachePolicyCheckInterval(); +} + +int FakeIntervalCheck::collectionScheduleInterval(const Collection &collection) +{ + return collection.cachePolicyCheckInterval(); +} + +void FakeIntervalCheck::collectionExpired(const Collection &collection) +{ + Q_UNUSED(collection) + // Nothing here. The granularity is in whole minutes, we don't have time to wait for that in a unittest. +} diff --git a/autotests/server/fakeintervalcheck.h b/autotests/server/fakeintervalcheck.h new file mode 100644 index 0000000..c9ae977 --- /dev/null +++ b/autotests/server/fakeintervalcheck.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace Akonadi +{ +namespace Server +{ +class ItemRetrievalManager; + +class FakeIntervalCheck : public IntervalCheck +{ + Q_OBJECT +public: + FakeIntervalCheck(ItemRetrievalManager &retrievalManager); + void waitForInit(); + +protected: + void init() override; + + bool shouldScheduleCollection(const Collection &) override; + bool hasChanged(const Collection &collection, const Collection &changed) override; + int collectionScheduleInterval(const Collection &collection) override; + void collectionExpired(const Collection &collection) override; + +private: + QSemaphore m_initCalled; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/autotests/server/fakeitemretrievalmanager.cpp b/autotests/server/fakeitemretrievalmanager.cpp new file mode 100644 index 0000000..a85081f --- /dev/null +++ b/autotests/server/fakeitemretrievalmanager.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "fakeitemretrievalmanager.h" +#include "storage/itemretrievalrequest.h" + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(ItemRetrievalResult) + +FakeItemRetrievalManager::FakeItemRetrievalManager() +{ + qRegisterMetaType(); +} + +void FakeItemRetrievalManager::requestItemDelivery(ItemRetrievalRequest request) +{ + QMetaObject::invokeMethod( + this, + [this, r = std::move(request)] { + Q_EMIT requestFinished({r}); + }, + Qt::QueuedConnection); +} diff --git a/autotests/server/fakeitemretrievalmanager.h b/autotests/server/fakeitemretrievalmanager.h new file mode 100644 index 0000000..9006a97 --- /dev/null +++ b/autotests/server/fakeitemretrievalmanager.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "storage/itemretrievalmanager.h" + +namespace Akonadi +{ +namespace Server +{ +class FakeItemRetrievalManager : public ItemRetrievalManager +{ + Q_OBJECT +public: + explicit FakeItemRetrievalManager(); + + void requestItemDelivery(ItemRetrievalRequest request) override; +}; + +} // namespace Server +} // namespace Akonadi diff --git a/autotests/server/fakesearchmanager.cpp b/autotests/server/fakesearchmanager.cpp new file mode 100644 index 0000000..3c09baf --- /dev/null +++ b/autotests/server/fakesearchmanager.cpp @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "fakesearchmanager.h" + +#include "entities.h" + +using namespace Akonadi::Server; + +FakeSearchManager::FakeSearchManager(SearchTaskManager &agentSearchManager) + : SearchManager(QStringList(), agentSearchManager) +{ +} + +FakeSearchManager::~FakeSearchManager() +{ +} + +void FakeSearchManager::registerInstance(const QString &id) +{ + Q_UNUSED(id) +} + +void FakeSearchManager::unregisterInstance(const QString &id) +{ + Q_UNUSED(id) +} + +void FakeSearchManager::updateSearch(const Collection &collection) +{ + Q_UNUSED(collection) +} + +void FakeSearchManager::updateSearchAsync(const Collection &collection) +{ + Q_UNUSED(collection) +} + +QVector FakeSearchManager::searchPlugins() const +{ + return QVector(); +} + +void FakeSearchManager::scheduleSearchUpdate() +{ +} diff --git a/autotests/server/fakesearchmanager.h b/autotests/server/fakesearchmanager.h new file mode 100644 index 0000000..d0cfd5c --- /dev/null +++ b/autotests/server/fakesearchmanager.h @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +class SearchTaskManager; + +/** + * Subclass of SearchManager that does nothing. + */ +class FakeSearchManager : public SearchManager +{ + Q_OBJECT + +public: + explicit FakeSearchManager(SearchTaskManager &searchTaskManager); + ~FakeSearchManager() override; + + void registerInstance(const QString &id) override; + void unregisterInstance(const QString &id) override; + void updateSearch(const Collection &collection) override; + void updateSearchAsync(const Collection &collection) override; + QVector searchPlugins() const override; + + void scheduleSearchUpdate() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/autotests/server/fetchhandlertest.cpp b/autotests/server/fetchhandlertest.cpp new file mode 100644 index 0000000..28cabf1 --- /dev/null +++ b/autotests/server/fetchhandlertest.cpp @@ -0,0 +1,259 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include +#include + +#include "aktest.h" +#include "dbinitializer.h" +#include "entities.h" +#include "fakeakonadiserver.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::Tag::List) +Q_DECLARE_METATYPE(Akonadi::Server::Tag) + +class FetchHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + FetchHandlerTest() + : QObject() + { + qRegisterMetaType(); + + mAkonadi.setPopulateDb(false); + mAkonadi.init(); + } + + Protocol::FetchItemsCommandPtr createCommand(const Scope &scope, const Protocol::ScopeContext &ctx = Protocol::ScopeContext()) + { + auto cmd = Protocol::FetchItemsCommandPtr::create(scope, ctx); + auto fetchScope = cmd->itemFetchScope(); + fetchScope.setFetch(Protocol::ItemFetchScope::IgnoreErrors); + cmd->setItemFetchScope(fetchScope); + return cmd; + } + + Protocol::FetchItemsResponsePtr createResponse(const PimItem &item) + { + auto resp = Protocol::FetchItemsResponsePtr::create(item.id()); + resp->setMimeType(item.mimeType().name()); + resp->setParentId(item.collectionId()); + + return resp; + } + + QScopedPointer initializer; + +private Q_SLOTS: + void testFetch_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col = initializer->createCollection("root"); + PimItem item1 = initializer->createItem("item1", col); + PimItem item2 = initializer->createItem("item2", col); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item1.id())) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + + QTest::newRow("basic fetch") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item2)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + QTest::newRow("collection context") << scenarios; + } + } + + void testFetch() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testFetchByTag_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col = initializer->createCollection("root"); + PimItem item1 = initializer->createItem("item1", col); + PimItem item2 = initializer->createItem("item2", col); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid")); + tag.insert(); + + item1.addTag(tag); + item1.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Tag, tag.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + + QTest::newRow("fetch by tag") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() << mAkonadi.selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Tag, tag.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + + QTest::newRow("uid fetch by tag from resource") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() << mAkonadi.selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item2)) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + + QTest::newRow("fetch collection") << scenarios; + } + } + + void testFetchByTag() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testFetchCommandContext_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + Collection col2 = initializer->createCollection("col2"); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid")); + tag.insert(); + + item1.addTag(tag); + item1.update(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() << mAkonadi.selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col2.id()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()) + << TestScenario::create(6, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Tag, tag.id()))) + << TestScenario::create(6, TestScenario::ServerCmd, createResponse(item1)) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + + // Special case that used to be broken due to persistent command context + QTest::newRow("fetch by tag after collection") << scenarios; + } + } + + void testFetchCommandContext() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } + + void testList_data() + { + QElapsedTimer timer; + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + + timer.start(); + QList items; + for (int i = 0; i < 1000; i++) { + items.append(initializer->createItem(QString::number(i).toLatin1().constData(), col1)); + } + qDebug() << timer.nsecsElapsed() / 1.0e6 << "ms"; + timer.start(); + + QTest::addColumn("scenarios"); + + { + TestScenario::List scenarios; + scenarios << mAkonadi.loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + createCommand(ImapSet::all(), Protocol::ScopeContext(Protocol::ScopeContext::Collection, col1.id()))); + while (!items.isEmpty()) { + const PimItem &item = items.takeLast(); + scenarios << TestScenario::create(5, TestScenario::ServerCmd, createResponse(item)); + } + scenarios << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchItemsResponsePtr::create()); + QTest::newRow("complete list") << scenarios; + } + qDebug() << timer.nsecsElapsed() / 1.0e6 << "ms"; + } + + void testList() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + // StorageDebugger::instance()->enableSQLDebugging(true); + // StorageDebugger::instance()->writeToFile(QStringLiteral("sqllog.txt")); + mAkonadi.runTest(); + } +}; + +AKTEST_FAKESERVER_MAIN(FetchHandlerTest) + +#include "fetchhandlertest.moc" diff --git a/autotests/server/handlertest.cpp b/autotests/server/handlertest.cpp new file mode 100644 index 0000000..368a774 --- /dev/null +++ b/autotests/server/handlertest.cpp @@ -0,0 +1,191 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include +#include + +#include + +#include "handler.h" +#include "handler/collectioncopyhandler.h" +#include "handler/collectioncreatehandler.h" +#include "handler/collectiondeletehandler.h" +#include "handler/collectionfetchhandler.h" +#include "handler/collectionmodifyhandler.h" +#include "handler/collectionmovehandler.h" +#include "handler/collectionstatsfetchhandler.h" +#include "handler/itemcopyhandler.h" +#include "handler/itemcreatehandler.h" +#include "handler/itemdeletehandler.h" +#include "handler/itemfetchhandler.h" +#include "handler/itemlinkhandler.h" +#include "handler/itemmodifyhandler.h" +#include "handler/itemmovehandler.h" +#include "handler/loginhandler.h" +#include "handler/logouthandler.h" +#include "handler/resourceselecthandler.h" +#include "handler/searchcreatehandler.h" +#include "handler/searchhandler.h" +#include "handler/transactionhandler.h" + +#include "fakeakonadiserver.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define MAKE_CMD_ROW(command, class) QTest::newRow(#command) << (command) << QByteArray(typeid(Akonadi::Server::class).name()); + +class HandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + explicit HandlerTest() + { + mAkonadi.init(); + } + +private: + void setupTestData() + { + QTest::addColumn("command"); + QTest::addColumn("className"); + } + + void addAuthCommands() + { + MAKE_CMD_ROW(Protocol::Command::CreateCollection, CollectionCreateHandler) + MAKE_CMD_ROW(Protocol::Command::FetchCollections, CollectionFetchHandler) + MAKE_CMD_ROW(Protocol::Command::StoreSearch, SearchCreateHandler) + MAKE_CMD_ROW(Protocol::Command::Search, SearchHandler) + MAKE_CMD_ROW(Protocol::Command::FetchItems, ItemFetchHandler) + MAKE_CMD_ROW(Protocol::Command::ModifyItems, ItemModifyHandler) + MAKE_CMD_ROW(Protocol::Command::FetchCollectionStats, CollectionStatsFetchHandler) + MAKE_CMD_ROW(Protocol::Command::DeleteCollection, CollectionDeleteHandler) + MAKE_CMD_ROW(Protocol::Command::ModifyCollection, CollectionModifyHandler) + MAKE_CMD_ROW(Protocol::Command::Transaction, TransactionHandler) + MAKE_CMD_ROW(Protocol::Command::CreateItem, ItemCreateHandler) + MAKE_CMD_ROW(Protocol::Command::CopyItems, ItemCopyHandler) + MAKE_CMD_ROW(Protocol::Command::CopyCollection, CollectionCopyHandler) + MAKE_CMD_ROW(Protocol::Command::LinkItems, ItemLinkHandler) + MAKE_CMD_ROW(Protocol::Command::SelectResource, ResourceSelectHandler) + MAKE_CMD_ROW(Protocol::Command::DeleteItems, ItemDeleteHandler) + MAKE_CMD_ROW(Protocol::Command::MoveItems, ItemMoveHandler) + MAKE_CMD_ROW(Protocol::Command::MoveCollection, CollectionMoveHandler) + } + + void addNonAuthCommands() + { + MAKE_CMD_ROW(Protocol::Command::Login, LoginHandler) + } + + void addAlwaysCommands() + { + MAKE_CMD_ROW(Protocol::Command::Logout, LogoutHandler) + } + + void addInvalidCommands() + { + // MAKE_CMD_ROW(Protocol::Command::Invalid, UnknownCommandHandler) + } + + template QByteArray typeName(const T &t) + { + const auto &v = *t; + return typeid(v).name(); + } + +private Q_SLOTS: + void testFindAuthenticatedCommand_data() + { + setupTestData(); + addAuthCommands(); + } + + void testFindAuthenticatedCommand() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QByteArray, className); + const auto handler = Handler::findHandlerForCommandAuthenticated(command, mAkonadi); + QVERIFY(handler); + QCOMPARE(typeName(handler), className); + } + + void testFindAuthenticatedCommandNegative_data() + { + setupTestData(); + addNonAuthCommands(); + addAlwaysCommands(); + addInvalidCommands(); + } + + void testFindAuthenticatedCommandNegative() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QByteArray, className); + + const auto handler = Handler::findHandlerForCommandAuthenticated(command, mAkonadi); + QVERIFY(!handler); + } + + void testFindNonAutenticatedCommand_data() + { + setupTestData(); + addNonAuthCommands(); + } + + void testFindNonAutenticatedCommand() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QByteArray, className); + + auto handler = Handler::findHandlerForCommandNonAuthenticated(command, mAkonadi); + QVERIFY(handler); + QCOMPARE(typeName(handler), className); + } + + void testFindAlwaysCommand_data() + { + setupTestData(); + addAlwaysCommands(); + } + + void testFindAlwaysCommand() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QByteArray, className); + + const auto handler = Handler::findHandlerForCommandAlwaysAllowed(command, mAkonadi); + QVERIFY(handler); + QCOMPARE(typeName(handler), className); + } + + void testFindAlwaysCommandNegative_data() + { + setupTestData(); + addAuthCommands(); + addNonAuthCommands(); + addInvalidCommands(); + } + + void testFindAlwaysCommandNegative() + { + QFETCH(Protocol::Command::Type, command); + QFETCH(QByteArray, className); + + const auto handler = Handler::findHandlerForCommandAlwaysAllowed(command, mAkonadi); + QVERIFY(!handler); + } +}; + +AKTEST_MAIN(HandlerTest) + +#include "handlertest.moc" diff --git a/autotests/server/inspectablenotificationcollector.cpp b/autotests/server/inspectablenotificationcollector.cpp new file mode 100644 index 0000000..7594685 --- /dev/null +++ b/autotests/server/inspectablenotificationcollector.cpp @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2018 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "inspectablenotificationcollector.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +InspectableNotificationCollector::InspectableNotificationCollector(AkonadiServer &akonadi, DataStore *store) + : NotificationCollector(akonadi, store) +{ +} + +void InspectableNotificationCollector::notify(Protocol::ChangeNotificationList &&ntfs) +{ + Q_EMIT notifySignal(ntfs); + NotificationCollector::notify(std::move(ntfs)); +} diff --git a/autotests/server/inspectablenotificationcollector.h b/autotests/server/inspectablenotificationcollector.h new file mode 100644 index 0000000..d7461fd --- /dev/null +++ b/autotests/server/inspectablenotificationcollector.h @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2018 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include "storage/notificationcollector.h" + +namespace Akonadi +{ +namespace Server +{ +class AkonadiServer; +class DataStore; + +class InspectableNotificationCollector : public QObject, public NotificationCollector +{ + Q_OBJECT +public: + InspectableNotificationCollector(AkonadiServer &akonadi, DataStore *store); + ~InspectableNotificationCollector() override = default; + + void notify(Protocol::ChangeNotificationList &&ntfs) override; + +Q_SIGNALS: + void notifySignal(const Akonadi::Protocol::ChangeNotificationList &msgs); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/autotests/server/itemcreatehandlertest.cpp b/autotests/server/itemcreatehandlertest.cpp new file mode 100644 index 0000000..5b8b0c5 --- /dev/null +++ b/autotests/server/itemcreatehandlertest.cpp @@ -0,0 +1,887 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include + +#include +#include + +#include "fakeakonadiserver.h" +#include "fakeentities.h" + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(PimItem) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(QVector) +Q_DECLARE_METATYPE(QVector) + +class ItemCreateHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + ItemCreateHandlerTest() + { + // Effectively disable external payload parts, we have a dedicated unit-test + // for that + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + settings.setValue(QStringLiteral("General/SizeThreshold"), std::numeric_limits::max()); + + mAkonadi.init(); + } + + void updatePimItem(PimItem &pimItem, const QString &remoteId, const qint64 size) + { + pimItem.setRemoteId(remoteId); + pimItem.setGid(remoteId); + pimItem.setSize(size); + } + + void updateNotifcationEntity(Protocol::ItemChangeNotificationPtr &ntf, const PimItem &pimItem) + { + Protocol::FetchItemsResponse item; + item.setId(pimItem.id()); + item.setRemoteId(pimItem.remoteId()); + item.setRemoteRevision(pimItem.remoteRevision()); + item.setMimeType(pimItem.mimeType().name()); + ntf->setItems({std::move(item)}); + } + + struct PartHelper { + PartHelper(const QString &type_, const QByteArray &data_, int size_, Part::Storage storage_ = Part::Internal, int version_ = 0) + : type(type_) + , data(data_) + , size(size_) + , storage(storage_) + , version(version_) + { + } + QString type; + QByteArray data; + int size; + Part::Storage storage; + int version; + }; + + void updateParts(QVector &parts, const std::vector &updatedParts) + { + parts.clear(); + Q_FOREACH (const PartHelper &helper, updatedParts) { + FakePart part; + + const QStringList types = helper.type.split(QLatin1Char(':')); + Q_ASSERT(types.count() == 2); + part.setPartType(PartType(types[1], types[0])); + part.setData(helper.data); + part.setDatasize(helper.size); + part.setStorage(helper.storage); + part.setVersion(helper.version); + parts << part; + } + } + + void updateFlags(QVector &flags, const QStringList &updatedFlags) + { + flags.clear(); + for (const QString &flagName : updatedFlags) { + Flag flag; + flag.setName(flagName); + flags << flag; + } + } + + struct TagHelper { + TagHelper(const QString &tagType_, const QString &gid_, const QString &remoteId_ = QString()) + : tagType(tagType_) + , gid(gid_) + , remoteId(remoteId_) + { + } + QString tagType; + QString gid; + QString remoteId; + }; + void updateTags(QVector &tags, const std::vector &updatedTags) + { + tags.clear(); + Q_FOREACH (const TagHelper &helper, updatedTags) { + FakeTag tag; + + TagType tagType; + tagType.setName(helper.tagType); + + tag.setTagType(tagType); + tag.setGid(helper.gid); + tag.setRemoteId(helper.remoteId); + tags << tag; + } + } + + Protocol::CreateItemCommandPtr createCommand(const PimItem &pimItem, const QDateTime &dt, const QSet &parts, qint64 overrideSize = -1) + { + const qint64 size = overrideSize > -1 ? overrideSize : pimItem.size(); + + auto cmd = Protocol::CreateItemCommandPtr::create(); + cmd->setCollection(Scope(pimItem.collectionId())); + cmd->setItemSize(size); + cmd->setRemoteId(pimItem.remoteId()); + cmd->setRemoteRevision(pimItem.remoteRevision()); + cmd->setMimeType(pimItem.mimeType().name()); + cmd->setGid(pimItem.gid()); + cmd->setDateTime(dt); + cmd->setParts(parts); + + return cmd; + } + + Protocol::FetchItemsResponsePtr createResponse(qint64 expectedId, + const PimItem &pimItem, + const QDateTime &datetime, + const QVector &parts, + qint64 overrideSize = -1) + { + const qint64 size = overrideSize > -1 ? overrideSize : pimItem.size(); + + auto resp = Protocol::FetchItemsResponsePtr::create(expectedId); + resp->setParentId(pimItem.collectionId()); + resp->setSize(size); + resp->setRemoteId(pimItem.remoteId()); + resp->setRemoteRevision(pimItem.remoteRevision()); + resp->setMimeType(pimItem.mimeType().name()); + resp->setGid(pimItem.gid()); + resp->setMTime(datetime); + resp->setParts(parts); + resp->setAncestors({Protocol::Ancestor(4, QLatin1String("ColC"))}); + + return resp; + } + + TestScenario errorResponse(const QString &errorMsg) + { + auto response = Protocol::CreateItemResponsePtr::create(); + response->setError(1, errorMsg); + return TestScenario::create(5, TestScenario::ServerCmd, response); + } + +private Q_SLOTS: + void testItemCreate_data() + { + using Notifications = QVector; + + QTest::addColumn("scenarios"); + QTest::addColumn("notifications"); + QTest::addColumn("pimItem"); + QTest::addColumn>("parts"); + QTest::addColumn>("flags"); + QTest::addColumn>("tags"); + QTest::addColumn("uidnext"); + QTest::addColumn("datetime"); + QTest::addColumn("expectFail"); + + TestScenario::List scenarios; + auto notification = Protocol::ItemChangeNotificationPtr::create(); + qint64 uidnext = 0; + QDateTime datetime(QDate(2014, 05, 12), QTime(14, 46, 00), Qt::UTC); + PimItem pimItem; + QVector parts; + QVector flags; + QVector tags; + + pimItem.setCollectionId(4); + pimItem.setSize(10); + pimItem.setRemoteId(QStringLiteral("TEST-1")); + pimItem.setRemoteRevision(QStringLiteral("1")); + pimItem.setGid(QStringLiteral("TEST-1")); + pimItem.setMimeType(MimeType::retrieveByName(QStringLiteral("application/octet-stream"))); + pimItem.setDatetime(datetime); + updateParts(parts, {{QLatin1String("PLD:DATA"), "0123456789", 10}}); + notification->setOperation(Protocol::ItemChangeNotification::Add); + notification->setParentCollection(4); + notification->setResource("akonadi_fake_resource_0"); + Protocol::FetchItemsResponse item; + item.setId(-1); + item.setRemoteId(QStringLiteral("TEST-1")); + item.setRemoteRevision(QStringLiteral("1")); + item.setMimeType(QStringLiteral("application/octet-stream")); + notification->setItems({std::move(item)}); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + uidnext = 13; + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 10))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "0123456789")) + << TestScenario::create(5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 10), "0123456789")})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("single-part") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-2"), 20); + updateParts(parts, {{QLatin1String("PLD:DATA"), "Random Data", 11}, {QLatin1String("PLD:PLDTEST"), "Test Data", 9}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA", "PLD:PLDTEST"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 11, 0))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "Random Data")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:PLDTEST", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:PLDTEST", Protocol::PartMetaData("PLD:PLDTEST", 9, 0))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:PLDTEST", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:PLDTEST", "Test Data")) + << TestScenario::create(5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 11), "Random Data"), + Protocol::StreamPayloadResponse("PLD:PLDTEST", Protocol::PartMetaData("PLD:PLDTEST", 9), "Test Data")})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("multi-part") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + TestScenario inScenario, outScenario; + { + auto cmd = Protocol::CreateItemCommandPtr::create(); + cmd->setCollection(Scope(100)); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << errorResponse(QStringLiteral("Invalid parent collection")); + QTest::newRow("invalid collection") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll + << QDateTime() << true; + + { + auto cmd = Protocol::CreateItemCommandPtr::create(); + cmd->setCollection(Scope(6)); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << errorResponse(QStringLiteral("Cannot append item into virtual collection")); + QTest::newRow("virtual collection") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll + << QDateTime() << true; + + updatePimItem(pimItem, QStringLiteral("TEST-3"), 5); + updateParts(parts, {{QLatin1String("PLD:DATA"), "12345", 5}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"}, 1)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "12345")) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse(uidnext, pimItem, datetime, {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5), "12345")})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("mismatch item sizes (smaller)") + << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-4"), 10); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"}, 10)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "12345")) + << TestScenario::create(5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5), "12345")}, + 10)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("mismatch item sizes (bigger)") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime + << false; + + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 5))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "123")) + << errorResponse(QStringLiteral("Payload size mismatch")); + QTest::newRow("incomplete part data") << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() + << -1ll << QDateTime() << true; + + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "1234567890")) + << errorResponse(QStringLiteral("Payload size mismatch")); + QTest::newRow("part data larger than advertised") + << scenarios << Notifications{} << PimItem() << QVector() << QVector() << QVector() << -1ll << QDateTime() << true; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-5"), 0); + updateParts(parts, {{QLatin1String("PLD:DATA"), QByteArray(), 0}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 0))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", QByteArray())) + << TestScenario::create(5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 0), QByteArray())})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("empty payload part") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-8"), 1); + updateParts(parts, {{QLatin1String("PLD:DATA"), QByteArray("\0", 1), 1}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 1))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", QByteArray("\0", 1))) + << TestScenario::create(5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 1), QByteArray("\0", 1))})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("part data will null character") + << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + const QString utf8String = QStringLiteral("äöüß@€µøđ¢©®"); + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-9"), utf8String.toUtf8().size()); + updateParts(parts, {{QLatin1String("PLD:DATA"), utf8String.toUtf8(), utf8String.toUtf8().size()}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", utf8String.toUtf8())) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse( + uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", utf8String.toUtf8().size()), utf8String.toUtf8())})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("utf8 part data") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + const QByteArray hugeData = QByteArray("a").repeated(1 << 20); + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-10"), 1 << 20); + updateParts(parts, {{QLatin1String("PLD:DATA"), hugeData, 1 << 20}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", hugeData)) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()), hugeData)})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("huge part data") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + const QByteArray dataWithNewLines = "Bernard, Bernard, Bernard, Bernard, look, look Bernard!\nWHAT!!!!!!!\nI'm a prostitute robot from the future!"; + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-11"), dataWithNewLines.size()); + updateParts(parts, {{QLatin1String("PLD:DATA"), dataWithNewLines, dataWithNewLines.size()}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", dataWithNewLines)) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", dataWithNewLines.size()), dataWithNewLines)})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("data with newlines") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + const QByteArray lotsOfNewlines = QByteArray("\n").repeated(1 << 20); + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-12"), lotsOfNewlines.size()); + updateParts(parts, {{QLatin1String("PLD:DATA"), lotsOfNewlines, lotsOfNewlines.size()}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios + << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:DATA"})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", lotsOfNewlines)) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:DATA", Protocol::PartMetaData("PLD:DATA", parts.first().datasize()), lotsOfNewlines)})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("data with lots of newlines") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime + << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-13"), 20); + updateParts(parts, {{QLatin1String("PLD:NEWPARTTYPE1"), "0123456789", 10}, {QLatin1String("PLD:NEWPARTTYPE2"), "9876543210", 10}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, createCommand(pimItem, datetime, {"PLD:NEWPARTTYPE1", "PLD:NEWPARTTYPE2"})) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE2", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE2", Protocol::PartMetaData("PLD:NEWPARTTYPE2", 10))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE2", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE2", "9876543210")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE1", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE1", Protocol::PartMetaData("PLD:NEWPARTTYPE1", 10))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:NEWPARTTYPE1", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:NEWPARTTYPE1", "0123456789")) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse(uidnext, + pimItem, + datetime, + {Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE2", Protocol::PartMetaData("PLD:NEWPARTTYPE2", 10), "9876543210"), + Protocol::StreamPayloadResponse("PLD:NEWPARTTYPE1", Protocol::PartMetaData("PLD:NEWPARTTYPE1", 10), "0123456789")})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("non-existent part types") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime + << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-14"), 0); + updateParts(parts, {}); + updateFlags(flags, QStringList() << QStringLiteral("\\SEEN") << QStringLiteral("\\RANDOM")); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setFlags({"\\SEEN", "\\RANDOM"}); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setFlags({"\\SEEN", "\\RANDOM"}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with flags") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-15"), 0); + updateFlags(flags, {}); + updateTags(tags, {{QLatin1String("PLAIN"), QLatin1String("TAG-1")}, {QLatin1String("PLAIN"), QLatin1String("TAG-2")}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setTags(Scope(Scope::Gid, {QLatin1String("TAG-1"), QLatin1String("TAG-2")})); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setTags({Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN")}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with non-existent tags (GID)") + << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-16"), 0); + updateTags(tags, {{QLatin1String("PLAIN"), QLatin1String("TAG-3")}, {QLatin1String("PLAIN"), QLatin1String("TAG-4")}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setTags(Scope(Scope::Rid, {QLatin1String("TAG-3"), QLatin1String("TAG-4")})); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setTags({Protocol::FetchTagsResponse(4, "TAG-3", "PLAIN"), Protocol::FetchTagsResponse(5, "TAG-4", "PLAIN")}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(QStringLiteral("akonadi_fake_resource_0")) << inScenario + << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with non-existent tags (RID)") + << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-17"), 0); + updateNotifcationEntity(notification, pimItem); + updateTags(tags, {{QLatin1String("PLAIN"), QLatin1String("TAG-1")}, {QLatin1String("PLAIN"), QLatin1String("TAG-2")}}); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setTags(Scope(Scope::Rid, {QLatin1String("TAG-1"), QLatin1String("TAG-2")})); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setTags({Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN")}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(QStringLiteral("akonadi_fake_resource_0")) << inScenario + << outScenario << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with existing tags (RID)") + << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-18"), 0); + updateNotifcationEntity(notification, pimItem); + updateTags(tags, {{QLatin1String("PLAIN"), QLatin1String("TAG-3")}, {QLatin1String("PLAIN"), QLatin1String("TAG-4")}}); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setTags(Scope(Scope::Gid, {QLatin1String("TAG-3"), QLatin1String("TAG-4")})); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setTags({Protocol::FetchTagsResponse(4, "TAG-3", "PLAIN"), Protocol::FetchTagsResponse(5, "TAG-4", "PLAIN")}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with existing tags (GID)") + << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-19"), 0); + updateFlags(flags, QStringList() << QStringLiteral("\\SEEN") << QStringLiteral("$FLAG")); + updateTags(tags, {{QLatin1String("PLAIN"), QLatin1String("TAG-1")}, {QLatin1String("PLAIN"), QLatin1String("TAG-2")}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setTags(Scope(Scope::Gid, {QLatin1String("TAG-1"), QLatin1String("TAG-2")})); + cmd->setFlags({"\\SEEN", "$FLAG"}); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setTags({Protocol::FetchTagsResponse(2, "TAG-1", "PLAIN"), Protocol::FetchTagsResponse(3, "TAG-2", "PLAIN")}); + rsp->setFlags({"\\SEEN", "$FLAG"}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with flags and tags") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime + << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-20"), 0); + updateFlags(flags, {}); + updateTags(tags, {{QLatin1String("PLAIN"), utf8String}}); + updateNotifcationEntity(notification, pimItem); + ++uidnext; + { + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setTags(Scope(Scope::Gid, {utf8String})); + inScenario = TestScenario::create(5, TestScenario::ClientCmd, cmd); + + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + rsp->setTags({Protocol::FetchTagsResponse(6, utf8String.toUtf8(), "PLAIN")}); + outScenario = TestScenario::create(5, TestScenario::ServerCmd, rsp); + } + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() << inScenario << outScenario + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + QTest::newRow("item with UTF-8 tag") << scenarios << Notifications{notification} << pimItem << parts << flags << tags << uidnext << datetime << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-21"), 0); + updateFlags(flags, {}); + updateTags(tags, {}); + pimItem.setGid(QStringLiteral("GID-21")); + updateNotifcationEntity(notification, pimItem); + scenarios = FakeAkonadiServer::loginScenario(); + // Create a normal item with RID + { + ++uidnext; + auto cmd = createCommand(pimItem, datetime, {}); + scenarios << TestScenario::create(5, TestScenario::ClientCmd, cmd); + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + scenarios << TestScenario::create(5, TestScenario::ServerCmd, rsp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + } + // Create the same item again (no merging, so it will just be created) + { + ++uidnext; + auto cmd = createCommand(pimItem, datetime, {}); + scenarios << TestScenario::create(6, TestScenario::ClientCmd, cmd); + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + scenarios << TestScenario::create(6, TestScenario::ServerCmd, rsp) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + } + // Now try to create the item once again, but in merge mode, we should fail now + { + ++uidnext; + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setMergeModes(Protocol::CreateItemCommand::RemoteID); + scenarios << TestScenario::create(7, TestScenario::ClientCmd, cmd); + auto rsp = Protocol::CreateItemResponsePtr::create(); + rsp->setError(1, QStringLiteral("Multiple merge candidates")); + scenarios << TestScenario::create(7, TestScenario::ServerCmd, rsp); + } + Notifications notifications = {notification, Protocol::ItemChangeNotificationPtr::create(*notification)}; + QTest::newRow("multiple merge candidates (RID)") << scenarios << notifications << pimItem << parts << flags << tags << uidnext << datetime << true; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-22"), 0); + pimItem.setGid(QStringLiteral("GID-22")); + updateNotifcationEntity(notification, pimItem); + scenarios = FakeAkonadiServer::loginScenario(); + // Create a normal item with GID + { + // Don't increase uidnext, we will reuse the one from previous test, + // since that did not actually create a new Item + auto cmd = createCommand(pimItem, datetime, {}); + scenarios << TestScenario::create(5, TestScenario::ClientCmd, cmd); + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + scenarios << TestScenario::create(5, TestScenario::ServerCmd, rsp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + } + // Create the same item again (no merging, so it will just be created) + { + ++uidnext; + auto cmd = createCommand(pimItem, datetime, {}); + scenarios << TestScenario::create(6, TestScenario::ClientCmd, cmd); + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + scenarios << TestScenario::create(6, TestScenario::ServerCmd, rsp) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + } + // Now try to create the item once again, but in merge mode, we should fail now + { + ++uidnext; + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setMergeModes(Protocol::CreateItemCommand::GID); + scenarios << TestScenario::create(7, TestScenario::ClientCmd, cmd); + auto rsp = Protocol::CreateItemResponsePtr::create(); + rsp->setError(1, QStringLiteral("Multiple merge candidates")); + scenarios << TestScenario::create(7, TestScenario::ServerCmd, rsp); + } + notifications = {notification, Protocol::ItemChangeNotificationPtr::create(*notification)}; + QTest::newRow("multiple merge candidates (GID)") << scenarios << notifications << pimItem << parts << flags << tags << uidnext << datetime << true; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + updatePimItem(pimItem, QStringLiteral("TEST-23"), 0); + pimItem.setGid(QString()); + updateNotifcationEntity(notification, pimItem); + scenarios = FakeAkonadiServer::loginScenario(); + // Create a normal item with RID, but with empty GID + { + // Don't increase uidnext, we will reuse the one from previous test, + // since that did not actually create a new Item + auto cmd = createCommand(pimItem, datetime, {}); + scenarios << TestScenario::create(5, TestScenario::ClientCmd, cmd); + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + scenarios << TestScenario::create(5, TestScenario::ServerCmd, rsp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + } + // Merge by GID - should not create a new Item but actually merge by RID, + // since an item with matching RID but empty GID exists + { + ++uidnext; + pimItem.setGid(QStringLiteral("GID-23")); + auto cmd = createCommand(pimItem, datetime, {}); + cmd->setMergeModes(Protocol::CreateItemCommand::GID); + scenarios << TestScenario::create(6, TestScenario::ClientCmd, cmd); + auto rsp = createResponse(uidnext, pimItem, datetime, {}); + scenarios << TestScenario::create(6, TestScenario::ServerCmd, rsp) + << TestScenario::create(6, TestScenario::ServerCmd, Protocol::CreateItemResponsePtr::create()); + } + notifications = {notification, Protocol::ItemChangeNotificationPtr::create(*notification)}; + QTest::newRow("merge into empty GID if RID matches") << scenarios << notifications << pimItem << parts << flags << tags << uidnext << datetime << false; + } + + void testItemCreate() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QVector, notifications); + QFETCH(PimItem, pimItem); + QFETCH(QVector, parts); + QFETCH(QVector, flags); + QFETCH(QVector, tags); + QFETCH(qint64, uidnext); + QFETCH(bool, expectFail); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + auto notificationSpy = mAkonadi.notificationSpy(); + + QCOMPARE(notificationSpy->count(), notifications.count()); + for (int i = 0; i < notifications.count(); ++i) { + const auto incomingNtfs = notificationSpy->at(i).first().value(); + QCOMPARE(incomingNtfs.count(), 1); + const auto itemNotification = incomingNtfs.at(0).staticCast(); + + QVERIFY(AkTest::compareNotifications(itemNotification, notifications.at(i), QFlag(AkTest::NtfAll & ~AkTest::NtfEntities))); + QCOMPARE(itemNotification->items().count(), notifications.at(i)->items().count()); + } + + const PimItem actualItem = PimItem::retrieveById(uidnext); + if (expectFail) { + QVERIFY(!actualItem.isValid()); + } else { + QVERIFY(actualItem.isValid()); + QCOMPARE(actualItem.remoteId(), pimItem.remoteId()); + QCOMPARE(actualItem.remoteRevision(), pimItem.remoteRevision()); + QCOMPARE(actualItem.gid(), pimItem.gid()); + QCOMPARE(actualItem.size(), pimItem.size()); + QCOMPARE(actualItem.datetime(), pimItem.datetime()); + QCOMPARE(actualItem.collectionId(), pimItem.collectionId()); + QCOMPARE(actualItem.mimeTypeId(), pimItem.mimeTypeId()); + + const auto actualFlags = actualItem.flags() | AkRanges::Actions::toQList; + QCOMPARE(actualFlags.count(), flags.count()); + Q_FOREACH (const Flag &flag, flags) { + const QList::const_iterator actualFlagIter = + std::find_if(actualFlags.constBegin(), actualFlags.constEnd(), [flag](Flag const &actualFlag) { + return flag.name() == actualFlag.name(); + }); + QVERIFY(actualFlagIter != actualFlags.constEnd()); + const Flag actualFlag = *actualFlagIter; + QVERIFY(actualFlag.isValid()); + } + + const auto actualTags = actualItem.tags() | AkRanges::Actions::toQList; + QCOMPARE(actualTags.count(), tags.count()); + Q_FOREACH (const FakeTag &tag, tags) { + const QList::const_iterator actualTagIter = std::find_if(actualTags.constBegin(), actualTags.constEnd(), [tag](Tag const &actualTag) { + return tag.gid() == actualTag.gid(); + }); + + QVERIFY(actualTagIter != actualTags.constEnd()); + const Tag actualTag = *actualTagIter; + QVERIFY(actualTag.isValid()); + QCOMPARE(actualTag.tagType().name(), tag.tagType().name()); + QCOMPARE(actualTag.gid(), tag.gid()); + if (!tag.remoteId().isEmpty()) { + SelectQueryBuilder qb; + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, QLatin1String("akonadi_fake_resource_0")); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, actualTag.id()); + QVERIFY(qb.exec()); + QCOMPARE(qb.result().size(), 1); + QCOMPARE(qb.result().at(0).remoteId(), tag.remoteId()); + } + } + + const auto actualParts = actualItem.parts() | AkRanges::Actions::toQList; + QCOMPARE(actualParts.count(), parts.count()); + Q_FOREACH (const FakePart &part, parts) { + const QList::const_iterator actualPartIter = + std::find_if(actualParts.constBegin(), actualParts.constEnd(), [part](Part const &actualPart) { + return part.partType().ns() == actualPart.partType().ns() && part.partType().name() == actualPart.partType().name(); + }); + + QVERIFY(actualPartIter != actualParts.constEnd()); + const Part actualPart = *actualPartIter; + QVERIFY(actualPart.isValid()); + QCOMPARE(QString::fromUtf8(actualPart.data()), QString::fromUtf8(part.data())); + QCOMPARE(actualPart.data(), part.data()); + QCOMPARE(actualPart.datasize(), part.datasize()); + QCOMPARE(actualPart.storage(), part.storage()); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(ItemCreateHandlerTest) + +#include "itemcreatehandlertest.moc" diff --git a/autotests/server/itemlinkhandlertest.cpp b/autotests/server/itemlinkhandlertest.cpp new file mode 100644 index 0000000..1286ef8 --- /dev/null +++ b/autotests/server/itemlinkhandlertest.cpp @@ -0,0 +1,277 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "entities.h" +#include "fakeakonadiserver.h" +#include + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class ItemLinkHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + ItemLinkHandlerTest() + { + qRegisterMetaType(); + + mAkonadi.init(); + } + + Protocol::LinkItemsResponsePtr createError(const QString &error) + { + auto resp = Protocol::LinkItemsResponsePtr::create(); + resp->setError(1, error); + return resp; + } + + Protocol::FetchItemsResponse itemResponse(qint64 id, const QString &rid, const QString &rrev, const QString &mimeType) + { + Protocol::FetchItemsResponse item; + item.setId(id); + item.setRemoteId(rid); + item.setRemoteRevision(rrev); + item.setMimeType(mimeType); + return item; + } + +private Q_SLOTS: + void testLink_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + QTest::addColumn("expectFail"); + + TestScenario::List scenarios; + + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Link, ImapInterval(1, 3), 3)) + << TestScenario::create(5, TestScenario::ServerCmd, createError(QStringLiteral("Can't link items to non-virtual collections"))); + QTest::newRow("non-virtual collection") << scenarios << Protocol::ItemChangeNotificationPtr::create() << true; + + auto notification = Protocol::ItemChangeNotificationPtr::create(); + notification->setOperation(Protocol::ItemChangeNotification::Link); + notification->setItems({itemResponse(1, QLatin1String("A"), QString(), QLatin1String("application/octet-stream")), + itemResponse(2, QLatin1String("B"), QString(), QLatin1String("application/octet-stream")), + itemResponse(3, QLatin1String("C"), QString(), QLatin1String("application/octet-stream"))}); + notification->setParentCollection(6); + notification->setResource("akonadi_fake_resource_with_virtual_collections_0"); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Link, ImapInterval(1, 3), 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponsePtr::create()); + QTest::newRow("normal") << scenarios << notification << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + notification->setItems({itemResponse(4, QLatin1String("D"), QString(), QLatin1String("application/octet-stream"))}); + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Link, QVector{4, 123456}, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponsePtr::create()); + QTest::newRow("existent and non-existent item") << scenarios << notification << false; + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Link, 4, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponsePtr::create()); + QTest::newRow("non-existent item only") << scenarios << Protocol::ItemChangeNotificationPtr::create() << false; + + // FIXME: All RID related operations are currently broken because we reset the collection context before every command, + // and LINK still relies on SELECT to set the collection context. + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << "C: 3 UID LINK 6 RID (\"F\" \"G\")\n" + // << "S: 3 OK LINK complete"; + // notification.clearEntities(); + // notification.clearEntities(); + // notification.addEntity(6, QLatin1String("F"), QString(), QLatin1String("application/octet-stream")); + // notification.addEntity(7, QLatin1String("G"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("RID items") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << "C: 4 HRID LINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) UID 5" + // << "S: 4 OK LINK complete"; + // notification.setParentCollection(7); + // notification.clearEntities(); + // notification.addEntity(5, QLatin1String("E"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << "C: 4 HRID LINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) RID \"H\"" + // << "S: 4 OK LINK complete"; + // notification.clearEntities(); + // notification.addEntity(8, QLatin1String("H"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection, RID items") << scenario << notification << false; + } + + void testLink() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ItemChangeNotificationPtr, notification); + QFETCH(bool, expectFail); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + auto notificationSpy = mAkonadi.notificationSpy(); + if (notification->operation() != Protocol::ItemChangeNotification::InvalidOp) { + QCOMPARE(notificationSpy->count(), 1); + const Protocol::ChangeNotificationList notifications = notificationSpy->takeFirst().first().value(); + QCOMPARE(notifications.count(), 1); + QCOMPARE(*notifications.first().staticCast(), *notification); + } else { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } + + Q_FOREACH (const auto &entity, notification->items()) { + if (expectFail) { + QVERIFY(!Collection::relatesToPimItem(notification->parentCollection(), entity.id())); + } else { + QVERIFY(Collection::relatesToPimItem(notification->parentCollection(), entity.id())); + } + } + } + + void testUnlink_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("notification"); + QTest::addColumn("expectFail"); + + TestScenario::List scenarios; + + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Unlink, ImapInterval(1, 3), 3)) + << TestScenario::create(5, TestScenario::ServerCmd, createError(QStringLiteral("Can't link items to non-virtual collections"))); + QTest::newRow("non-virtual collection") << scenarios << Protocol::ItemChangeNotificationPtr::create() << true; + + auto notification = Protocol::ItemChangeNotificationPtr::create(); + notification->setOperation(Protocol::ItemChangeNotification::Unlink); + notification->setItems({itemResponse(1, QLatin1String("A"), QString(), QLatin1String("application/octet-stream")), + itemResponse(2, QLatin1String("B"), QString(), QLatin1String("application/octet-stream")), + itemResponse(3, QLatin1String("C"), QString(), QLatin1String("application/octet-stream"))}); + notification->setParentCollection(6); + notification->setResource("akonadi_fake_resource_with_virtual_collections_0"); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Unlink, ImapInterval(1, 3), 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponsePtr::create()); + QTest::newRow("normal") << scenarios << notification << false; + + notification = Protocol::ItemChangeNotificationPtr::create(*notification); + notification->setItems({itemResponse(4, QLatin1String("D"), QString(), QLatin1String("application/octet-stream"))}); + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Unlink, QVector{4, 2048}, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponsePtr::create()); + QTest::newRow("existent and non-existent item") << scenarios << notification << false; + + scenarios.clear(); + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::LinkItemsCommandPtr::create(Protocol::LinkItemsCommand::Unlink, 4096, 6)) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::LinkItemsResponsePtr::create()); + QTest::newRow("non-existent item only") << scenarios << Protocol::ItemChangeNotificationPtr::create() << false; + + // FIXME: All RID related operations are currently broken because we reset the collection context before every command, + // and LINK still relies on SELECT to set the collection context. + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << "C: 4 UID UNLINK 6 RID (\"F\" \"G\")" + // << "S: 4 OK LINK complete"; + // notification.clearEntities(); + // notification.clearEntities(); + // notification.addEntity(6, QLatin1String("F"), QString(), QLatin1String("application/octet-stream")); + // notification.addEntity(7, QLatin1String("G"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("RID items") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << "C: 4 HRID UNLINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) UID 5" + // << "S: 4 OK LINK complete"; + // notification.setParentCollection(7); + // notification.clearEntities(); + // notification.addEntity(5, QLatin1String("E"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection") << scenario << notification << false; + + // scenario.clear(); + // scenario << FakeAkonadiServer::defaultScenario() + // << FakeAkonadiServer::selectCollectionScenario(QLatin1String("Collection B")) + // << FakeAkonadiServer::selectResourceScenario(QLatin1String("akonadi_fake_resource_with_virtual_collections_0")) + // << "C: 4 HRID UNLINK ((-1, \"virtual2\") (-1, \"virtual\") (-1, \"\")) RID \"H\"" + // << "S: 4 OK LINK complete"; + // notification.clearEntities(); + // notification.addEntity(8, QLatin1String("H"), QString(), QLatin1String("application/octet-stream")); + // QTest::newRow("HRID collection, RID items") << scenario << notification << false; + } + + void testUnlink() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ItemChangeNotificationPtr, notification); + QFETCH(bool, expectFail); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + auto notificationSpy = mAkonadi.notificationSpy(); + if (notification->operation() != Protocol::ItemChangeNotification::InvalidOp) { + QCOMPARE(notificationSpy->count(), 1); + const auto notifications = notificationSpy->takeFirst().first().value(); + QCOMPARE(notifications.count(), 1); + QCOMPARE(*notifications.first().staticCast(), *notification); + } else { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + } + + Q_FOREACH (const auto &entity, notification->items()) { + if (expectFail) { + QVERIFY(Collection::relatesToPimItem(notification->parentCollection(), entity.id())); + } else { + QVERIFY(!Collection::relatesToPimItem(notification->parentCollection(), entity.id())); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(ItemLinkHandlerTest) + +#include "itemlinkhandlertest.moc" diff --git a/autotests/server/itemmovehandlertest.cpp b/autotests/server/itemmovehandlertest.cpp new file mode 100644 index 0000000..4d94520 --- /dev/null +++ b/autotests/server/itemmovehandlertest.cpp @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include + +#include + +#include "aktest.h" +#include "entities.h" +#include "fakeakonadiserver.h" + +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class ItemMoveHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + ItemMoveHandlerTest() + { + mAkonadi.init(); + } + + Protocol::FetchItemsResponse fetchResponse(quint64 id, const QString &rid, const QString &rrev, const QString &mt) + { + Protocol::FetchItemsResponse item; + item.setId(id); + item.setRemoteId(rid); + item.setRemoteRevision(rrev); + item.setMimeType(mt); + return item; + } + +private Q_SLOTS: + void testMove_data() + { + const Collection srcCol = Collection::retrieveByName(QStringLiteral("Collection B")); + const Collection destCol = Collection::retrieveByName(QStringLiteral("Collection A")); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedNotifications"); + QTest::addColumn("newValue"); + + auto notificationTemplate = Protocol::ItemChangeNotificationPtr::create(); + notificationTemplate->setOperation(Protocol::ItemChangeNotification::Move); + notificationTemplate->setResource("akonadi_fake_resource_0"); + notificationTemplate->setDestinationResource("akonadi_fake_resource_0"); + notificationTemplate->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notificationTemplate->setParentCollection(srcCol.id()); + notificationTemplate->setParentDestCollection(destCol.id()); + + { + auto cmd = Protocol::MoveItemsCommandPtr::create(1, destCol.id()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::MoveItemsResponsePtr::create()); + + auto notification = Protocol::ItemChangeNotificationPtr::create(*notificationTemplate); + notification->setItems({fetchResponse(1, QStringLiteral("A"), QString(), QStringLiteral("application/octet-stream"))}); + + QTest::newRow("move item") << scenarios << Protocol::ChangeNotificationList{notification} << QVariant::fromValue(destCol.id()); + } + + { + auto cmd = Protocol::MoveItemsCommandPtr::create(QVector{2, 3}, destCol.id()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::MoveItemsResponsePtr::create()); + + auto notification = Protocol::ItemChangeNotificationPtr::create(*notificationTemplate); + notification->setItems({fetchResponse(3, QStringLiteral("C"), QString(), QStringLiteral("application/octet-stream")), + fetchResponse(2, QStringLiteral("B"), QString(), QStringLiteral("application/octet-stream"))}); + + QTest::newRow("move items") << scenarios << Protocol::ChangeNotificationList{notification} << QVariant::fromValue(destCol.id()); + } + } + + void testMove() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + QFETCH(QVariant, newValue); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + auto notificationSpy = mAkonadi.notificationSpy(); + if (expectedNotifications.isEmpty()) { + QVERIFY(notificationSpy->isEmpty() || notificationSpy->takeFirst().first().value().isEmpty()); + return; + } + QCOMPARE(notificationSpy->count(), 1); + // Only one notify call + QCOMPARE(notificationSpy->first().count(), 1); + const auto receivedNotifications = notificationSpy->first().first().value(); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(*receivedNotifications.at(i).staticCast(), + *expectedNotifications.at(i).staticCast()); + const auto notification = receivedNotifications.at(i).staticCast(); + QCOMPARE(notification->parentDestCollection(), newValue.toInt()); + + Q_FOREACH (const auto &ntfItem, notification->items()) { + const PimItem item = PimItem::retrieveById(ntfItem.id()); + QCOMPARE(item.collectionId(), newValue.toInt()); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(ItemMoveHandlerTest) + +#include "itemmovehandlertest.moc" diff --git a/autotests/server/itemretrievertest.cpp b/autotests/server/itemretrievertest.cpp new file mode 100644 index 0000000..800dad1 --- /dev/null +++ b/autotests/server/itemretrievertest.cpp @@ -0,0 +1,348 @@ +/* + SPDX-FileCopyrightText: 2013 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include "commandcontext.h" +#include "storage/datastore.h" +#include "storage/itemretrievaljob.h" +#include "storage/itemretrievalmanager.h" +#include "storage/itemretrievalrequest.h" +#include "storage/itemretriever.h" + +#include "dbinitializer.h" +#include "fakeakonadiserver.h" + +#include + +using namespace Akonadi::Server; + +struct JobResult { + qint64 pimItemId; + QByteArray partname; + QByteArray partdata; + QString error; +}; + +class FakeItemRetrievalJob : public AbstractItemRetrievalJob +{ + Q_OBJECT +public: + FakeItemRetrievalJob(ItemRetrievalRequest req, DbInitializer &dbInitializer, const QVector &results, QObject *parent) + : AbstractItemRetrievalJob(std::move(req), parent) + , mDbInitializer(dbInitializer) + , mResults(results) + { + } + + void start() override + { + Q_FOREACH (const JobResult &res, mResults) { + if (res.error.isEmpty()) { + // This is analogous to what STORE/MERGE does + const PimItem item = PimItem::retrieveById(res.pimItemId); + const auto parts = item.parts(); + // Try to find the part by name + auto it = std::find_if(parts.begin(), parts.end(), [res](const Part &part) { + return part.partType().name().toLatin1() == res.partname; + }); + if (it == parts.end()) { + // Does not exist, create it + mDbInitializer.createPart(res.pimItemId, "PLD:" + res.partname, res.partdata); + } else { + // Exist, update it + Part part(*it); + part.setData(res.partdata); + part.setDatasize(res.partdata.size()); + part.update(); + } + } else { + m_result.errorMsg = res.error; + break; + } + } + + QTimer::singleShot(0, this, [this]() { + Q_EMIT requestCompleted(this); + }); + } + + void kill() override + { + // TODO + Q_ASSERT(false); + } + +private: + DbInitializer &mDbInitializer; + QVector mResults; +}; + +class FakeItemRetrievalJobFactory : public AbstractItemRetrievalJobFactory +{ +public: + explicit FakeItemRetrievalJobFactory(DbInitializer &initializer) + : mJobsCount(0) + , mDbInitializer(initializer) + { + } + + void addJobResult(qint64 itemId, const QByteArray &partname, const QByteArray &partdata) + { + mJobResults.insert(itemId, JobResult{itemId, partname, partdata, QString()}); + } + + void addJobResult(qint64 itemId, const QString &error) + { + mJobResults.insert(itemId, JobResult{itemId, QByteArray(), QByteArray(), error}); + } + + AbstractItemRetrievalJob *retrievalJob(ItemRetrievalRequest request, QObject *parent) override + { + QVector results; + Q_FOREACH (auto id, request.ids) { + auto it = mJobResults.constFind(id); + while (it != mJobResults.constEnd() && it.key() == id) { + if (request.parts.contains(it->partname)) { + results << *it; + } + ++it; + } + } + + ++mJobsCount; + return new FakeItemRetrievalJob(std::move(request), mDbInitializer, results, parent); + } + + int jobsCount() const + { + return mJobsCount; + } + +private: + int mJobsCount; + DbInitializer &mDbInitializer; + QMultiHash mJobResults; +}; + +using RequestedParts = QVector; + +class ClientThread : public QThread +{ +public: + ClientThread(Entity::Id itemId, const RequestedParts &requestedParts, ItemRetrievalManager &manager) + : m_itemId(itemId) + , m_requestedParts(requestedParts) + , m_manager(manager) + { + } + + void run() override + { + // ItemRetriever should... + CommandContext context; + ItemRetriever retriever(m_manager, nullptr, context); + retriever.setItem(m_itemId); + retriever.setRetrieveParts(m_requestedParts); + QSignalSpy spy(&retriever, &ItemRetriever::itemsRetrieved); + + const bool success = retriever.exec(); + + QMutexLocker lock(&m_mutex); + m_results.success = success; + m_results.signalsCount = spy.count(); + if (m_results.signalsCount > 0) { + m_results.emittedItems = spy.at(0).at(0).value>(); + } + + DataStore::self()->close(); + } + + struct Results { + bool success; + int signalsCount; + QVector emittedItems; + }; + Results results() const + { + QMutexLocker lock(&m_mutex); + return m_results; + } + +private: + const Entity::Id m_itemId; + const RequestedParts m_requestedParts; + ItemRetrievalManager &m_manager; + + mutable QMutex m_mutex; // protects results below + Results m_results; +}; + +class ItemRetrieverTest : public QObject +{ + Q_OBJECT + + using ExistingParts = QVector>; + using AvailableParts = QVector>; + + FakeAkonadiServer mAkonadi; + +public: + ItemRetrieverTest() + { + mAkonadi.setPopulateDb(false); + mAkonadi.disableItemRetrievalManager(); + mAkonadi.init(); + } + +private Q_SLOTS: + void testFullPayload() + { + CommandContext context; + ItemRetriever r1(mAkonadi.itemRetrievalManager(), nullptr, context); + r1.setRetrieveFullPayload(true); + QCOMPARE(r1.retrieveParts().size(), 1); + QCOMPARE(r1.retrieveParts().at(0), {"PLD:RFC822"}); + r1.setRetrieveParts({"PLD:FOO"}); + QCOMPARE(r1.retrieveParts().size(), 2); + } + + void testRetrieval_data() + { + QTest::addColumn("existingParts"); + QTest::addColumn("availableParts"); + QTest::addColumn("requestedParts"); + QTest::addColumn("expectedRetrievalJobs"); + QTest::addColumn("expectedSignals"); + QTest::addColumn("expectedParts"); + + QTest::newRow("should retrieve missing payload part") + << ExistingParts() << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:RFC822"} << 1 << 1 << 1; + + QTest::newRow("should retrieve multiple missing payload parts") + << ExistingParts() << AvailableParts{{"RFC822", "somedata"}, {"HEAD", "head"}} << RequestedParts{"PLD:HEAD", "PLD:RFC822"} << 1 << 1 << 2; + + QTest::newRow("should not retrieve existing payload part") + << ExistingParts{{"PLD:RFC822", "somedata"}} << AvailableParts() << RequestedParts{"PLD:RFC822"} << 0 << 1 << 1; + + QTest::newRow("should not retrieve multiple existing payload parts") + << ExistingParts{{"PLD:RFC822", "somedata"}, {"PLD:HEAD", "head"}} << AvailableParts() << RequestedParts{"PLD:RFC822", "PLD:HEAD"} << 0 << 1 << 2; + + QTest::newRow("should retrieve missing but not existing payload part") + << ExistingParts{{"PLD:HEAD", "head"}} << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:HEAD", "PLD:RFC822"} << 1 << 1 << 2; + + QTest::newRow("should retrieve expired payload part") + << ExistingParts{{"PLD:RFC822", QByteArray()}} << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:RFc822"} << 1 << 1 << 1; + + QTest::newRow("should not retrieve one out of multiple existing payload parts") + << ExistingParts{{"PLD:RFC822", "somedata"}, {"PLD:HEAD", "head"}, {"PLD:ENVELOPE", "envelope"}} << AvailableParts() + << RequestedParts{"PLD:RFC822", "PLD:HEAD"} << 0 << 1 << 3; + + QTest::newRow("should retrieve missing payload part and ignore attributes") + << ExistingParts{{"ATR:MYATTR", "myattrdata"}} << AvailableParts{{"RFC822", "somedata"}} << RequestedParts{"PLD:RFC822"} << 1 << 1 << 2; + } + + void testRetrieval() + { + QFETCH(ExistingParts, existingParts); + QFETCH(AvailableParts, availableParts); + QFETCH(RequestedParts, requestedParts); + QFETCH(int, expectedRetrievalJobs); + QFETCH(int, expectedSignals); + QFETCH(int, expectedParts); + + // Setup + for (int step = 0; step < 2; ++step) { + DbInitializer dbInitializer; + auto factory = new FakeItemRetrievalJobFactory(dbInitializer); + ItemRetrievalManager mgr{std::unique_ptr(factory)}; + QTest::qWait(100); + + // Given a PimItem with existing parts + Resource res = dbInitializer.createResource("testresource"); + Collection col = dbInitializer.createCollection("col1"); + + // step 0: do it in the main thread, for easier debugging + PimItem item = dbInitializer.createItem("1", col); + Q_FOREACH (const auto &existingPart, existingParts) { + dbInitializer.createPart(item.id(), existingPart.first, existingPart.second); + } + + Q_FOREACH (const auto &availablePart, availableParts) { + factory->addJobResult(item.id(), availablePart.first, availablePart.second); + } + + if (step == 0) { + ClientThread thread(item.id(), requestedParts, mgr); + thread.run(); + + const ClientThread::Results results = thread.results(); + // ItemRetriever should ... succeed + QVERIFY(results.success); + // Emit exactly one signal ... + QCOMPARE(results.signalsCount, expectedSignals); + // ... with that one item + if (expectedSignals > 0) { + QCOMPARE(results.emittedItems, QVector{item.id()}); + } + + // Check that the factory had exactly one retrieval job + QCOMPARE(factory->jobsCount(), expectedRetrievalJobs); + + } else { + QVector threads; + for (int i = 0; i < 20; ++i) { + threads.append(new ClientThread(item.id(), requestedParts, mgr)); + } + for (int i = 0; i < threads.size(); ++i) { + threads.at(i)->start(); + } + for (int i = 0; i < threads.size(); ++i) { + threads.at(i)->wait(); + } + for (int i = 0; i < threads.size(); ++i) { + const ClientThread::Results results = threads.at(i)->results(); + QVERIFY(results.success); + QCOMPARE(results.signalsCount, expectedSignals); + if (expectedSignals > 0) { + QCOMPARE(results.emittedItems, QVector{item.id()}); + } + } + qDeleteAll(threads); + } + + // Check that the parts now exist in the DB + const auto parts = item.parts(); + QCOMPARE(parts.count(), expectedParts); + Q_FOREACH (const Part &dbPart, item.parts()) { + const QString fqname = dbPart.partType().ns() + QLatin1Char(':') + dbPart.partType().name(); + if (!requestedParts.contains(fqname.toLatin1())) { + continue; + } + + auto it = std::find_if(availableParts.constBegin(), availableParts.constEnd(), [dbPart](const QPair &p) { + return dbPart.partType().name().toLatin1() == p.first; + }); + if (it == availableParts.constEnd()) { + it = std::find_if(existingParts.constBegin(), existingParts.constEnd(), [fqname](const QPair &p) { + return fqname.toLatin1() == p.first; + }); + QVERIFY(it != existingParts.constEnd()); + } + + QCOMPARE(dbPart.data(), it->second); + QCOMPARE(dbPart.datasize(), it->second.size()); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(ItemRetrieverTest) + +#include "itemretrievertest.moc" diff --git a/autotests/server/mockobjects.h b/autotests/server/mockobjects.h new file mode 100644 index 0000000..e5a808c --- /dev/null +++ b/autotests/server/mockobjects.h @@ -0,0 +1,46 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "akonadiconnection.h" +#include "teststoragebackend.h" + +using namespace Akonadi; + +static AkonadiConnection *s_connection = nullptr; +static DataStore *s_backend = nullptr; + +class MockConnection : public AkonadiConnection +{ +public: + MockConnection() + { + } + DataStore *storageBackend() + { + if (!s_backend) { + s_backend = new MockBackend(); + } + return s_backend; + } +}; + +class MockObjects +{ +public: + MockObjects(); + ~MockObjects(); + + static AkonadiConnection *mockConnection() + { + if (!s_connection) { + s_connection = new MockConnection(); + } + return s_connection; + } +}; // End of class MockObjects + diff --git a/autotests/server/notificationmanagertest.cpp b/autotests/server/notificationmanagertest.cpp new file mode 100644 index 0000000..c244793 --- /dev/null +++ b/autotests/server/notificationmanagertest.cpp @@ -0,0 +1,152 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "aggregatedfetchscope.h" +#include "entities.h" +#include "notificationmanager.h" +#include "notificationsubscriber.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +class TestableNotificationSubscriber : public NotificationSubscriber +{ +public: + explicit TestableNotificationSubscriber(NotificationManager *manager) + : NotificationSubscriber(manager) + { + mSubscriber = "TestSubscriber"; + } + + using NotificationSubscriber::disconnectSubscriber; + using NotificationSubscriber::modifySubscription; + using NotificationSubscriber::registerSubscriber; +}; + +class NotificationManagerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testAggregatedFetchScope() + { + NotificationManager manager(AkThread::NoThread); + QMetaObject::invokeMethod(&manager, "init", Qt::DirectConnection); + + // first subscriber, A + TestableNotificationSubscriber subscriberA(&manager); + Protocol::CreateSubscriptionCommand createCmd; + createCmd.setSession("session1"); + subscriberA.registerSubscriber(createCmd); + QVERIFY(!manager.tagFetchScope()->fetchIdOnly()); + QVERIFY(manager.tagFetchScope()->fetchAllAttributes()); // default is true + QVERIFY(!manager.collectionFetchScope()->fetchIdOnly()); + QVERIFY(!manager.collectionFetchScope()->fetchStatistics()); + + // set A's subscription settings + Protocol::ModifySubscriptionCommand modifyCmd; + { + Protocol::TagFetchScope tagFetchScope; + tagFetchScope.setFetchIdOnly(true); + tagFetchScope.setFetchAllAttributes(false); + modifyCmd.setTagFetchScope(tagFetchScope); + + Protocol::CollectionFetchScope collectionFetchScope; + collectionFetchScope.setFetchIdOnly(true); + collectionFetchScope.setIncludeStatistics(true); + modifyCmd.setCollectionFetchScope(collectionFetchScope); + + Protocol::ItemFetchScope itemFetchScope; + itemFetchScope.setFetch(Protocol::ItemFetchScope::FullPayload); + itemFetchScope.setFetch(Protocol::ItemFetchScope::AllAttributes); + itemFetchScope.setFetch(Protocol::ItemFetchScope::Size); + itemFetchScope.setFetch(Protocol::ItemFetchScope::MTime); + itemFetchScope.setFetch(Protocol::ItemFetchScope::RemoteRevision); + itemFetchScope.setFetch(Protocol::ItemFetchScope::Flags); + itemFetchScope.setFetch(Protocol::ItemFetchScope::RemoteID); + itemFetchScope.setFetch(Protocol::ItemFetchScope::GID); + itemFetchScope.setFetch(Protocol::ItemFetchScope::Tags); + itemFetchScope.setFetch(Protocol::ItemFetchScope::Relations); + itemFetchScope.setFetch(Protocol::ItemFetchScope::VirtReferences); + modifyCmd.setItemFetchScope(itemFetchScope); + } + subscriberA.modifySubscription(modifyCmd); + QVERIFY(manager.tagFetchScope()->fetchIdOnly()); + QVERIFY(!manager.tagFetchScope()->fetchAllAttributes()); + QVERIFY(manager.collectionFetchScope()->fetchIdOnly()); + QVERIFY(manager.collectionFetchScope()->fetchStatistics()); + QVERIFY(manager.itemFetchScope()->fullPayload()); + QVERIFY(manager.itemFetchScope()->allAttributes()); + + // second subscriber, B + TestableNotificationSubscriber subscriberB(&manager); + subscriberB.registerSubscriber(createCmd); + QVERIFY(!manager.tagFetchScope()->fetchIdOnly()); // A and B don't agree, so: false + QVERIFY(manager.tagFetchScope()->fetchAllAttributes()); + QVERIFY(!manager.collectionFetchScope()->fetchIdOnly()); + QVERIFY(manager.collectionFetchScope()->fetchStatistics()); // at least one - so still true + QVERIFY(manager.itemFetchScope()->fullPayload()); + QVERIFY(manager.itemFetchScope()->allAttributes()); + QVERIFY(manager.itemFetchScope()->fetchSize()); + QVERIFY(manager.itemFetchScope()->fetchMTime()); + QVERIFY(manager.itemFetchScope()->fetchRemoteRevision()); + QVERIFY(manager.itemFetchScope()->fetchFlags()); + QVERIFY(manager.itemFetchScope()->fetchRemoteId()); + QVERIFY(manager.itemFetchScope()->fetchGID()); + QVERIFY(manager.itemFetchScope()->fetchTags()); + QVERIFY(manager.itemFetchScope()->fetchRelations()); + QVERIFY(manager.itemFetchScope()->fetchVirtualReferences()); + + // give it the same settings + subscriberB.modifySubscription(modifyCmd); + QVERIFY(manager.tagFetchScope()->fetchIdOnly()); // now they agree + QVERIFY(!manager.tagFetchScope()->fetchAllAttributes()); + QVERIFY(manager.collectionFetchScope()->fetchIdOnly()); + QVERIFY(manager.collectionFetchScope()->fetchStatistics()); // no change for the "at least one" settings + + // revert B's settings, so we can check what happens when disconnecting + modifyCmd.setTagFetchScope(Protocol::TagFetchScope()); + modifyCmd.setCollectionFetchScope(Protocol::CollectionFetchScope()); + subscriberB.modifySubscription(modifyCmd); + QVERIFY(!manager.tagFetchScope()->fetchIdOnly()); + QVERIFY(manager.tagFetchScope()->fetchAllAttributes()); + QVERIFY(!manager.collectionFetchScope()->fetchIdOnly()); + QVERIFY(manager.collectionFetchScope()->fetchStatistics()); + + // B goes away + subscriberB.disconnectSubscriber(); + QVERIFY(manager.tagFetchScope()->fetchIdOnly()); // B cleaned up after itself, so A can have id-only again + QVERIFY(!manager.tagFetchScope()->fetchAllAttributes()); + QVERIFY(manager.collectionFetchScope()->fetchIdOnly()); + QVERIFY(manager.collectionFetchScope()->fetchStatistics()); + + // A goes away + subscriberA.disconnectSubscriber(); + QVERIFY(!manager.collectionFetchScope()->fetchStatistics()); + QVERIFY(!manager.itemFetchScope()->fullPayload()); + QVERIFY(!manager.itemFetchScope()->allAttributes()); + QVERIFY(!manager.itemFetchScope()->fetchSize()); + QVERIFY(!manager.itemFetchScope()->fetchMTime()); + QVERIFY(!manager.itemFetchScope()->fetchRemoteRevision()); + QVERIFY(!manager.itemFetchScope()->fetchFlags()); + QVERIFY(!manager.itemFetchScope()->fetchRemoteId()); + QVERIFY(!manager.itemFetchScope()->fetchGID()); + QVERIFY(!manager.itemFetchScope()->fetchTags()); + QVERIFY(!manager.itemFetchScope()->fetchRelations()); + QVERIFY(!manager.itemFetchScope()->fetchVirtualReferences()); + + QMetaObject::invokeMethod(&manager, "quit", Qt::DirectConnection); + } +}; + +AKTEST_MAIN(NotificationManagerTest) + +#include "notificationmanagertest.moc" diff --git a/autotests/server/notificationsubscribertest.cpp b/autotests/server/notificationsubscribertest.cpp new file mode 100644 index 0000000..bca2bff --- /dev/null +++ b/autotests/server/notificationsubscribertest.cpp @@ -0,0 +1,293 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "entities.h" +#include "notificationsubscriber.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QVector) + +class TestableNotificationSubscriber : public NotificationSubscriber +{ +public: + TestableNotificationSubscriber() + { + mSubscriber = "TestSubscriber"; + } + + void setAllMonitored(bool allMonitored) + { + mAllMonitored = allMonitored; + } + + void setMonitoredCollection(qint64 collection, bool monitored) + { + if (monitored) { + mMonitoredCollections.insert(collection); + } else { + mMonitoredCollections.remove(collection); + } + } + + void setMonitoredItem(qint64 item, bool monitored) + { + if (monitored) { + mMonitoredItems.insert(item); + } else { + mMonitoredItems.remove(item); + } + } + + void setMonitoredResource(const QByteArray &resource, bool monitored) + { + if (monitored) { + mMonitoredResources.insert(resource); + } else { + mMonitoredResources.remove(resource); + } + } + + void setMonitoredMimeType(const QString &mimeType, bool monitored) + { + if (monitored) { + mMonitoredMimeTypes.insert(mimeType); + } else { + mMonitoredMimeTypes.remove(mimeType); + } + } + + void setIgnoredSession(const QByteArray &session, bool ignored) + { + if (ignored) { + mIgnoredSessions.insert(session); + } else { + mIgnoredSessions.remove(session); + } + } + + void writeNotification(const Protocol::ChangeNotificationPtr ¬ification) override + { + emittedNotifications << notification; + } + + Protocol::ChangeNotificationList emittedNotifications; +}; + +class NotificationSubscriberTest : public QObject +{ + Q_OBJECT + + using NSList = QList; + + Protocol::FetchItemsResponse itemResponse(qint64 id, const QString &rid, const QString &rrev, const QString &mt) + { + Protocol::FetchItemsResponse item; + item.setId(id); + item.setRemoteId(rid); + item.setRemoteRevision(rrev); + item.setMimeType(mt); + return item; + } + +private Q_SLOTS: + void testSourceFilter_data() + { + qRegisterMetaType(); + + QTest::addColumn("allMonitored"); + QTest::addColumn>("monitoredCollections"); + QTest::addColumn>("monitoredItems"); + QTest::addColumn>("monitoredResources"); + QTest::addColumn>("monitoredMimeTypes"); + QTest::addColumn>("ignoredSessions"); + QTest::addColumn("notification"); + QTest::addColumn("accepted"); + +#define EmptyList(T) (QVector()) +#define List(T, x) (QVector() << (x)) + + auto itemMsg = Protocol::ItemChangeNotificationPtr::create(); + itemMsg->setOperation(Protocol::ItemChangeNotification::Add); + itemMsg->setParentCollection(1); + QTest::newRow("monitorAll vs notification without items") + << true << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << EmptyList(QString) << EmptyList(QByteArray) + << itemMsg.staticCast() << false; + + itemMsg = Protocol::ItemChangeNotificationPtr::create(*itemMsg); + itemMsg->setItems({itemResponse(1, QString(), QString(), QStringLiteral("message/rfc822"))}); + QTest::newRow("monitorAll vs notification with one item") + << true << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << EmptyList(QString) << EmptyList(QByteArray) + << itemMsg.staticCast() << true; + + QTest::newRow("item monitored but different mimetype") + << false << EmptyList(Entity::Id) << List(Entity::Id, 1 << 2) << EmptyList(QByteArray) << List(QString, QStringLiteral("random/mimetype")) + << EmptyList(QByteArray) << Protocol::ItemChangeNotificationPtr::create(*itemMsg).staticCast() << false; + + QTest::newRow("item not monitored, but mimetype matches") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << List(QString, QStringLiteral("message/rfc822")) + << EmptyList(QByteArray) << Protocol::ItemChangeNotificationPtr::create(*itemMsg).staticCast() << true; + + itemMsg = Protocol::ItemChangeNotificationPtr::create(*itemMsg); + itemMsg->setSessionId("testSession"); + QTest::newRow("item monitored but session ignored") + << false << EmptyList(Entity::Id) << List(Entity::Id, 1) << EmptyList(QByteArray) << EmptyList(QString) << List(QByteArray, "testSession") + << itemMsg.staticCast() << false; + + // Simulate adding a new resource + auto colMsg = Protocol::CollectionChangeNotificationPtr::create(); + colMsg->setOperation(Protocol::CollectionChangeNotification::Add); + Protocol::FetchCollectionsResponse col; + col.setId(1); + col.setRemoteId(QStringLiteral("imap://user@some.domain/")); + colMsg->setCollection(std::move(col)); + colMsg->setParentCollection(0); + colMsg->setSessionId("akonadi_imap_resource_0"); + colMsg->setResource("akonadi_imap_resource_0"); + QTest::newRow("new root collection in non-monitored resource") + << false << List(Entity::Id, 0) << EmptyList(Entity::Id) << List(QByteArray, "akonadi_search_resource") + << List(QString, QStringLiteral("message/rfc822")) << EmptyList(QByteArray) << colMsg.staticCast() << true; + + itemMsg = Protocol::ItemChangeNotificationPtr::create(); + itemMsg->setOperation(Protocol::ItemChangeNotification::Move); + itemMsg->setResource("akonadi_resource_1"); + itemMsg->setDestinationResource("akonadi_resource_2"); + itemMsg->setParentCollection(1); + itemMsg->setParentDestCollection(2); + itemMsg->setSessionId("kmail"); + itemMsg->setItems({itemResponse(10, QStringLiteral("123"), QStringLiteral("1"), QStringLiteral("message/rfc822"))}); + QTest::newRow("inter-resource move, source source") << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << List(QByteArray, "akonadi_resource_1") + << List(QString, QStringLiteral("message/rfc822")) << List(QByteArray, "akonadi_resource_1") + << itemMsg.staticCast() << true; + + QTest::newRow("inter-resource move, destination source") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << List(QByteArray, "akonadi_resource_2") + << List(QString, QStringLiteral("message/rfc822")) << List(QByteArray, "akonadi_resource_2") << itemMsg.staticCast() + << true; + + QTest::newRow("inter-resource move, uninterested party") + << false << List(Entity::Id, 12) << EmptyList(Entity::Id) << EmptyList(QByteArray) << List(QString, QStringLiteral("inode/directory")) + << EmptyList(QByteArray) << itemMsg.staticCast() << false; + + itemMsg = Protocol::ItemChangeNotificationPtr::create(); + itemMsg->setOperation(Protocol::ItemChangeNotification::Move); + itemMsg->setResource("akonadi_resource_0"); + itemMsg->setDestinationResource("akonadi_resource_0"); + itemMsg->setParentCollection(1); + itemMsg->setParentDestCollection(2); + itemMsg->setSessionId("kmail"); + itemMsg->setItems({itemResponse(10, QStringLiteral("123"), QStringLiteral("1"), QStringLiteral("message/rfc822")), + itemResponse(11, QStringLiteral("456"), QStringLiteral("1"), QStringLiteral("message/rfc822"))}); + QTest::newRow("intra-resource move, owning resource") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << List(QByteArray, "akonadi_imap_resource_0") + << List(QString, QStringLiteral("message/rfc822")) << List(QByteArray, "akonadi_imap_resource_0") + << itemMsg.staticCast() << true; + + colMsg = Protocol::CollectionChangeNotificationPtr::create(); + colMsg->setOperation(Protocol::CollectionChangeNotification::Add); + colMsg->setSessionId("kmail"); + colMsg->setResource("akonadi_resource_1"); + colMsg->setParentCollection(1); + QTest::newRow("new subfolder") << false << List(Entity::Id, 0) << EmptyList(Entity::Id) << EmptyList(QByteArray) + << List(QString, QStringLiteral("message/rfc822")) << EmptyList(QByteArray) + << colMsg.staticCast() << false; + + itemMsg = Protocol::ItemChangeNotificationPtr::create(); + itemMsg->setOperation(Protocol::ItemChangeNotification::Add); + itemMsg->setSessionId("randomSession"); + itemMsg->setResource("randomResource"); + itemMsg->setParentCollection(1); + itemMsg->setItems({itemResponse(10, QString(), QString(), QStringLiteral("message/rfc822"))}); + QTest::newRow("new mail for mailfilter or maildispatcher") + << false << List(Entity::Id, 0) << EmptyList(Entity::Id) << EmptyList(QByteArray) << List(QString, QStringLiteral("message/rfc822")) + << EmptyList(QByteArray) << itemMsg.staticCast() << true; + + auto tagMsg = Protocol::TagChangeNotificationPtr::create(); + tagMsg->setOperation(Protocol::TagChangeNotification::Remove); + tagMsg->setSessionId("randomSession"); + tagMsg->setResource("akonadi_random_resource_0"); + { + Protocol::FetchTagsResponse tagMsgTag; + tagMsgTag.setId(1); + tagMsgTag.setRemoteId("TAG"); + tagMsg->setTag(std::move(tagMsgTag)); + } + QTest::newRow("Tag removal - resource notification - matching resource source") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << EmptyList(QString) + << List(QByteArray, "akonadi_random_resource_0") << tagMsg.staticCast() << true; + + QTest::newRow("Tag removal - resource notification - wrong resource source") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << EmptyList(QString) + << List(QByteArray, "akonadi_another_resource_1") << tagMsg.staticCast() << false; + + tagMsg = Protocol::TagChangeNotificationPtr::create(); + tagMsg->setOperation(Protocol::TagChangeNotification::Remove); + tagMsg->setSessionId("randomSession"); + { + Protocol::FetchTagsResponse tagMsgTag; + tagMsgTag.setId(1); + tagMsgTag.setRemoteId("TAG"); + tagMsg->setTag(std::move(tagMsgTag)); + } + QTest::newRow("Tag removal - client notification - client source") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << EmptyList(QString) << EmptyList(QByteArray) + << tagMsg.staticCast() << true; + + QTest::newRow("Tag removal - client notification - resource source") + << false << EmptyList(Entity::Id) << EmptyList(Entity::Id) << EmptyList(QByteArray) << EmptyList(QString) + << List(QByteArray, "akonadi_some_resource_0") << tagMsg.staticCast() << false; + } + + void testSourceFilter() + { + QFETCH(bool, allMonitored); + QFETCH(QVector, monitoredCollections); + QFETCH(QVector, monitoredItems); + QFETCH(QVector, monitoredResources); + QFETCH(QVector, monitoredMimeTypes); + QFETCH(QVector, ignoredSessions); + QFETCH(Protocol::ChangeNotificationPtr, notification); + QFETCH(bool, accepted); + + TestableNotificationSubscriber subscriber; + + subscriber.setAllMonitored(allMonitored); + for (Entity::Id id : monitoredCollections) { + subscriber.setMonitoredCollection(id, true); + } + for (Entity::Id id : monitoredItems) { + subscriber.setMonitoredItem(id, true); + } + for (const QByteArray &res : monitoredResources) { + subscriber.setMonitoredResource(res, true); + } + for (const QString &mimeType : monitoredMimeTypes) { + subscriber.setMonitoredMimeType(mimeType, true); + } + for (const QByteArray &session : ignoredSessions) { + subscriber.setIgnoredSession(session, true); + } + + subscriber.notify({notification}); + + QTRY_COMPARE(subscriber.emittedNotifications.count(), accepted ? 1 : 0); + + if (accepted) { + const Protocol::ChangeNotificationPtr ntf = subscriber.emittedNotifications.at(0); + QVERIFY(ntf->isValid()); + } + } +}; + +AKTEST_MAIN(NotificationSubscriberTest) + +#include "notificationsubscribertest.moc" diff --git a/autotests/server/parthelpertest.cpp b/autotests/server/parthelpertest.cpp new file mode 100644 index 0000000..4eb9005 --- /dev/null +++ b/autotests/server/parthelpertest.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "storage/parthelper.h" +#include "entities.h" +#include + +#include +#include +#include + +#define QL1S(x) QString::fromLatin1(x) + +using namespace Akonadi::Server; + +class PartHelperTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: +#if 0 + void testFileName() + { + akTestSetInstanceIdentifier(QString()); + + Part p; + p.setId(42); + + QString fileName = PartHelper::fileNameForPart(&p); + QVERIFY(fileName.endsWith(QL1S("42"))); + } +#endif + + void testRemoveFile_data() + { + QTest::addColumn("instance"); + QTest::newRow("main") << QString(); + QTest::newRow("multi-instance") << QL1S("foo"); + } + +#if 0 + void testRemoveFile() + { + QFETCH(QString, instance); + akTestSetInstanceIdentifier(instance); + + Part p; + p.setId(23); + const QString validFileName = PartHelper::storagePath() + QDir::separator() + PartHelper::fileNameForPart(&p); + PartHelper::removeFile(validFileName); // no throw + } +#endif + +#if 0 + void testInvalidRemoveFile_data() + { + QTest::addColumn("fileName"); + QTest::newRow("empty") << QString(); + QTest::newRow("relative") << QL1S("foo"); + QTest::newRow("absolute") << QL1S("/foo"); + + akTestSetInstanceIdentifier(QL1S("foo")); + Part p; + p.setId(23); + QTest::newRow("wrong instance") << PartHelper::fileNameForPart(&p); + } +#endif + +#if 0 + void testInvalidRemoveFile() + { + QFETCH(QString, fileName); + akTestSetInstanceIdentifier(QString()); + try { + PartHelper::removeFile(fileName); + } catch (const PartHelperException &e) { + return; // all good + } + QVERIFY(false); // didn't throw + } +#endif + +#if 0 + void testStorageLocation() + { + akTestSetInstanceIdentifier(QString()); + const QString mainLocation = PartHelper::storagePath(); + QVERIFY(mainLocation.endsWith(QDir::separator())); + QVERIFY(mainLocation.startsWith(QDir::separator())); + + akTestSetInstanceIdentifier(QL1S("foo")); + QVERIFY(PartHelper::storagePath().endsWith(QDir::separator())); + QVERIFY(PartHelper::storagePath().startsWith(QDir::separator())); + QVERIFY(mainLocation != PartHelper::storagePath()); + } +#endif + +#if 0 + void testResolveAbsolutePath() + { +#ifndef Q_OS_WIN + QVERIFY(PartHelper::resolveAbsolutePath("foo").startsWith(QLatin1Char('/'))); + QCOMPARE(PartHelper::resolveAbsolutePath("/foo"), QString::fromLatin1("/foo")); + QVERIFY(!PartHelper::resolveAbsolutePath("foo").contains(QL1S("//"))); // no double separator +#endif + } +#endif +}; + +AKTEST_MAIN(PartHelperTest) + +#include "parthelpertest.moc" diff --git a/autotests/server/partstreamertest.cpp b/autotests/server/partstreamertest.cpp new file mode 100644 index 0000000..c507876 --- /dev/null +++ b/autotests/server/partstreamertest.cpp @@ -0,0 +1,308 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include + +#include "aktest.h" +#include "entities.h" +#include "fakeakonadiserver.h" +#include "fakeconnection.h" + +#include +#include + +#include "storage/partstreamer.h" +#include + +#include +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::PimItem) +Q_DECLARE_METATYPE(Akonadi::Server::Part::Storage) + +class PartStreamerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + PartStreamerTest() + { + // Set a very small treshold for easier testing + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + settings.setValue(QStringLiteral("General/SizeThreshold"), 5); + + mAkonadi.init(); + } + + Protocol::ModifyItemsCommandPtr createCommand(const PimItem &item) + { + auto cmd = Protocol::ModifyItemsCommandPtr::create(item.id()); + cmd->setParts({"PLD:DATA"}); + return cmd; + } + +private Q_SLOTS: + void testStreamer_data() + { + QTest::addColumn("scenarios"); + QTest::addColumn("expectedPartName"); + QTest::addColumn("expectedPartData"); + QTest::addColumn("expectedFileData"); + QTest::addColumn("expectedPartSize"); + QTest::addColumn("expectedChanged"); + QTest::addColumn("storage"); + QTest::addColumn("pimItem"); + + PimItem item; + item.setCollectionId(Collection::retrieveByName(QStringLiteral("Col A")).id()); + item.setMimeType(MimeType::retrieveByName(QStringLiteral("application/octet-stream"))); + item.setSize(1); // this will not match reality during the test, but that does not matter, as + // that's not the subject of this test + QVERIFY(item.insert()); + + qint64 partId = -1; + Part::List parts = Part::retrieveAll(); + if (parts.isEmpty()) { + partId = 0; + } else { + partId = parts.last().id() + 1; + } + + // Order of these tests matters! + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 3))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "123")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item.id(), 1)); + + QTest::newRow("item 1, internal") << scenarios << QByteArray("PLD:DATA") << QByteArray("123") << QByteArray() << 3ll << true << Part::Internal + << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 9))) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data, QStringLiteral("%1_r0").arg(partId))) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item.id(), 2)); + + QTest::newRow("item 1, change to external") + << scenarios << QByteArray("PLD:DATA") << QByteArray("15_r0") << QByteArray("123456789") << 9ll << true << Part::External << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 9))) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data, QStringLiteral("%1_r1").arg(partId))) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item.id(), 3)); + + QTest::newRow("item 1, update external") << scenarios << QByteArray("PLD:DATA") << QByteArray("15_r1") << QByteArray("987654321") << 9ll << true + << Part::External << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 9))) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data, QStringLiteral("%1_r2").arg(partId))) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item.id(), 4)); + + QTest::newRow("item 1, external, no change") + << scenarios << QByteArray("PLD:DATA") << QByteArray("15_r2") << QByteArray("987654321") << 9ll << false << Part::External << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "1234")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item.id(), 5)); + + QTest::newRow("item 1, change to internal") + << scenarios << QByteArray("PLD:DATA") << QByteArray("1234") << QByteArray() << 4ll << true << Part::Internal << item; + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 4))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", "1234")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item.id(), 6)); + + QTest::newRow("item 1, internal, no change") + << scenarios << QByteArray("PLD:DATA") << QByteArray("1234") << QByteArray() << 4ll << false << Part::Internal << item; + } + + // Insert new item + PimItem item2 = item; + QVERIFY(item2.insert()); + + const QString foreignPath = FakeAkonadiServer::basePath() + QStringLiteral("/tmp/foreignPayloadFile"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, createCommand(item2)) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::MetaData)) + << TestScenario::create( + 5, + TestScenario::ClientCmd, + Protocol::StreamPayloadResponsePtr::create("PLD:DATA", Protocol::PartMetaData("PLD:DATA", 3, 0, Protocol::PartMetaData::Foreign))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::StreamPayloadCommandPtr::create("PLD:DATA", Protocol::StreamPayloadCommand::Data)) + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::StreamPayloadResponsePtr::create("PLD:DATA", foreignPath.toUtf8())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyItemsResponsePtr::create(item2.id(), 1)); + + QTest::newRow("item 2, new foreign part") << scenarios << QByteArray("PLD:DATA") << foreignPath.toUtf8() << QByteArray("123") << 3ll << false + << Part::Foreign << item2; + } + } + + void testStreamer() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QByteArray, expectedPartName); + QFETCH(QByteArray, expectedPartData); + QFETCH(QByteArray, expectedFileData); + QFETCH(qint64, expectedPartSize); + QFETCH(Part::Storage, storage); + QFETCH(PimItem, pimItem); + + if (storage == Part::External) { + // Create the payload file now, since don't have means to react + // directly to the streaming command + QFile file(ExternalPartStorage::resolveAbsolutePath(expectedPartData)); + file.open(QIODevice::WriteOnly); + file.write(expectedFileData); + file.close(); + } else if (storage == Part::Foreign) { + // Create the foreign payload file + QDir().mkpath(FakeAkonadiServer::basePath() + QStringLiteral("/tmp")); + QFile file(QString::fromUtf8(expectedPartData)); + file.open(QIODevice::WriteOnly); + file.write(expectedFileData); + file.close(); + } + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + PimItem item = PimItem::retrieveById(pimItem.id()); + const QVector parts = item.parts(); + QVERIFY(parts.count() == 1); + const Part part = parts[0]; + QCOMPARE(part.datasize(), expectedPartSize); + QCOMPARE(part.storage(), storage); + const QByteArray data = part.data(); + + if (storage == Part::External) { + QCOMPARE(data, expectedPartData); + QFile file(ExternalPartStorage::resolveAbsolutePath(data)); + QVERIFY(file.exists()); + QCOMPARE(file.size(), expectedPartSize); + QVERIFY(file.open(QIODevice::ReadOnly)); + + const QByteArray fileData = file.readAll(); + QCOMPARE(fileData, expectedFileData); + + // Make sure no previous versions are left behind in file_db_data + const int revision = data.mid(data.indexOf("_r") + 2).toInt(); + for (int i = 0; i < revision; ++i) { + const QByteArray fileName = QByteArray::number(part.id()) + "_r" + QByteArray::number(i); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); + // TRY because the deletion happens in another thread + QTRY_VERIFY2(!QFile::exists(filePath), qPrintable(filePath)); + } + } else if (storage == Part::Foreign) { + QCOMPARE(data, expectedPartData); + QFile file(QString::fromUtf8(data)); + QVERIFY(file.exists()); + QCOMPARE(file.size(), expectedPartSize); + QVERIFY(file.open(QIODevice::ReadOnly)); + + const QByteArray fileData = file.readAll(); + QCOMPARE(fileData, expectedFileData); + } else { + QCOMPARE(data, expectedPartData); + + // Make sure nothing is left behind in file_db_data + // TODO: we have no way of knowing what is the last revision + for (int i = 0; i <= 100; ++i) { + const QByteArray fileName = QByteArray::number(part.id()) + "_r" + QByteArray::number(i); + const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); + QTRY_VERIFY2(!QFile::exists(filePath), qPrintable(filePath)); + } + } + } +}; + +AKTEST_FAKESERVER_MAIN(PartStreamerTest) + +#include "partstreamertest.moc" diff --git a/autotests/server/parttypehelpertest.cpp b/autotests/server/parttypehelpertest.cpp new file mode 100644 index 0000000..901b671 --- /dev/null +++ b/autotests/server/parttypehelpertest.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include +#include + +#define QL1S(x) QStringLiteral(x) + +using namespace Akonadi::Server; + +class PartTypeHelperTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testParseFqName_data() + { + QTest::addColumn("fqName"); + QTest::addColumn("ns"); + QTest::addColumn("name"); + QTest::addColumn("shouldThrow"); + + QTest::newRow("empty") << QString() << QString() << QString() << true; + QTest::newRow("valid") << "PLD:RFC822" + << "PLD" + << "RFC822" << false; + QTest::newRow("no separator") << "ABC" << QString() << QString() << true; + QTest::newRow("no ns") << ":RFC822" << QString() << QString() << true; + QTest::newRow("no name") << "PLD:" << QString() << QString() << true; + QTest::newRow("too many separators") << "A:B:C" << QString() << QString() << true; + } + + void testParseFqName() + { + QFETCH(QString, fqName); + QFETCH(QString, ns); + QFETCH(QString, name); + QFETCH(bool, shouldThrow); + + std::pair p; + bool didThrow = false; + try { + p = PartTypeHelper::parseFqName(fqName); + } catch (const PartTypeException &e) { + didThrow = true; + } + + QCOMPARE(didThrow, shouldThrow); + QCOMPARE(p.first, ns); + QCOMPARE(p.second, name); + } + + void testConditionFromName() + { + Query::Condition c = PartTypeHelper::conditionFromFqName(QL1S("PLD:RFC822")); + QVERIFY(!c.isEmpty()); + QCOMPARE(c.subConditions().size(), 2); + } + + void testConditionFromNames() + { + Query::Condition c = PartTypeHelper::conditionFromFqNames(QStringList() << QL1S("PLD:RFC822") << QL1S("PLD:HEAD") << QL1S("PLD:ENVELOPE")); + QVERIFY(!c.isEmpty()); + QCOMPARE(c.subConditions().size(), 3); + Q_FOREACH (const Query::Condition &subC, c.subConditions()) { + QCOMPARE(subC.subConditions().size(), 2); + } + } +}; + +AKTEST_MAIN(PartTypeHelperTest) + +#include "parttypehelpertest.moc" diff --git a/autotests/server/querybuildertest.cpp b/autotests/server/querybuildertest.cpp new file mode 100644 index 0000000..6d3b886 --- /dev/null +++ b/autotests/server/querybuildertest.cpp @@ -0,0 +1,358 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "querybuildertest.h" +#include "moc_querybuildertest.cpp" + +#define QUERYBUILDER_UNITTEST + +#include "storage/query.cpp" +#include "storage/querybuilder.cpp" + +#include + +QTEST_MAIN(QueryBuilderTest) + +Q_DECLARE_METATYPE(QVector) + +using namespace Akonadi::Server; + +void QueryBuilderTest::testQueryBuilder_data() +{ + qRegisterMetaType>(); + mBuilders.clear(); + QTest::addColumn("qbId"); + QTest::addColumn("sql"); + QTest::addColumn>("bindValues"); + + QueryBuilder qb(QStringLiteral("table"), QueryBuilder::Select); + qb.addColumn(QStringLiteral("col1")); + mBuilders << qb; + QTest::newRow("simple select") << mBuilders.count() << QStringLiteral("SELECT col1 FROM table") << QVector(); + + qb.addColumn(QStringLiteral("col2")); + mBuilders << qb; + QTest::newRow("simple select 2") << mBuilders.count() << QStringLiteral("SELECT col1, col2 FROM table") << QVector(); + + qb.addValueCondition(QStringLiteral("col1"), Query::Equals, QVariant(5)); + QVector bindVals; + bindVals << QVariant(5); + mBuilders << qb; + QTest::newRow("single where") << mBuilders.count() << QStringLiteral("SELECT col1, col2 FROM table WHERE ( col1 = :0 )") << bindVals; + + qb.addColumnCondition(QStringLiteral("col1"), Query::LessOrEqual, QStringLiteral("col2")); + mBuilders << qb; + QTest::newRow("flat where") << mBuilders.count() << QStringLiteral("SELECT col1, col2 FROM table WHERE ( col1 = :0 AND col1 <= col2 )") << bindVals; + + qb.setSubQueryMode(Query::Or); + mBuilders << qb; + QTest::newRow("flat where 2") << mBuilders.count() << QStringLiteral("SELECT col1, col2 FROM table WHERE ( col1 = :0 OR col1 <= col2 )") << bindVals; + + Condition subCon; + subCon.addColumnCondition(QStringLiteral("col1"), Query::Greater, QStringLiteral("col2")); + subCon.addValueCondition(QStringLiteral("col1"), Query::NotEquals, QVariant()); + qb.addCondition(subCon); + mBuilders << qb; + QTest::newRow("hierarchical where") << mBuilders.count() + << QStringLiteral( + "SELECT col1, col2 FROM table WHERE ( col1 = :0 OR col1 <= col2 OR ( col1 > col2 AND col1 <> NULL ) )") + << bindVals; + + qb = QueryBuilder(QStringLiteral("table")); + qb.addAggregation(QStringLiteral("col1"), QStringLiteral("count")); + mBuilders << qb; + QTest::newRow("single aggregation") << mBuilders.count() << QStringLiteral("SELECT count(col1) FROM table") << QVector(); + + qb = QueryBuilder(QStringLiteral("table")); + qb.addColumn(QStringLiteral("col1")); + qb.addSortColumn(QStringLiteral("col1")); + mBuilders << qb; + QTest::newRow("single order by") << mBuilders.count() << QStringLiteral("SELECT col1 FROM table ORDER BY col1 ASC") << QVector(); + + qb.addSortColumn(QStringLiteral("col2"), Query::Descending); + mBuilders << qb; + QTest::newRow("multiple order by") << mBuilders.count() << QStringLiteral("SELECT col1 FROM table ORDER BY col1 ASC, col2 DESC") << QVector(); + + qb = QueryBuilder(QStringLiteral("table")); + qb.addColumn(QStringLiteral("col1")); + QStringList vals; + vals << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("c"); + qb.addValueCondition(QStringLiteral("col1"), Query::In, vals); + bindVals.clear(); + bindVals << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("c"); + mBuilders << qb; + QTest::newRow("where in") << mBuilders.count() << QStringLiteral("SELECT col1 FROM table WHERE ( col1 IN ( :0, :1, :2 ) )") << bindVals; + + qb = QueryBuilder(QStringLiteral("table"), QueryBuilder::Select); + qb.setDatabaseType(DbType::MySQL); + qb.addColumn(QStringLiteral("col1")); + qb.setLimit(1); + mBuilders << qb; + QTest::newRow("SELECT with LIMIT") << mBuilders.count() << QStringLiteral("SELECT col1 FROM table LIMIT 1") << QVector(); + + qb = QueryBuilder(QStringLiteral("table"), QueryBuilder::Update); + qb.setColumnValue(QStringLiteral("col1"), QStringLiteral("bla")); + bindVals.clear(); + bindVals << QStringLiteral("bla"); + mBuilders << qb; + QTest::newRow("update") << mBuilders.count() << QStringLiteral("UPDATE table SET col1 = :0") << bindVals; + + qb = QueryBuilder(QStringLiteral("table1"), QueryBuilder::Update); + qb.setDatabaseType(DbType::MySQL); + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table2"), QStringLiteral("table1.id"), QStringLiteral("table2.id")); + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table3"), QStringLiteral("table1.id"), QStringLiteral("table3.id")); + qb.setColumnValue(QStringLiteral("col1"), QStringLiteral("bla")); + bindVals.clear(); + bindVals << QStringLiteral("bla"); + mBuilders << qb; + QTest::newRow("update multi table MYSQL") + << mBuilders.count() << QStringLiteral("UPDATE table1, table2, table3 SET col1 = :0 WHERE ( ( table1.id = table2.id ) AND ( table1.id = table3.id ) )") + << bindVals; + + qb = QueryBuilder(QStringLiteral("table1"), QueryBuilder::Update); + qb.setDatabaseType(DbType::PostgreSQL); + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table2"), QStringLiteral("table1.id"), QStringLiteral("table2.id")); + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table3"), QStringLiteral("table1.id"), QStringLiteral("table3.id")); + qb.setColumnValue(QStringLiteral("col1"), QStringLiteral("bla")); + mBuilders << qb; + QTest::newRow("update multi table PSQL") + << mBuilders.count() + << QStringLiteral("UPDATE table1 SET col1 = :0 FROM table2 JOIN table3 WHERE ( ( table1.id = table2.id ) AND ( table1.id = table3.id ) )") << bindVals; + /// TODO: test for subquery in SQLite case + + qb = QueryBuilder(QStringLiteral("table"), QueryBuilder::Insert); + qb.setColumnValue(QStringLiteral("col1"), QStringLiteral("bla")); + mBuilders << qb; + QTest::newRow("insert single column") << mBuilders.count() << QStringLiteral("INSERT INTO table (col1) VALUES (:0)") << bindVals; + + qb = QueryBuilder(QStringLiteral("table"), QueryBuilder::Insert); + qb.setColumnValue(QStringLiteral("col1"), QStringLiteral("bla")); + qb.setColumnValue(QStringLiteral("col2"), 5); + bindVals << 5; + mBuilders << qb; + QTest::newRow("insert multi column") << mBuilders.count() << QStringLiteral("INSERT INTO table (col1, col2) VALUES (:0, :1)") << bindVals; + + qb = QueryBuilder(QStringLiteral("table"), QueryBuilder::Insert); + qb.setDatabaseType(DbType::PostgreSQL); + qb.setColumnValue(QStringLiteral("col1"), QStringLiteral("bla")); + qb.setColumnValue(QStringLiteral("col2"), 5); + mBuilders << qb; + QTest::newRow("insert multi column PSQL") << mBuilders.count() << QStringLiteral("INSERT INTO table (col1, col2) VALUES (:0, :1) RETURNING id") << bindVals; + + qb.setIdentificationColumn(QString()); + mBuilders << qb; + QTest::newRow("insert multi column PSQL without id") << mBuilders.count() << QStringLiteral("INSERT INTO table (col1, col2) VALUES (:0, :1)") << bindVals; + + // test GROUP BY foo + bindVals.clear(); + qb = QueryBuilder(QStringLiteral("table"), QueryBuilder::Select); + qb.addColumn(QStringLiteral("foo")); + qb.addGroupColumn(QStringLiteral("id1")); + mBuilders << qb; + QTest::newRow("select group by single column") << mBuilders.count() << QStringLiteral("SELECT foo FROM table GROUP BY id1") << bindVals; + // test GROUP BY foo, bar + qb.addGroupColumn(QStringLiteral("id2")); + mBuilders << qb; + QTest::newRow("select group by two columns") << mBuilders.count() << QStringLiteral("SELECT foo FROM table GROUP BY id1, id2") << bindVals; + // test: HAVING .addValueCondition() + qb.addValueCondition(QStringLiteral("bar"), Equals, 1, QueryBuilder::HavingCondition); + mBuilders << qb; + bindVals << 1; + QTest::newRow("select with having valueCond") << mBuilders.count() << QStringLiteral("SELECT foo FROM table GROUP BY id1, id2 HAVING ( bar = :0 )") + << bindVals; + // test: HAVING .addColumnCondition() + qb.addColumnCondition(QStringLiteral("asdf"), Equals, QStringLiteral("yxcv"), QueryBuilder::HavingCondition); + mBuilders << qb; + QTest::newRow("select with having columnCond") << mBuilders.count() + << QStringLiteral("SELECT foo FROM table GROUP BY id1, id2 HAVING ( bar = :0 AND asdf = yxcv )") << bindVals; + // test: HAVING .addCondition() + qb.addCondition(subCon, QueryBuilder::HavingCondition); + mBuilders << qb; + QTest::newRow("select with having condition") + << mBuilders.count() + << QStringLiteral("SELECT foo FROM table GROUP BY id1, id2 HAVING ( bar = :0 AND asdf = yxcv AND ( col1 > col2 AND col1 <> NULL ) )") << bindVals; + // test: HAVING and WHERE + qb.addValueCondition(QStringLiteral("bla"), Equals, 2, QueryBuilder::WhereCondition); + mBuilders << qb; + bindVals.clear(); + bindVals << 2 << 1; + QTest::newRow("select with having and where") + << mBuilders.count() + << QStringLiteral("SELECT foo FROM table WHERE ( bla = :0 ) GROUP BY id1, id2 HAVING ( bar = :1 AND asdf = yxcv AND ( col1 > col2 AND col1 <> NULL ) )") + << bindVals; + + { + /// SELECT with JOINS + QueryBuilder qbTpl = QueryBuilder(QStringLiteral("table1"), QueryBuilder::Select); + qbTpl.setDatabaseType(DbType::MySQL); + qbTpl.addColumn(QStringLiteral("col")); + bindVals.clear(); + + QueryBuilder qb = qbTpl; + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table2"), QStringLiteral("table2.t1_id"), QStringLiteral("table1.id")); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("table3"), QStringLiteral("table1.id"), QStringLiteral("table3.t1_id")); + mBuilders << qb; + QTest::newRow("select left join and inner join (different tables)") + << mBuilders.count() + << QStringLiteral("SELECT col FROM table1 INNER JOIN table2 ON ( table2.t1_id = table1.id ) LEFT JOIN table3 ON ( table1.id = table3.t1_id )") + << bindVals; + + qb = qbTpl; + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table2"), QStringLiteral("table2.t1_id"), QStringLiteral("table1.id")); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("table2"), QStringLiteral("table2.t1_id"), QStringLiteral("table1.id")); + mBuilders << qb; + // join-condition too verbose but should not have any impact on speed + QTest::newRow("select left join and inner join (same table)") + << mBuilders.count() << QStringLiteral("SELECT col FROM table1 INNER JOIN table2 ON ( table2.t1_id = table1.id AND ( table2.t1_id = table1.id ) )") + << bindVals; + + // order of joins in the query should be the same as we add the joins in code + qb = qbTpl; + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("b_table"), QStringLiteral("b_table.t1_id"), QStringLiteral("table1.id")); + qb.addJoin(QueryBuilder::InnerJoin, QStringLiteral("a_table"), QStringLiteral("a_table.b_id"), QStringLiteral("b_table.id")); + mBuilders << qb; + QTest::newRow("select join order") + << mBuilders.count() + << QStringLiteral("SELECT col FROM table1 INNER JOIN b_table ON ( b_table.t1_id = table1.id ) INNER JOIN a_table ON ( a_table.b_id = b_table.id )") + << bindVals; + } + + { + /// SELECT with CASE + QueryBuilder qbTpl = QueryBuilder(QStringLiteral("table1"), QueryBuilder::Select); + qbTpl.setDatabaseType(DbType::MySQL); + + QueryBuilder qb = qbTpl; + qb.addColumn(QStringLiteral("col")); + qb.addColumn(Query::Case(QStringLiteral("col1"), Query::Greater, 42, QStringLiteral("1"), QStringLiteral("0"))); + bindVals.clear(); + bindVals << 42; + mBuilders << qb; + QTest::newRow("select case simple") << mBuilders.count() << QStringLiteral("SELECT col, CASE WHEN ( col1 > :0 ) THEN 1 ELSE 0 END FROM table1") + << bindVals; + + qb = qbTpl; + qb.addAggregation(QStringLiteral("table1.col1"), QStringLiteral("sum")); + qb.addAggregation(QStringLiteral("table1.col2"), QStringLiteral("count")); + Query::Condition cond(Query::Or); + cond.addValueCondition(QStringLiteral("table3.col2"), Query::Equals, "value1"); + cond.addValueCondition(QStringLiteral("table3.col2"), Query::Equals, "value2"); + Query::Case caseStmt(cond, QStringLiteral("1"), QStringLiteral("0")); + qb.addAggregation(caseStmt, QStringLiteral("sum")); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("table2"), QStringLiteral("table1.col3"), QStringLiteral("table2.col1")); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("table3"), QStringLiteral("table2.col2"), QStringLiteral("table3.col1")); + bindVals.clear(); + bindVals << QStringLiteral("value1") << QStringLiteral("value2"); + mBuilders << qb; + QTest::newRow("select case, aggregation and joins") + << mBuilders.count() + << QString( + "SELECT sum(table1.col1), count(table1.col2), sum(CASE WHEN ( table3.col2 = :0 OR table3.col2 = :1 ) THEN 1 ELSE 0 END) " + "FROM table1 " + "LEFT JOIN table2 ON ( table1.col3 = table2.col1 ) " + "LEFT JOIN table3 ON ( table2.col2 = table3.col1 )") + << bindVals; + } + + { + /// UPDATE with INNER JOIN + QueryBuilder qbTpl = QueryBuilder(QStringLiteral("table1"), QueryBuilder::Update); + qbTpl.setColumnValue(QStringLiteral("col"), 42); + qbTpl.addJoin(QueryBuilder::InnerJoin, QStringLiteral("table2"), QStringLiteral("table2.t1_id"), QStringLiteral("table1.id")); + qbTpl.addValueCondition(QStringLiteral("table2.answer"), NotEquals, "foo"); + bindVals.clear(); + bindVals << QVariant(42) << QVariant("foo"); + + qb = qbTpl; + qb.setDatabaseType(DbType::MySQL); + mBuilders << qb; + QTest::newRow("update inner join MySQL") << mBuilders.count() + << QStringLiteral( + "UPDATE table1, table2 SET col = :0 WHERE ( table2.answer <> :1 AND ( table2.t1_id = table1.id ) )") + << bindVals; + + qb = qbTpl; + qb.setDatabaseType(DbType::PostgreSQL); + mBuilders << qb; + QTest::newRow("update inner join PSQL") << mBuilders.count() + << QStringLiteral( + "UPDATE table1 SET col = :0 FROM table2 WHERE ( table2.answer <> :1 AND ( table2.t1_id = table1.id ) )") + << bindVals; + + qb = qbTpl; + qb.setDatabaseType(DbType::Sqlite); + mBuilders << qb; + QTest::newRow("update inner join SQLite") + << mBuilders.count() + << QStringLiteral("UPDATE table1 SET col = :0 WHERE ( ( SELECT table2.answer FROM table2 WHERE ( ( table2.t1_id = table1.id ) ) ) <> :1 )") + << bindVals; + + qb = qbTpl; + qb.setDatabaseType(DbType::Sqlite); + Query::Condition condition; + condition.addValueCondition(QStringLiteral("table2.col2"), Query::Equals, 666); + condition.addValueCondition(QStringLiteral("table1.col3"), Query::Equals, "text"); + qb.addCondition(condition); + qb.addValueCondition(QStringLiteral("table1.id"), Query::Equals, 10); + mBuilders << qb; + bindVals << 666 << "text" << 10; + QTest::newRow("update inner join SQLite with subcondition") + << mBuilders.count() + << QString( + "UPDATE table1 SET col = :0 WHERE ( ( SELECT table2.answer FROM table2 WHERE " + "( ( table2.t1_id = table1.id ) ) ) <> :1 AND " + "( ( SELECT table2.col2 FROM table2 WHERE ( ( table2.t1_id = table1.id ) ) ) = :2 AND table1.col3 = :3 ) AND " + "table1.id = :4 )") + << bindVals; + } +} + +void QueryBuilderTest::testQueryBuilder() +{ + QFETCH(int, qbId); + QFETCH(QString, sql); + QFETCH(QVector, bindValues); + + --qbId; + + QVERIFY(mBuilders[qbId].exec()); + QCOMPARE(mBuilders[qbId].mStatement, sql); + QCOMPARE(mBuilders[qbId].mBindValues, bindValues); +} + +void QueryBuilderTest::benchQueryBuilder() +{ + const QString table1 = QStringLiteral("Table1"); + const QString table2 = QStringLiteral("Table2"); + const QString table3 = QStringLiteral("Table3"); + const QString table1_id = QStringLiteral("Table1.id"); + const QString table2_id = QStringLiteral("Table2.id"); + const QString table3_id = QStringLiteral("Table3.id"); + const QString aggregate = QStringLiteral("COUNT"); + const QVariant value = QVariant::fromValue(QStringLiteral("asdf")); + + const QStringList columns = QStringList() << QStringLiteral("Table1.id") << QStringLiteral("Table1.fooAsdf") << QStringLiteral("Table2.barLala") + << QStringLiteral("Table3.xyzFsd"); + + bool executed = true; + + QBENCHMARK { + QueryBuilder builder(table1, QueryBuilder::Select); + builder.setDatabaseType(DbType::MySQL); + builder.addColumns(columns); + builder.addJoin(QueryBuilder::InnerJoin, table2, table2_id, table1_id); + builder.addJoin(QueryBuilder::LeftJoin, table3, table1_id, table3_id); + builder.addAggregation(columns.first(), aggregate); + builder.addColumnCondition(columns.at(1), Query::LessOrEqual, columns.last()); + builder.addValueCondition(columns.at(3), Query::Equals, value); + builder.addSortColumn(columns.at(2)); + builder.setLimit(10); + builder.addGroupColumn(columns.at(3)); + executed = executed && builder.exec(); + } + + QVERIFY(executed); +} diff --git a/autotests/server/querybuildertest.h b/autotests/server/querybuildertest.h new file mode 100644 index 0000000..40eb27d --- /dev/null +++ b/autotests/server/querybuildertest.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef AKONADI_QUERYBUILDERTEST_H +#define AKONADI_QUERYBUILDERTEST_H + +#undef QT_NO_CAST_FROM_ASCII + +#include + +namespace Akonadi +{ +namespace Server +{ +class QueryBuilder; +} +} + +class QueryBuilderTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testQueryBuilder_data(); + void testQueryBuilder(); + void benchQueryBuilder(); + +private: + QList mBuilders; +}; + +#endif diff --git a/autotests/server/relationhandlertest.cpp b/autotests/server/relationhandlertest.cpp new file mode 100644 index 0000000..7f09bb7 --- /dev/null +++ b/autotests/server/relationhandlertest.cpp @@ -0,0 +1,495 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include + +#include "aktest.h" +#include "dbinitializer.h" +#include "entities.h" +#include "fakeakonadiserver.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::Relation::List) +Q_DECLARE_METATYPE(Akonadi::Server::Relation) + +static Protocol::ChangeNotificationList extractNotifications(const QSharedPointer ¬ificationSpy) +{ + Protocol::ChangeNotificationList receivedNotifications; + for (int q = 0; q < notificationSpy->size(); q++) { + // Only one notify call + if (notificationSpy->at(q).count() != 1) { + qWarning() << "Error: We're assuming only one notify call."; + return Protocol::ChangeNotificationList(); + } + const Protocol::ChangeNotificationList n = notificationSpy->at(q).first().value(); + for (int i = 0; i < n.size(); i++) { + // qDebug() << n.at(i); + receivedNotifications.append(n.at(i)); + } + } + return receivedNotifications; +} + +class RelationHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + RelationHandlerTest() + : QObject() + { + qRegisterMetaType(); + + mAkonadi.setPopulateDb(false); + mAkonadi.init(); + + RelationType type; + type.setName(QStringLiteral("type")); + type.insert(); + + RelationType type2; + type2.setName(QStringLiteral("type2")); + type2.insert(); + } + + QScopedPointer initializer; + + Protocol::RelationChangeNotificationPtr relationNotification(Protocol::RelationChangeNotification::Operation op, + const PimItem &item1, + const PimItem &item2, + const QString &rid, + const QString &type = QStringLiteral("type")) + { + auto notification = Protocol::RelationChangeNotificationPtr::create(); + notification->setOperation(op); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + Protocol::FetchRelationsResponse relation; + relation.setLeft(item1.id()); + relation.setLeftMimeType(item1.mimeType().name().toLatin1()); + relation.setRight(item2.id()); + relation.setRightMimeType(item2.mimeType().name().toLatin1()); + relation.setRemoteId(rid.toLatin1()); + relation.setType(type.toLatin1()); + notification->setRelation(std::move(relation)); + return notification; + } + +private Q_SLOTS: + void testStoreRelation_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + PimItem item2 = initializer->createItem("item2", col1); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedRelations"); + QTest::addColumn("expectedNotifications"); + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::ModifyRelationCommandPtr::create(item1.id(), item2.id(), "type")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyRelationResponsePtr::create()); + + Relation rel; + rel.setLeftId(item1.id()); + rel.setRightId(item2.id()); + RelationType type; + type.setName(QStringLiteral("type")); + rel.setRelationType(type); + + auto itemNotification = Protocol::ItemChangeNotificationPtr::create(); + itemNotification->setOperation(Protocol::ItemChangeNotification::ModifyRelations); + itemNotification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemNotification->setResource("testresource"); + itemNotification->setParentCollection(col1.id()); + itemNotification->setItems({*initializer->fetchResponse(item1), *initializer->fetchResponse(item2)}); + itemNotification->setAddedRelations({Protocol::ItemChangeNotification::Relation(item1.id(), item2.id(), QStringLiteral("type"))}); + + const auto notification = relationNotification(Protocol::RelationChangeNotification::Add, item1, item2, rel.remoteId()); + + QTest::newRow("uid create relation") << scenarios << (Relation::List() << rel) + << (Protocol::ChangeNotificationList() << notification << itemNotification); + } + } + + void testStoreRelation() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Relation::List, expectedRelations); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + const auto receivedNotifications = extractNotifications(mAkonadi.notificationSpy()); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(*receivedNotifications.at(i), *expectedNotifications.at(i)); + } + + const Relation::List relations = Relation::retrieveAll(); + // Q_FOREACH (const Relation &rel, relations) { + // akDebug() << rel.leftId() << rel.rightId(); + // } + QCOMPARE(relations.size(), expectedRelations.size()); + for (int i = 0; i < relations.size(); i++) { + QCOMPARE(relations.at(i).leftId(), expectedRelations.at(i).leftId()); + QCOMPARE(relations.at(i).rightId(), expectedRelations.at(i).rightId()); + // QCOMPARE(relations.at(i).typeId(), expectedRelations.at(i).typeId()); + QCOMPARE(relations.at(i).remoteId(), expectedRelations.at(i).remoteId()); + } + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.exec(); + } + + void testRemoveRelation_data() + { + initializer.reset(new DbInitializer); + QCOMPARE(Relation::retrieveAll().size(), 0); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + PimItem item2 = initializer->createItem("item2", col1); + + Relation rel; + rel.setLeftId(item1.id()); + rel.setRightId(item2.id()); + rel.setRelationType(RelationType::retrieveByName(QStringLiteral("type"))); + QVERIFY(rel.insert()); + + Relation rel2; + rel2.setLeftId(item1.id()); + rel2.setRightId(item2.id()); + rel2.setRelationType(RelationType::retrieveByName(QStringLiteral("type2"))); + QVERIFY(rel2.insert()); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedRelations"); + QTest::addColumn("expectedNotifications"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::RemoveRelationsCommandPtr::create(item1.id(), item2.id(), "type")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::RemoveRelationsResponsePtr::create()); + + auto itemNotification = Protocol::ItemChangeNotificationPtr::create(); + itemNotification->setOperation(Protocol::ItemChangeNotification::ModifyRelations); + itemNotification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemNotification->setResource("testresource"); + itemNotification->setParentCollection(col1.id()); + itemNotification->setItems({*initializer->fetchResponse(item1), *initializer->fetchResponse(item2)}); + itemNotification->setRemovedRelations({Protocol::ItemChangeNotification::Relation(item1.id(), item2.id(), QStringLiteral("type"))}); + + const auto notification = relationNotification(Protocol::RelationChangeNotification::Remove, item1, item2, rel.remoteId()); + + QTest::newRow("uid remove relation") << scenarios << (Relation::List() << rel2) + << (Protocol::ChangeNotificationList() << notification << itemNotification); + } + + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::RemoveRelationsCommandPtr::create(item1.id(), item2.id())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::RemoveRelationsResponsePtr::create()); + + auto itemNotification = Protocol::ItemChangeNotificationPtr::create(); + itemNotification->setOperation(Protocol::ItemChangeNotification::ModifyRelations); + itemNotification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemNotification->setResource("testresource"); + itemNotification->setParentCollection(col1.id()); + itemNotification->setItems({*initializer->fetchResponse(item1), *initializer->fetchResponse(item2)}); + itemNotification->setRemovedRelations({Protocol::ItemChangeNotification::Relation(item1.id(), item2.id(), QStringLiteral("type2"))}); + + const auto notification = relationNotification(Protocol::RelationChangeNotification::Remove, item1, item2, rel.remoteId(), QStringLiteral("type2")); + + QTest::newRow("uid remove relation without type") + << scenarios << Relation::List() << (Protocol::ChangeNotificationList() << notification << itemNotification); + } + } + + void testRemoveRelation() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Relation::List, expectedRelations); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + const auto receivedNotifications = extractNotifications(mAkonadi.notificationSpy()); + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(*receivedNotifications.at(i), *expectedNotifications.at(i)); + } + + const Relation::List relations = Relation::retrieveAll(); + // Q_FOREACH (const Relation &rel, relations) { + // akDebug() << rel.leftId() << rel.rightId() << rel.relationType().name() << rel.remoteId(); + // } + QCOMPARE(relations.size(), expectedRelations.size()); + for (int i = 0; i < relations.size(); i++) { + QCOMPARE(relations.at(i).leftId(), expectedRelations.at(i).leftId()); + QCOMPARE(relations.at(i).rightId(), expectedRelations.at(i).rightId()); + QCOMPARE(relations.at(i).typeId(), expectedRelations.at(i).typeId()); + QCOMPARE(relations.at(i).remoteId(), expectedRelations.at(i).remoteId()); + } + } + + void testListRelation_data() + { + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.exec(); + + initializer.reset(new DbInitializer); + QCOMPARE(Relation::retrieveAll().size(), 0); + Resource res = initializer->createResource("testresource"); + Collection col1 = initializer->createCollection("col1"); + PimItem item1 = initializer->createItem("item1", col1); + PimItem item2 = initializer->createItem("item2", col1); + PimItem item3 = initializer->createItem("item3", col1); + PimItem item4 = initializer->createItem("item4", col1); + + Relation rel; + rel.setLeftId(item1.id()); + rel.setRightId(item2.id()); + rel.setRelationType(RelationType::retrieveByName(QStringLiteral("type"))); + rel.setRemoteId(QStringLiteral("foobar1")); + QVERIFY(rel.insert()); + + Relation rel2; + rel2.setLeftId(item1.id()); + rel2.setRightId(item2.id()); + rel2.setRelationType(RelationType::retrieveByName(QStringLiteral("type2"))); + rel2.setRemoteId(QStringLiteral("foobar2")); + QVERIFY(rel2.insert()); + + Relation rel3; + rel3.setLeftId(item3.id()); + rel3.setRightId(item4.id()); + rel3.setRelationType(RelationType::retrieveByName(QStringLiteral("type"))); + rel3.setRemoteId(QStringLiteral("foobar3")); + QVERIFY(rel3.insert()); + + Relation rel4; + rel4.setLeftId(item4.id()); + rel4.setRightId(item3.id()); + rel4.setRelationType(RelationType::retrieveByName(QStringLiteral("type"))); + rel4.setRemoteId(QStringLiteral("foobar4")); + QVERIFY(rel4.insert()); + + QTest::addColumn("scenarios"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommandPtr::create(-1, QVector{"type"})) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type", + "foobar1")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item3.id(), + item3.mimeType().name().toUtf8(), + item4.id(), + item4.mimeType().name().toUtf8(), + "type", + "foobar3")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item4.id(), + item4.mimeType().name().toUtf8(), + item3.id(), + item3.mimeType().name().toUtf8(), + "type", + "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("filter by type") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommandPtr::create()) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type", + "foobar1")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type2", + "foobar2")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item3.id(), + item3.mimeType().name().toUtf8(), + item4.id(), + item4.mimeType().name().toUtf8(), + "type", + "foobar3")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item4.id(), + item4.mimeType().name().toUtf8(), + item3.id(), + item3.mimeType().name().toUtf8(), + "type", + "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("no filter") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::FetchRelationsCommandPtr::create(-1, QVector{}, QLatin1String("testresource"))) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type", + "foobar1")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type2", + "foobar2")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item3.id(), + item3.mimeType().name().toUtf8(), + item4.id(), + item4.mimeType().name().toUtf8(), + "type", + "foobar3")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item4.id(), + item4.mimeType().name().toUtf8(), + item3.id(), + item3.mimeType().name().toUtf8(), + "type", + "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("filter by resource with matching resource") << scenarios; + } + { + Resource res; + res.setName(QStringLiteral("testresource2")); + res.insert(); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::FetchRelationsCommandPtr::create(-1, QVector{}, QLatin1String("testresource2"))) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("filter by resource with nonmatching resource") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::FetchRelationsCommandPtr::create(item1.id(), -1, QVector{"type"})) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type", + "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("filter by left and type") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, + TestScenario::ClientCmd, + Protocol::FetchRelationsCommandPtr::create(-1, item2.id(), QVector{"type"})) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item1.id(), + item1.mimeType().name().toUtf8(), + item2.id(), + item2.mimeType().name().toUtf8(), + "type", + "foobar1")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("filter by right and type") << scenarios; + } + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() + << TestScenario::create(5, TestScenario::ClientCmd, Protocol::FetchRelationsCommandPtr::create(item3.id(), QVector{"type"})) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item3.id(), + item3.mimeType().name().toUtf8(), + item4.id(), + item4.mimeType().name().toUtf8(), + "type", + "foobar3")) + << TestScenario::create(5, + TestScenario::ServerCmd, + Protocol::FetchRelationsResponsePtr::create(item4.id(), + item4.mimeType().name().toUtf8(), + item3.id(), + item3.mimeType().name().toUtf8(), + "type", + "foobar4")) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::FetchRelationsResponsePtr::create()); + + QTest::newRow("fetch by side with typefilter") << scenarios; + } + } + + void testListRelation() + { + QFETCH(TestScenario::List, scenarios); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + } +}; + +AKTEST_FAKESERVER_MAIN(RelationHandlerTest) + +#include "relationhandlertest.moc" diff --git a/autotests/server/searchtest.cpp b/autotests/server/searchtest.cpp new file mode 100644 index 0000000..d792859 --- /dev/null +++ b/autotests/server/searchtest.cpp @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "aktest.h" +#include "fakeakonadiserver.h" +#include "handler/searchhelper.h" + +#include + +#include + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(QList) + +class SearchTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + SearchTest() + : QObject() + { + mAkonadi.setPopulateDb(false); + mAkonadi.init(); + } + + Collection createCollection(const Resource &res, const QString &name, const Collection &parent, const QStringList &mimetypes) + { + Collection col; + col.setName(name); + col.setResource(res); + col.setParentId(parent.isValid() ? parent.id() : 0); + col.insert(); + for (const QString &mimeType : mimetypes) { + MimeType mt = MimeType::retrieveByName(mimeType); + if (!mt.isValid()) { + mt = MimeType(mimeType); + mt.insert(); + } + col.addMimeType(mt); + } + return col; + } + +private Q_SLOTS: + void testSearchHelperCollectionListing_data() + { + /* + Fake Resource + |- Col 1 (inode/directory) + | |- Col 2 (inode/direcotry, application/octet-stream) + | | |- Col 3(application/octet-stream) + | |- Col 4 (text/plain) + |- Col 5 (inode/directory, text/plain) + |- Col 6 (inode/directory, application/octet-stream) + |- Col 7 (inode/directory, text/plain) + |- Col 8 (inode/directory, application/octet-stream) + |- Col 9 (unique/mime-type) + */ + + Resource res(QStringLiteral("Test Resource"), false); + res.insert(); + + Collection col1 = createCollection(res, QStringLiteral("Col 1"), Collection(), QStringList() << QStringLiteral("inode/directory")); + Collection col2 = createCollection(res, + QStringLiteral("Col 2"), + col1, + QStringList() << QStringLiteral("inode/directory") << QStringLiteral("application/octet-stream")); + Collection col3 = createCollection(res, QStringLiteral("Col 3"), col2, QStringList() << QStringLiteral("application/octet-stream")); + Collection col4 = createCollection(res, QStringLiteral("Col 4"), col2, QStringList() << QStringLiteral("text/plain")); + Collection col5 = + createCollection(res, QStringLiteral("Col 5"), Collection(), QStringList() << QStringLiteral("inode/directory") << QStringLiteral("text/plain")); + Collection col6 = createCollection(res, + QStringLiteral("Col 6"), + col5, + QStringList() << QStringLiteral("inode/directory") << QStringLiteral("application/octet-stream")); + Collection col7 = + createCollection(res, QStringLiteral("Col 7"), col5, QStringList() << QStringLiteral("inode/directory") << QStringLiteral("text/plain")); + Collection col8 = createCollection(res, + QStringLiteral("Col 8"), + col7, + QStringList() << QStringLiteral("text/directory") << QStringLiteral("application/octet-stream")); + Collection col9 = createCollection(res, QStringLiteral("Col 9"), col8, QStringList() << QStringLiteral("unique/mime-type")); + + QTest::addColumn>("ancestors"); + QTest::addColumn("mimetypes"); + QTest::addColumn>("expectedResults"); + + QTest::newRow("") << (QVector() << 0) << (QStringList() << QStringLiteral("text/plain")) + << (QVector() << col4.id() << col5.id() << col7.id()); + QTest::newRow("") << (QVector() << 0) << (QStringList() << QStringLiteral("application/octet-stream")) + << (QVector() << col2.id() << col3.id() << col6.id() << col8.id()); + QTest::newRow("") << (QVector() << col1.id()) << (QStringList() << QStringLiteral("text/plain")) << (QVector() << col4.id()); + QTest::newRow("") << (QVector() << col1.id()) << (QStringList() << QStringLiteral("unique/mime-type")) << QVector(); + QTest::newRow("") << (QVector() << col2.id() << col7.id()) << (QStringList() << QStringLiteral("application/octet-stream")) + << (QVector() << col3.id() << col8.id()); + } + + void testSearchHelperCollectionListing() + { + QFETCH(QVector, ancestors); + QFETCH(QStringList, mimetypes); + QFETCH(QVector, expectedResults); + + QVector results = SearchHelper::matchSubcollectionsByMimeType(ancestors, mimetypes); + + std::sort(expectedResults.begin(), expectedResults.end()); + std::sort(results.begin(), results.end()); + + QCOMPARE(results.size(), expectedResults.size()); + QCOMPARE(results, expectedResults); + } +}; + +AKTEST_FAKESERVER_MAIN(SearchTest) + +#include "searchtest.moc" diff --git a/autotests/server/taghandlertest.cpp b/autotests/server/taghandlertest.cpp new file mode 100644 index 0000000..617eec8 --- /dev/null +++ b/autotests/server/taghandlertest.cpp @@ -0,0 +1,446 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include + +#include "aktest.h" +#include "dbinitializer.h" +#include "entities.h" +#include "fakeakonadiserver.h" + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +typedef QPair TagTagAttributeListPair; + +Q_DECLARE_METATYPE(Akonadi::Server::Tag::List) +Q_DECLARE_METATYPE(Akonadi::Server::Tag) +Q_DECLARE_METATYPE(QVector) + +static Protocol::ChangeNotificationList extractNotifications(const QSharedPointer ¬ificationSpy) +{ + Protocol::ChangeNotificationList receivedNotifications; + for (int q = 0; q < notificationSpy->size(); q++) { + // Only one notify call + if (notificationSpy->at(q).count() != 1) { + qWarning() << "Error: We're assuming only one notify call."; + return Protocol::ChangeNotificationList(); + } + const auto n = notificationSpy->at(q).first().value(); + for (int i = 0; i < n.size(); i++) { + // qDebug() << n.at(i); + receivedNotifications.append(n.at(i)); + } + } + return receivedNotifications; +} + +class TagHandlerTest : public QObject +{ + Q_OBJECT + + FakeAkonadiServer mAkonadi; + +public: + TagHandlerTest() + : QObject() + { + qRegisterMetaType(); + + mAkonadi.setPopulateDb(false); + mAkonadi.init(); + } + + Protocol::FetchTagsResponsePtr + createResponse(const Tag &tag, const QByteArray &remoteId = QByteArray(), const Protocol::Attributes &attrs = Protocol::Attributes()) + { + auto resp = Protocol::FetchTagsResponsePtr::create(tag.id()); + resp->setGid(tag.gid().toUtf8()); + resp->setParentId(tag.parentId()); + resp->setType(tag.tagType().name().toUtf8()); + resp->setRemoteId(remoteId); + resp->setAttributes(attrs); + return resp; + } + + QScopedPointer initializer; + +private Q_SLOTS: + void testStoreTag_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + + // Make sure the type exists + TagType type = type.retrieveByName(QStringLiteral("PLAIN")); + if (!type.isValid()) { + type.setName(QStringLiteral("PLAIN")); + type.insert(); + } + + QTest::addColumn("scenarios"); + QTest::addColumn>>("expectedTags"); + QTest::addColumn("expectedNotifications"); + + { + auto cmd = Protocol::CreateTagCommandPtr::create(); + cmd->setGid("tag"); + cmd->setParentId(0); + cmd->setType("PLAIN"); + cmd->setAttributes({{"TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"}}); + + auto resp = Protocol::FetchTagsResponsePtr::create(1); + resp->setGid(cmd->gid()); + resp->setParentId(cmd->parentId()); + resp->setType(cmd->type()); + resp->setAttributes(cmd->attributes()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateTagResponsePtr::create()); + + Tag tag; + tag.setId(1); + tag.setTagType(type); + tag.setParentId(0); + + TagAttribute attribute; + attribute.setTagId(1); + attribute.setType("TAG"); + attribute.setValue("(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"); + + auto notification = Protocol::TagChangeNotificationPtr::create(); + notification->setOperation(Protocol::TagChangeNotification::Add); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification->setTag(Protocol::FetchTagsResponse(1)); + + QTest::newRow("uid create relation") << scenarios << QVector{{tag, {attribute}}} + << Protocol::ChangeNotificationList{notification}; + } + + { + auto cmd = Protocol::CreateTagCommandPtr::create(); + cmd->setGid("tag2"); + cmd->setParentId(1); + cmd->setType("PLAIN"); + cmd->setAttributes({{"TAG", "(\\\"tag3\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"}}); + + auto resp = Protocol::FetchTagsResponsePtr::create(2); + resp->setGid(cmd->gid()); + resp->setParentId(cmd->parentId()); + resp->setType(cmd->type()); + resp->setAttributes(cmd->attributes()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, resp) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::CreateTagResponsePtr::create()); + + Tag tag; + tag.setId(2); + tag.setTagType(type); + tag.setParentId(1); + + TagAttribute attribute; + attribute.setTagId(2); + attribute.setType("TAG"); + attribute.setValue("(\\\"tag3\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"); + + auto notification = Protocol::TagChangeNotificationPtr::create(); + notification->setOperation(Protocol::TagChangeNotification::Add); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification->setTag(Protocol::FetchTagsResponse(2)); + + QTest::newRow("create child tag") << scenarios << QVector{{tag, {attribute}}} + << Protocol::ChangeNotificationList{notification}; + } + } + + void testStoreTag() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(QVector, expectedTags); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + const auto receivedNotifications = extractNotifications(mAkonadi.notificationSpy()); + + QVariantList ids; + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < expectedNotifications.size(); i++) { + QCOMPARE(*receivedNotifications.at(i), *expectedNotifications.at(i)); + ids << Protocol::cmdCast(receivedNotifications.at(i)).tag().id(); + } + + SelectQueryBuilder qb; + qb.addValueCondition(Tag::idColumn(), Query::In, ids); + QVERIFY(qb.exec()); + const Tag::List tags = qb.result(); + QCOMPARE(tags.size(), expectedTags.size()); + for (int i = 0; i < tags.size(); i++) { + const Tag actual = tags.at(i); + const Tag expected = expectedTags.at(i).first; + const TagAttribute::List expectedAttrs = expectedTags.at(i).second; + + QCOMPARE(actual.id(), expected.id()); + QCOMPARE(actual.typeId(), expected.typeId()); + QCOMPARE(actual.parentId(), expected.parentId()); + + TagAttribute::List attributes = TagAttribute::retrieveFiltered(TagAttribute::tagIdColumn(), tags.at(i).id()); + QCOMPARE(attributes.size(), expectedAttrs.size()); + for (int j = 0; j < attributes.size(); ++j) { + const TagAttribute actualAttr = attributes.at(i); + const TagAttribute expectedAttr = expectedAttrs.at(i); + + QCOMPARE(actualAttr.tagId(), expectedAttr.tagId()); + QCOMPARE(actualAttr.type(), expectedAttr.type()); + QCOMPARE(actualAttr.value(), expectedAttr.value()); + } + } + } + + void testModifyTag_data() + { + initializer.reset(new DbInitializer); + Resource res = initializer->createResource("testresource"); + Resource res2 = initializer->createResource("testresource2"); + Collection col = initializer->createCollection("Col 1"); + PimItem pimItem = initializer->createItem("Item 1", col); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid")); + tag.insert(); + + pimItem.addTag(tag); + + TagRemoteIdResourceRelation rel; + rel.setRemoteId(QStringLiteral("TAG1RES2RID")); + rel.setResource(res2); + rel.setTag(tag); + rel.insert(); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedTags"); + QTest::addColumn("expectedNotifications"); + { + auto cmd = Protocol::ModifyTagCommandPtr::create(tag.id()); + cmd->setAttributes({{"TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"}}); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, createResponse(tag, QByteArray(), cmd->attributes())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponsePtr::create()); + + auto notification = Protocol::TagChangeNotificationPtr::create(); + notification->setOperation(Protocol::TagChangeNotification::Modify); + notification->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification->setTag(Protocol::FetchTagsResponse(tag.id())); + + QTest::newRow("uid store name") << scenarios << (Tag::List() << tag) << (Protocol::ChangeNotificationList() << notification); + } + + { + auto cmd = Protocol::ModifyTagCommandPtr::create(tag.id()); + cmd->setRemoteId("remote1"); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(QStringLiteral("testresource")) + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, + TestScenario::ServerCmd, + createResponse(tag, "remote1", {{"TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"}})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponsePtr::create()); + + // RID-only changes don't emit notifications + /* + Akonadi::Protocol::ChangeNotification notification; + notification.setType(Protocol::ChangeNotification::Tags); + notification.setOperation(Protocol::ChangeNotification::Modify); + notification.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + notification.addEntity(tag.id()); + */ + + QTest::newRow("uid store rid") << scenarios << (Tag::List() << tag) << Protocol::ChangeNotificationList(); + } + + { + auto cmd = Protocol::ModifyTagCommandPtr::create(tag.id()); + cmd->setRemoteId(QByteArray()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(res.name()) + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create( + 5, + TestScenario::ServerCmd, + createResponse(tag, QByteArray(), {{"TAG", "(\\\"tag2\\\" \\\"\\\" \\\"\\\" \\\"\\\" \\\"0\\\" () () \\\"-1\\\")"}})) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponsePtr::create()); + + // RID-only changes don't emit notifications + /* + Akonadi::Protocol::ChangeNotification tagChangeNtf; + tagChangeNtf.setType(Protocol::ChangeNotification::Tags); + tagChangeNtf.setOperation(Protocol::ChangeNotification::Modify); + tagChangeNtf.setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + tagChangeNtf.addEntity(tag.id()); + */ + + QTest::newRow("uid store unset one rid") << scenarios << (Tag::List() << tag) << Protocol::ChangeNotificationList(); + } + + { + auto cmd = Protocol::ModifyTagCommandPtr::create(tag.id()); + cmd->setRemoteId(QByteArray()); + + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << FakeAkonadiServer::selectResourceScenario(res2.name()) + << TestScenario::create(5, TestScenario::ClientCmd, cmd) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::DeleteTagResponsePtr::create()) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::ModifyTagResponsePtr::create()); + + auto itemUntaggedNtf = Protocol::ItemChangeNotificationPtr::create(); + itemUntaggedNtf->setOperation(Protocol::ItemChangeNotification::ModifyTags); + itemUntaggedNtf->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + itemUntaggedNtf->setItems({*initializer->fetchResponse(pimItem)}); + itemUntaggedNtf->setResource(res2.name().toLatin1()); + itemUntaggedNtf->setParentCollection(col.id()); + itemUntaggedNtf->setRemovedTags(QSet() << tag.id()); + + auto tagRemoveNtf = Protocol::TagChangeNotificationPtr::create(); + tagRemoveNtf->setOperation(Protocol::TagChangeNotification::Remove); + tagRemoveNtf->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + Protocol::FetchTagsResponse ntfTag; + ntfTag.setId(tag.id()); + ntfTag.setGid("gid"); + ntfTag.setType("PLAIN"); + tagRemoveNtf->setTag(std::move(ntfTag)); + + QTest::newRow("uid store unset last rid") << scenarios << Tag::List() << (Protocol::ChangeNotificationList() << itemUntaggedNtf << tagRemoveNtf); + } + } + + void testModifyTag() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Tag::List, expectedTags); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + const auto receivedNotifications = extractNotifications(mAkonadi.notificationSpy()); + + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < receivedNotifications.size(); i++) { + qDebug() << Protocol::debugString(receivedNotifications.at(i)); + qDebug() << Protocol::debugString(expectedNotifications.at(i)); + QCOMPARE(*receivedNotifications.at(i), *expectedNotifications.at(i)); + } + + const Tag::List tags = Tag::retrieveAll(); + QCOMPARE(tags.size(), expectedTags.size()); + for (int i = 0; i < tags.size(); i++) { + QCOMPARE(tags.at(i).id(), expectedTags.at(i).id()); + QCOMPARE(tags.at(i).tagType().name(), expectedTags.at(i).tagType().name()); + } + } + + void testRemoveTag_data() + { + initializer.reset(new DbInitializer); + Resource res1 = initializer->createResource("testresource3"); + Resource res2 = initializer->createResource("testresource4"); + + Tag tag; + TagType type; + type.setName(QStringLiteral("PLAIN")); + type.insert(); + tag.setTagType(type); + tag.setGid(QStringLiteral("gid2")); + tag.insert(); + + TagRemoteIdResourceRelation rel1; + rel1.setRemoteId(QStringLiteral("TAG2RES1RID")); + rel1.setResource(res1); + rel1.setTag(tag); + rel1.insert(); + + TagRemoteIdResourceRelation rel2; + rel2.setRemoteId(QStringLiteral("TAG2RES2RID")); + rel2.setResource(res2); + rel2.setTag(tag); + rel2.insert(); + + QTest::addColumn("scenarios"); + QTest::addColumn("expectedTags"); + QTest::addColumn("expectedNotifications"); + { + TestScenario::List scenarios; + scenarios << FakeAkonadiServer::loginScenario() << TestScenario::create(5, TestScenario::ClientCmd, Protocol::DeleteTagCommandPtr::create(tag.id())) + << TestScenario::create(5, TestScenario::ServerCmd, Protocol::DeleteTagResponsePtr::create()); + + auto ntf = Protocol::TagChangeNotificationPtr::create(); + ntf->setOperation(Protocol::TagChangeNotification::Remove); + ntf->setSessionId(FakeAkonadiServer::instanceName().toLatin1()); + + auto res1Ntf = Protocol::TagChangeNotificationPtr::create(*ntf); + Protocol::FetchTagsResponse res1NtfTag; + res1NtfTag.setId(tag.id()); + res1NtfTag.setRemoteId(rel1.remoteId().toLatin1()); + res1Ntf->setTag(std::move(res1NtfTag)); + res1Ntf->setResource(res1.name().toLatin1()); + + auto res2Ntf = Protocol::TagChangeNotificationPtr::create(*ntf); + Protocol::FetchTagsResponse res2NtfTag; + res2NtfTag.setId(tag.id()); + res2NtfTag.setRemoteId(rel2.remoteId().toLatin1()); + res2Ntf->setTag(std::move(res2NtfTag)); + res2Ntf->setResource(res2.name().toLatin1()); + + auto clientNtf = Protocol::TagChangeNotificationPtr::create(*ntf); + clientNtf->setTag(Protocol::FetchTagsResponse(tag.id())); + + QTest::newRow("uid remove") << scenarios << Tag::List() << (Protocol::ChangeNotificationList() << res1Ntf << res2Ntf << clientNtf); + } + } + + void testRemoveTag() + { + QFETCH(TestScenario::List, scenarios); + QFETCH(Tag::List, expectedTags); + QFETCH(Protocol::ChangeNotificationList, expectedNotifications); + + mAkonadi.setScenarios(scenarios); + mAkonadi.runTest(); + + const auto receivedNotifications = extractNotifications(mAkonadi.notificationSpy()); + + QCOMPARE(receivedNotifications.size(), expectedNotifications.count()); + for (int i = 0; i < receivedNotifications.size(); i++) { + QCOMPARE(*receivedNotifications.at(i), *expectedNotifications.at(i)); + } + + const Tag::List tags = Tag::retrieveAll(); + QCOMPARE(tags.size(), 0); + } +}; + +AKTEST_FAKESERVER_MAIN(TagHandlerTest) + +#include "taghandlertest.moc" diff --git a/autotests/shared/CMakeLists.txt b/autotests/shared/CMakeLists.txt new file mode 100644 index 0000000..f0eca2c --- /dev/null +++ b/autotests/shared/CMakeLists.txt @@ -0,0 +1,20 @@ +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) + +macro(add_unit_test _source) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} NAME_PREFIX "AkonadiShared-" ${_source}) + if (ENABLE_ASAN) + set_tests_properties(AkonadiShared-${_name} PROPERTIES + ENVIRONMENT ASAN_OPTIONS=symbolize=1 + ) + endif() + target_link_libraries(${_name} + akonadi_shared + Qt::Test + ${CMAKE_EXE_LINKER_FLAGS_ASAN} + ) +endmacro() + +add_unit_test(akrangestest.cpp) +add_unit_test(akscopeguardtest.cpp) diff --git a/autotests/shared/akrangestest.cpp b/autotests/shared/akrangestest.cpp new file mode 100644 index 0000000..ef3a753 --- /dev/null +++ b/autotests/shared/akrangestest.cpp @@ -0,0 +1,455 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include + +#include + +using namespace AkRanges; + +namespace +{ +int transformFreeFunc(int i) +{ + return i * 2; +} + +struct TransformHelper { +public: + static int transform(int i) + { + return transformFreeFunc(i); + } + + int operator()(int i) const + { + return transformFreeFunc(i); + } +}; + +bool filterFreeFunc(int i) +{ + return i % 2 == 0; +} + +struct FilterHelper { +public: + static bool filter(int i) + { + return filterFreeFunc(i); + } + + bool operator()(int i) + { + return filterFreeFunc(i); + } +}; + +} // namespace + +class AkRangesTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + qSetGlobalQHashSeed(0); + } + + void testTraits() + { + QVERIFY(AkTraits::isAppendable>); + QVERIFY(!AkTraits::isInsertable>); + QVERIFY(AkTraits::isReservable>); + + QVERIFY(!AkTraits::isAppendable>); + QVERIFY(AkTraits::isInsertable>); + QVERIFY(AkTraits::isReservable>); + + QVERIFY(!AkTraits::isAppendable); + QVERIFY(!AkTraits::isInsertable); + QVERIFY(AkTraits::isReservable); + } + + void testContainerConversion() + { + { + QVector in = {1, 2, 3, 4, 5}; + QCOMPARE(in | Actions::toQList, in.toList()); + QCOMPARE(in | Actions::toQList | Actions::toQVector, in); + QCOMPARE(in | Actions::toQSet, QSet(in.begin(), in.end())); + } + { + QList in = {1, 2, 3, 4, 5}; + QCOMPARE(in | Actions::toQVector, in.toVector()); + QCOMPARE(in | Actions::toQVector | Actions::toQList, in); + QCOMPARE(in | Actions::toQSet, QSet(in.begin(), in.end())); + } + } + + void testAssociativeContainerConversion() + { + QVector> in = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; + QMap out = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; + QCOMPARE(in | Actions::toQMap, out); + } + + void testRangeConversion() + { + { + QList in = {1, 2, 3, 4, 5}; + AkRanges::detail::Range::const_iterator> range(in.cbegin(), in.cend()); + QCOMPARE(range | Actions::toQVector, QVector::fromList(in)); + } + + { + QVector in = {1, 2, 3, 4, 5}; + AkRanges::detail::Range::const_iterator> range(in.cbegin(), in.cend()); + QCOMPARE(range | Actions::toQList, in.toList()); + } + + { + QVector> in = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; + QMap out = {{1, QStringLiteral("One")}, {2, QStringLiteral("Two")}, {3, QStringLiteral("Three")}}; + AkRanges::detail::Range>::const_iterator> range(in.cbegin(), in.cend()); + QCOMPARE(range | Actions::toQMap, out); + } + } + + void testTransform() + { + QList in = {1, 2, 3, 4, 5}; + QList out = {2, 4, 6, 8, 10}; + QCOMPARE(in | Views::transform([](int i) { + return i * 2; + }) | Actions::toQList, + out); + QCOMPARE(in | Views::transform(transformFreeFunc) | Actions::toQList, out); + QCOMPARE(in | Views::transform(&TransformHelper::transform) | Actions::toQList, out); + QCOMPARE(in | Views::transform(TransformHelper()) | Actions::toQList, out); + } + +private: + class CopyCounter + { + public: + CopyCounter() = default; + CopyCounter(const CopyCounter &other) + : copyCount(other.copyCount + 1) + , transformed(other.transformed) + { + } + CopyCounter(CopyCounter &&other) = default; + CopyCounter &operator=(const CopyCounter &other) + { + copyCount = other.copyCount + 1; + transformed = other.transformed; + return *this; + } + CopyCounter &operator=(CopyCounter &&other) = default; + ~CopyCounter() = default; + + int copyCount = 0; + bool transformed = false; + }; + +private Q_SLOTS: + + void testTransformCopyCount() + { + { + QList in = {{}}; // 1st copy (QList::append()) + QList out = in | Views::transform([](const auto &c) { + CopyCounter r(c); // 2nd copy (expected) + r.transformed = true; + return r; + }) + | Actions::toQList; // 3rd copy (QList::append()) + QCOMPARE(out.size(), in.size()); + QCOMPARE(out[0].copyCount, 3); + QCOMPARE(out[0].transformed, true); + } + + { + QVector in(1); // construct vector of one element, so no copying + // occurs at initialization + QVector out = in | Views::transform([](const auto &c) { + CopyCounter r(c); // 1st copy + r.transformed = true; + return r; + }) + | Actions::toQVector; + QCOMPARE(out.size(), in.size()); + QCOMPARE(out[0].copyCount, 1); + QCOMPARE(out[0].transformed, true); + } + } + + void testTransformConvert() + { + { + QList in = {1, 2, 3, 4, 5}; + QVector out = {2, 4, 6, 8, 10}; + QCOMPARE(in | Views::transform([](int i) { + return i * 2; + }) | Actions::toQVector, + out); + } + + { + QVector in = {1, 2, 3, 4, 5}; + QList out = {2, 4, 6, 8, 10}; + QCOMPARE(in | Views::transform([](int i) { + return i * 2; + }) | Actions::toQList, + out); + } + } + + void testCreateRange() + { + { + QList in = {1, 2, 3, 4, 5, 6}; + QList out = {3, 4, 5}; + QCOMPARE(Views::range(in.begin() + 2, in.begin() + 5) | Actions::toQList, out); + } + } + + void testRangeWithTransform() + { + { + QList in = {1, 2, 3, 4, 5, 6}; + QList out = {6, 8, 10}; + QCOMPARE(Views::range(in.begin() + 2, in.begin() + 5) | Views::transform([](int i) { + return i * 2; + }) | Actions::toQList, + out); + } + } + + void testTransformType() + { + { + QStringList in = {QStringLiteral("foo"), QStringLiteral("foobar"), QStringLiteral("foob")}; + QList out = {3, 6, 4}; + QCOMPARE(in | Views::transform([](const auto &str) { + return str.size(); + }) | Actions::toQList, + out); + } + } + + void testFilter() + { + { + QList in = {1, 2, 3, 4, 5, 6, 7, 8}; + QList out = {2, 4, 6, 8}; + QCOMPARE(in | Views::filter([](int i) { + return i % 2 == 0; + }) | Actions::toQList, + out); + QCOMPARE(in | Views::filter(filterFreeFunc) | Actions::toQList, out); + QCOMPARE(in | Views::filter(&FilterHelper::filter) | Actions::toQList, out); + QCOMPARE(in | Views::filter(FilterHelper()) | Actions::toQList, out); + } + } + + void testFilterTransform() + { + { + QStringList in = {QStringLiteral("foo"), QStringLiteral("foobar"), QStringLiteral("foob")}; + QList out = {6}; + QCOMPARE(in | Views::transform(&QString::size) | Views::filter([](int i) { + return i > 5; + }) | Actions::toQList, + out); + QCOMPARE(in | Views::filter([](const auto &str) { + return str.size() > 5; + }) | Views::transform(&QString::size) + | Actions::toQList, + out); + } + } + + void testTemporaryContainer() + { + const auto func = [] { + QStringList rv; + for (int i = 0; i < 5; i++) { + rv.push_back(QString::number(i)); + } + return rv; + }; + { + QList out = {0, 2, 4}; + QCOMPARE(func() | Views::transform([](const auto &str) { + return str.toInt(); + }) | Views::filter([](int i) { + return i % 2 == 0; + }) | Actions::toQList, + out); + } + { + QList out = {0, 2, 4}; + QCOMPARE(func() | Views::filter([](const auto &v) { + return v.toInt() % 2 == 0; + }) | Views::transform([](const auto &str) { + return str.toInt(); + }) | Actions::toQList, + out); + } + } + + void testTemporaryRange() + { + const auto func = [] { + QStringList rv; + for (int i = 0; i < 5; ++i) { + rv.push_back(QString::number(i)); + } + return rv | Views::transform([](const auto &str) { + return str.toInt(); + }); + }; + QList out = {1, 3}; + QCOMPARE(func() | Views::filter([](int i) { + return i % 2 == 1; + }) | Actions::toQList, + out); + } + +private: + struct ForEachCallable { + public: + explicit ForEachCallable(QList &out) + : mOut(out) + { + } + + void operator()(int i) + { + mOut.push_back(i); + } + + static void append(int i) + { + sOut.push_back(i); + } + static void clear() + { + sOut.clear(); + } + static QList sOut; + + private: + QList &mOut; + }; + +private Q_SLOTS: + void testForEach() + { + const QList in = {1, 2, 3, 4, 5, 6}; + { + QList out; + in | Actions::forEach([&out](int v) { + out.push_back(v); + }); + QCOMPARE(out, in); + } + { + QList out; + in | Actions::forEach(ForEachCallable(out)); + QCOMPARE(out, in); + } + { + ForEachCallable::clear(); + in | Actions::forEach(&ForEachCallable::append); + QCOMPARE(ForEachCallable::sOut, in); + } + { + QList out; + QCOMPARE(in | Actions::forEach([&out](int v) { + out.push_back(v); + }) | Views::filter([](int v) { + return v % 2 == 0; + }) | Views::transform([](int v) { + return v * 2; + }) | Actions::toQList, + QList({4, 8, 12})); + QCOMPARE(out, in); + } + } + +private: + template class Container> void testKeysValuesHelper() + { + const Container in = {{1, QStringLiteral("1")}, {2, QStringLiteral("2")}, {3, QStringLiteral("3")}}; + + { + const QList out = {1, 2, 3}; + QCOMPARE(out, in | Views::keys | Actions::toQList); + } + { + const QStringList out = {QStringLiteral("1"), QStringLiteral("2"), QStringLiteral("3")}; + QCOMPARE(out, in | Views::values | Actions::toQList); + } + } + +private Q_SLOTS: + void testKeysValues() + { + testKeysValuesHelper(); + testKeysValuesHelper(); + } + + void testAll() + { + const QList vals = {2, 4, 6, 8, 10}; + QVERIFY(vals | Actions::all([](int v) { + return v % 2 == 0; + })); + QVERIFY(!(vals | Actions::all([](int v) { + return v % 2 == 1; + }))); + } + + void testAny() + { + const QList vals = {1, 3, 5, 7, 9}; + QVERIFY(vals | Actions::any([](int v) { + return v % 2 == 1; + })); + QVERIFY(!(vals | Actions::any([](int v) { + return v % 2 == 0; + }))); + } + + void testNone() + { + const QList vals = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + QVERIFY(vals | Views::filter([](int i) { + return i % 2 == 0; + }) + | Actions::none([](int i) { + return i % 2 == 1; + })); + QVERIFY(!(vals | Views::filter([](int i) { + return i % 2 == 0; + }) + | Actions::none([](int i) { + return i % 2 == 0; + }))); + } +}; + +QList AkRangesTest::ForEachCallable::sOut; + +QTEST_GUILESS_MAIN(AkRangesTest) + +#include "akrangestest.moc" diff --git a/autotests/shared/akscopeguardtest.cpp b/autotests/shared/akscopeguardtest.cpp new file mode 100644 index 0000000..7bda636 --- /dev/null +++ b/autotests/shared/akscopeguardtest.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include "shared/akscopeguard.h" + +using namespace Akonadi; + +class AkScopeGuardTest : public QObject +{ + Q_OBJECT + +private: + static void staticMethod() + { + Q_ASSERT(!mCalled); + mCalled = true; + } + + void regularMethod() + { + Q_ASSERT(!mCalled); + mCalled = true; + } + +private Q_SLOTS: + + void testLambda() + { + mCalled = false; + { + AkScopeGuard guard([&]() { + Q_ASSERT(!mCalled); + mCalled = true; + }); + } + QVERIFY(mCalled); + } + + void testStaticMethod() + { + mCalled = false; + { + AkScopeGuard guard(&AkScopeGuardTest::staticMethod); + } + QVERIFY(mCalled); + } + + void testBindExpr() + { + mCalled = false; + { + AkScopeGuard guard(std::bind(&AkScopeGuardTest::regularMethod, this)); + } + QVERIFY(mCalled); + } + + void testStdFunction() + { + mCalled = false; + std::function func = [&]() { + Q_ASSERT(!mCalled); + mCalled = true; + }; + { + AkScopeGuard guard(func); + } + QVERIFY(mCalled); + } + +private: + static bool mCalled; +}; + +bool AkScopeGuardTest::mCalled = false; + +QTEST_GUILESS_MAIN(AkScopeGuardTest) + +#include "akscopeguardtest.moc" diff --git a/autotests/widgets/CMakeLists.txt b/autotests/widgets/CMakeLists.txt new file mode 100644 index 0000000..004b9f6 --- /dev/null +++ b/autotests/widgets/CMakeLists.txt @@ -0,0 +1,8 @@ +macro(add_akonadi_isolated_widget_test source) + add_akonadi_isolated_test(SOURCE ${source} LINK_LIBRARIES KF5::AkonadiWidgets KF5::Crash) +endmacro() + +add_akonadi_isolated_widget_test(tageditwidgettest.cpp) +add_akonadi_isolated_widget_test(tagwidgettest.cpp) +add_akonadi_isolated_widget_test(subscriptiondialogtest.cpp) +add_akonadi_isolated_widget_test(tagselectioncomboboxtest.cpp) diff --git a/autotests/widgets/subscriptiondialogtest.cpp b/autotests/widgets/subscriptiondialogtest.cpp new file mode 100644 index 0000000..8b75efd --- /dev/null +++ b/autotests/widgets/subscriptiondialogtest.cpp @@ -0,0 +1,253 @@ +/* + * SPDX-FileCopyrightText: 2020 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "qtest_akonadi.h" + +#include +#include + +#include "subscriptiondialog.h" +#include "subscriptionjob_p.h" +#include "subscriptionmodel_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +class SubscriptionDialogTest : public QObject +{ + Q_OBJECT + + struct TestSetup { + enum { + defaultCollectionCount = 7, + }; + + TestSetup() + { + widget = std::make_unique(QStringList{Collection::mimeType(), QStringLiteral("application/octet-stream")}); + widget->setAttribute(Qt::WA_DeleteOnClose, false); + widget->show(); + + model = widget->findChild(); + QVERIFY(model); + QSignalSpy modelLoadedSpy(model, &SubscriptionModel::modelLoaded); + + buttonBox = widget->findChild(); + QVERIFY(buttonBox); + QVERIFY(!buttonBox->button(QDialogButtonBox::Ok)->isEnabled()); + searchLineEdit = widget->findChild(QStringLiteral("searchLineEdit")); + QVERIFY(searchLineEdit); + subscribedOnlyChkBox = widget->findChild(QStringLiteral("subscribedOnlyCheckBox")); + QVERIFY(subscribedOnlyChkBox); + collectionView = widget->findChild(QStringLiteral("collectionView")); + QVERIFY(collectionView); + subscribeButton = widget->findChild(QStringLiteral("subscribeButton")); + QVERIFY(subscribeButton); + unsubscribeButton = widget->findChild(QStringLiteral("unsubscribeButton")); + QVERIFY(unsubscribeButton); + + QVERIFY(QTest::qWaitForWindowActive(widget.get())); + QVERIFY(modelLoadedSpy.wait()); + QTest::qWait(100); + + // Helps with testing :) + collectionView->expandAll(); + + // Post-setup conditions + QCOMPARE(countTotalRows(), defaultCollectionCount); + QVERIFY(buttonBox->button(QDialogButtonBox::Ok)->isEnabled()); + + valid = true; + } + + ~TestSetup() + { + } + + int countTotalRows(const QModelIndex &parent = {}) const + { + const auto count = collectionView->model()->rowCount(parent); + int total = count; + for (int i = 0; i < count; ++i) { + total += countTotalRows(collectionView->model()->index(i, 0, parent)); + } + return total; + } + + static bool unsubscribeCollection(const Collection &col) + { + return modifySubscription({}, {col}); + } + + static bool subscribeCollection(const Collection &col) + { + return modifySubscription({col}, {}); + } + + static bool modifySubscription(const Collection::List &subscribe, const Collection::List &unsubscribe) + { + auto job = new SubscriptionJob(); + job->subscribe(subscribe); + job->unsubscribe(unsubscribe); + bool ok = false; + [job, &ok]() { + AKVERIFYEXEC(job); + ok = true; + }(); + AKVERIFY(ok); + + return true; + } + + bool selectCollection(const Collection &col) const + { + AKVERIFY(col.isValid()); + const QModelIndex colIdx = indexForCollection(col); + AKVERIFY(colIdx.isValid()); + + collectionView->scrollTo(colIdx); + QTest::mouseClick(collectionView->viewport(), Qt::LeftButton, {}, collectionView->visualRect(colIdx).center()); + + AKCOMPARE(collectionView->currentIndex(), colIdx); + return true; + } + + bool isCollectionChecked(const Collection &col) const + { + AKVERIFY(col.isValid()); + const auto colIdx = indexForCollection(col); + AKVERIFY(colIdx.isValid()); + + return collectionView->model()->data(colIdx, Qt::CheckStateRole).value() == Qt::Checked; + } + + void acceptDialog() const + { + auto button = buttonBox->button(QDialogButtonBox::Ok); + QTest::mouseClick(button, Qt::LeftButton); + } + + QModelIndex indexForCollection(const Collection &col) const + { + auto model = collectionView->model(); + std::deque idxQueue; + idxQueue.push_back(QModelIndex{}); + while (!idxQueue.empty()) { + const auto idx = idxQueue.front(); + idxQueue.pop_front(); + if (model->data(idx, EntityTreeModel::CollectionIdRole).value() == col.id()) { + return idx; + } + for (int i = 0; i < model->rowCount(idx); ++i) { + idxQueue.push_back(model->index(i, 0, idx)); + } + } + return {}; + } + + std::unique_ptr widget; + QDialogButtonBox *buttonBox = nullptr; + QLineEdit *searchLineEdit = nullptr; + QCheckBox *subscribedOnlyChkBox = nullptr; + QTreeView *collectionView = nullptr; + QPushButton *subscribeButton = nullptr; + QPushButton *unsubscribeButton = nullptr; + SubscriptionModel *model = nullptr; + + bool valid = false; + }; + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testSearchFilter() + { + TestSetup test; + QVERIFY(test.valid); + + QTest::keyClicks(test.searchLineEdit, QStringLiteral("foo")); + QCOMPARE(test.countTotalRows(), 2); + } + + void testSubscribedOnlyCheckbox() + { + const auto col = Collection{AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bla"))}; + const AkScopeGuard guard([col]() { + TestSetup::subscribeCollection(col); + }); + + QVERIFY(TestSetup::unsubscribeCollection(col)); + + TestSetup test; + QVERIFY(test.valid); + + test.subscribedOnlyChkBox->setChecked(true); + + QTRY_COMPARE(test.countTotalRows(), test.defaultCollectionCount - 1); + + test.subscribedOnlyChkBox->setChecked(false); + + QTRY_COMPARE(test.countTotalRows(), test.defaultCollectionCount); + } + + void testSubscribeButton() + { + const auto col = Collection{AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bla"))}; + const AkScopeGuard guard([col]() { + TestSetup::subscribeCollection(col); + }); + + QVERIFY(TestSetup::unsubscribeCollection(col)); + + TestSetup test; + QVERIFY(test.valid); + + QVERIFY(test.selectCollection(col)); + QTest::mouseClick(test.subscribeButton, Qt::LeftButton); + QVERIFY(test.isCollectionChecked(col)); + + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy monitorSpy(monitor.get(), &Monitor::collectionSubscribed); + test.acceptDialog(); + QVERIFY(monitorSpy.wait()); + } + + void testUnsubscribeButton() + { + const auto col = Collection{AkonadiTest::collectionIdFromPath(QStringLiteral("res1/foo/bla"))}; + + TestSetup test; + QVERIFY(test.valid); + + QVERIFY(test.selectCollection(col)); + QTest::mouseClick(test.unsubscribeButton, Qt::LeftButton); + QVERIFY(!test.isCollectionChecked(col)); + + auto monitor = AkonadiTest::getTestMonitor(); + QSignalSpy monitorSpy(monitor.get(), &Monitor::collectionUnsubscribed); + test.acceptDialog(); + QVERIFY(monitorSpy.wait()); + } +}; + +QTEST_AKONADIMAIN(SubscriptionDialogTest) + +#include "subscriptiondialogtest.moc" diff --git a/autotests/widgets/tageditwidgettest.cpp b/autotests/widgets/tageditwidgettest.cpp new file mode 100644 index 0000000..9612a81 --- /dev/null +++ b/autotests/widgets/tageditwidgettest.cpp @@ -0,0 +1,356 @@ +/* + * SPDX-FileCopyrightText: 2020 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "qtest_akonadi.h" +#include + +#include "monitor.h" +#include "tag.h" +#include "tagcreatejob.h" +#include "tagdeletejob.h" +#include "tageditwidget.h" +#include "tagmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +/*** + * This test also covers TagManagementDialog and TagSelectionDialog, which + * both wrap TagEditWidget and provide their own own Monitor and TagModel, + * just one allows selection and the other does not + */ +class TagEditWidgetTest : public QObject +{ + Q_OBJECT + + struct TestSetup { + TestSetup() + { + monitor = std::make_unique(); + monitor->setTypeMonitored(Monitor::Tags); + + model = std::make_unique(monitor.get()); + QSignalSpy modelSpy(model.get(), &TagModel::populated); + QVERIFY(modelSpy.wait()); + QCOMPARE(model->rowCount(), 1); // there's one existing tag + + widget = std::make_unique(); + widget->setModel(model.get()); + widget->show(); + QVERIFY(QTest::qWaitForWindowActive(widget.get())); + + newTagEdit = widget->findChild(QStringLiteral("newTagEdit")); + QVERIFY(newTagEdit); + newTagButton = widget->findChild(QStringLiteral("newTagButton")); + QVERIFY(newTagButton); + QVERIFY(!newTagButton->isEnabled()); + tagsView = widget->findChild(QStringLiteral("tagsView")); + QVERIFY(tagsView); + tagDeleteButton = widget->findChild(QStringLiteral("tagDeleteButton")); + QVERIFY(tagDeleteButton); + QVERIFY(!tagDeleteButton->isVisible()); + + valid = true; + } + + ~TestSetup() + { + if (!createdTags.empty()) { + auto *deleteJob = new TagDeleteJob(createdTags); + AKVERIFYEXEC(deleteJob); + } + } + + bool createTags(int count) + { + const auto doCreateTags = [this, count]() { + QSignalSpy monitorSpy(monitor.get(), &Monitor::tagAdded); + for (int i = 0; i < count; ++i) { + auto *job = new TagCreateJob(Tag(QStringLiteral("TestTag-%1").arg(i))); + AKVERIFYEXEC(job); + createdTags.push_back(job->tag()); + } + QTRY_COMPARE(monitorSpy.count(), count); + }; + doCreateTags(); + return createdTags.size() == count; + } + + bool checkSelectionIsEmpty() const + { + auto *const model = tagsView->model(); + for (int i = 0; i < model->rowCount(); ++i) { + if (model->data(model->index(i, 0), Qt::CheckStateRole).value() != Qt::Unchecked) { + return false; + } + } + return true; + } + + QModelIndex indexForTag(const Tag &tag) const + { + for (int i = 0; i < tagsView->model()->rowCount(); ++i) { + const auto index = tagsView->model()->index(i, 0); + if (tagsView->model()->data(index, TagModel::TagRole).value() == tag) { + return index; + } + } + return {}; + } + + bool deleteTag(const Tag &tag, bool confirmDeletion) + { + const auto index = indexForTag(tag); + AKVERIFY(index.isValid()); + const auto itemRect = tagsView->visualRect(index); + // Hover over the item and confirm the button is there + QTest::mouseMove(tagsView->viewport(), itemRect.center()); + AKVERIFY(QTest::qWaitFor(std::bind(&QWidget::isVisible, tagDeleteButton))); + AKVERIFY(tagDeleteButton->geometry().intersects(itemRect)); + + // Clicking the button blocks (QDialog::exec), so we need to confirm the + // dialog from event loop + bool confirmed = false; + QTimer::singleShot(100, [this, confirmDeletion, &confirmed]() { + confirmed = confirmDialog(confirmDeletion); + QVERIFY(confirmed); + }); + QTest::mouseClick(tagDeleteButton, Qt::LeftButton); + + // Check that the confirmation was succesful + AKVERIFY(confirmed); + + return true; + } + + bool confirmDialog(bool confirmDeletion) + { + const auto windows = QApplication::topLevelWidgets(); + for (const auto *window : windows) { + // We are using KMessageBox, which is not a QMessageBox but rather a custom QDialog + if (window->objectName() == QLatin1String("questionYesNo")) { + const auto *const msgbox = qobject_cast(window); + AKVERIFY(msgbox); + + const auto *const buttonBox = msgbox->findChild(); + AKVERIFY(buttonBox); + auto *const button = buttonBox->button(confirmDeletion ? QDialogButtonBox::Yes : QDialogButtonBox::No); + AKVERIFY(button); + QTest::mouseClick(button, Qt::LeftButton); + return true; + } + } + + return false; + } + + std::unique_ptr monitor; + std::unique_ptr model; + std::unique_ptr widget; + + QLineEdit *newTagEdit = nullptr; + QPushButton *newTagButton = nullptr; + QListView *tagsView = nullptr; + QPushButton *tagDeleteButton = nullptr; + + Tag::List createdTags; + + bool valid = false; + }; + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testTagCreationWithEnter() + { + const auto tagName = QStringLiteral("TagEditWidgetTestTag"); + + TestSetup test; + QVERIFY(test.valid); + + QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagAdded); + + QTest::keyClicks(test.newTagEdit, tagName); + QVERIFY(test.newTagButton->isEnabled()); + QTest::keyClick(test.newTagEdit, Qt::Key_Return); + QVERIFY(!test.newTagButton->isEnabled()); + QVERIFY(!test.newTagEdit->isEnabled()); + + QTRY_COMPARE(monitorSpy.size(), 1); + test.createdTags.push_back(monitorSpy.at(0).at(0).value()); + QCOMPARE(test.model->rowCount(), 2); + QCOMPARE(test.model->data(test.model->index(1, 0), Qt::DisplayRole).toString(), tagName); + } + + void testTagCreationWithButton() + { + const auto tagName = QStringLiteral("TagEditWidgetTestTag"); + + TestSetup test; + QVERIFY(test.valid); + + QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagAdded); + + QTest::keyClicks(test.newTagEdit, tagName); + QVERIFY(test.newTagButton->isEnabled()); + QTest::mouseClick(test.newTagButton, Qt::LeftButton); + QVERIFY(!test.newTagButton->isEnabled()); + QVERIFY(!test.newTagEdit->isEnabled()); + + QTRY_COMPARE(monitorSpy.size(), 1); + test.createdTags.push_back(monitorSpy.at(0).at(0).value()); + QCOMPARE(test.model->rowCount(), 2); + QCOMPARE(test.model->data(test.model->index(1, 0), Qt::DisplayRole).toString(), tagName); + } + + void testDuplicatedTagCannotBeCreated() + { + TestSetup test; + QVERIFY(test.valid); + + // Create a tag + QVERIFY(test.createTags(1)); + + // Wait for the tag to appear in the model + QTRY_COMPARE(test.model->rowCount(), 2); + + // Type the entire string char-by-char - once the name is full the button + // should be disabled because we don't allow creating duplicated tags + const auto tagName = test.createdTags.front().name(); + for (int i = 0; i < tagName.size(); ++i) { + QTest::keyClicks(test.newTagEdit, tagName[i]); + QCOMPARE(test.newTagButton->isEnabled(), i != tagName.size() - 1); + } + } + + void testSettingSelectionFromCode() + { + TestSetup test; + QVERIFY(test.valid); + QVERIFY(test.createTags(10)); + + test.widget->setSelectionEnabled(true); + + // Nothing should be checked + QVERIFY(test.checkSelectionIsEmpty()); + + // Set selection + auto *model = test.tagsView->model(); + Tag::List selectTags; + for (int i = 0; i < model->rowCount(); i += 2) { + selectTags.push_back(model->data(model->index(i, 0), TagModel::TagRole).value()); + } + QVERIFY(!selectTags.empty()); + test.widget->setSelection(selectTags); + QCOMPARE(test.widget->selection(), selectTags); + + // Confirm that the items are visually selected + for (int i = 0; i < model->rowCount(); ++i) { + const auto tag = model->data(model->index(i, 0), TagModel::TagRole).value(); + const auto expectedState = selectTags.contains(tag) ? Qt::Checked : Qt::Unchecked; + QCOMPARE(model->data(model->index(i, 0), Qt::CheckStateRole).value(), expectedState); + } + } + + void testSelectingTagsByMouse() + { + TestSetup test; + QVERIFY(test.valid); + QVERIFY(test.createTags(10)); + + test.widget->setSelectionEnabled(true); + + // Nothing should be checked + QVERIFY(test.checkSelectionIsEmpty()); + + // Check several tags + Tag::List selectedTags; + auto *model = test.tagsView->model(); + for (int i = 0; i < model->rowCount(); i += 2) { + const auto index = model->index(i, 0); + selectedTags.push_back(model->data(index, TagModel::TagRole).value()); + // Select the row + QTest::mouseClick(test.tagsView->viewport(), Qt::LeftButton, {}, test.tagsView->visualRect(index).topLeft() + QPoint(5, 5)); + // Use spacebar to toggle selection, we can't possibly hit the checkbox with mouse in a + // reliable manner. + QTest::keyClick(test.tagsView, Qt::Key_Space); + } + + // Confirm that the selection occured + for (int i = 0; i < model->rowCount(); ++i) { + const auto expectedState = i % 2 == 0 ? Qt::Checked : Qt::Unchecked; + QCOMPARE(model->data(model->index(i, 0), Qt::CheckStateRole).value(), expectedState); + } + + // Compare the selectede tags + auto currentSelection = test.widget->selection(); + const auto sortTag = [](const Tag &l, const Tag &r) { + return l.id() < r.id(); + }; + std::sort(currentSelection.begin(), currentSelection.end(), sortTag); + std::sort(selectedTags.begin(), selectedTags.end(), sortTag); + QCOMPARE(currentSelection, selectedTags); + } + + void testDeletingTags() + { + TestSetup test; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + while (!test.createdTags.empty()) { + QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagRemoved); + // Get the last tag in the list and delete it + const auto tag = test.createdTags.last(); + QVERIFY(test.deleteTag(tag, true)); + + // Wait for confirmation + QTRY_COMPARE(monitorSpy.size(), 1); + QCOMPARE(monitorSpy.at(0).at(0).value(), tag); + + test.createdTags.pop_back(); // remove the tag from the list + } + + // Verify that we've deleted everything + QVERIFY(test.createdTags.empty()); + QCOMPARE(test.model->rowCount(), 1); // only the default tag remains + } + + void testRejectingDeleteDialogDoesntDeleteTheTAg() + { + TestSetup test; + QVERIFY(test.valid); + + QSignalSpy monitorSpy(test.monitor.get(), &Monitor::tagRemoved); + const auto index = test.model->index(0, 0); + QVERIFY(index.isValid()); + + const auto tag = test.model->data(index, TagModel::TagRole).value(); + QVERIFY(test.deleteTag(tag, false)); + + QTest::qWait(500); // wait some amount of time to see that nothing has changed... + QVERIFY(monitorSpy.empty()); + QCOMPARE(test.model->rowCount(), 1); + } +}; + +QTEST_AKONADIMAIN(TagEditWidgetTest) + +#include "tageditwidgettest.moc" diff --git a/autotests/widgets/tagselectioncomboboxtest.cpp b/autotests/widgets/tagselectioncomboboxtest.cpp new file mode 100644 index 0000000..67102f1 --- /dev/null +++ b/autotests/widgets/tagselectioncomboboxtest.cpp @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2020 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "qtest_akonadi.h" +#include + +#include "monitor.h" +#include "tag.h" +#include "tagcreatejob.h" +#include "tagdeletejob.h" +#include "tagmodel.h" +#include "tagselectioncombobox.h" + +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +class TagSelectionComboBoxTest : public QObject +{ + Q_OBJECT + + struct TestSetup { + explicit TestSetup(bool checkable) + { + widget = std::make_unique(); + widget->setCheckable(checkable); + widget->show(); + + monitor = widget->findChild(); + QVERIFY(monitor); + model = widget->findChild(); + QVERIFY(model); + QSignalSpy modelSpy(model, &TagModel::populated); + QVERIFY(modelSpy.wait()); + + QVERIFY(QTest::qWaitForWindowActive(widget.get())); + + valid = true; + } + + ~TestSetup() + { + if (!createdTags.empty()) { + auto deleteJob = new TagDeleteJob(createdTags); + AKVERIFYEXEC(deleteJob); + } + } + + bool createTags(int count) + { + const auto doCreateTags = [this, count]() { + QSignalSpy monitorSpy(monitor, &Monitor::tagAdded); + for (int i = 0; i < count; ++i) { + auto job = new TagCreateJob(Tag(QStringLiteral("TestTag-%1").arg(i))); + AKVERIFYEXEC(job); + createdTags.push_back(job->tag()); + } + QTRY_COMPARE(monitorSpy.count(), count); + }; + doCreateTags(); + return createdTags.size() == count; + } + + bool testSelectionMatches(QSignalSpy &selectionSpy, const Tag::List &selection) const + { + QStringList names; + std::transform(selection.begin(), selection.end(), std::back_inserter(names), std::bind(&Tag::name, std::placeholders::_1)); + + AKCOMPARE(widget->selection(), selection); + AKCOMPARE(widget->selectionNames(), names); + AKCOMPARE(selectionSpy.size(), 1); + + AKCOMPARE(selectionSpy.at(0).at(0).value(), selection); + AKCOMPARE(widget->currentText(), QLocale{}.createSeparatedList(names)); + return true; + } + + bool selectTagsInComboBox(const Tag::List & /*selection*/) + { + const auto windows = QApplication::topLevelWidgets(); + for (auto window : windows) { + if (auto combo = qobject_cast(window)) { + QTest::mouseClick(combo, Qt::LeftButton); + return true; + } + } + + return false; + } + + bool toggleDropdown() const + { + auto view = widget->view()->parentWidget(); + const bool visible = view->isVisible(); + QTest::mouseClick(widget->lineEdit(), Qt::LeftButton); + QTest::qWait(10); + AKCOMPARE(view->isVisible(), !visible); + + return true; + } + + QModelIndex indexForTag(const Tag &tag) const + { + for (int i = 0; i < widget->model()->rowCount(); ++i) { + const auto index = widget->model()->index(i, 0); + if (widget->model()->data(index, TagModel::TagRole).value().name() == tag.name()) { + return index; + } + } + return {}; + } + + std::unique_ptr widget; + Monitor *monitor = nullptr; + TagModel *model = nullptr; + + Tag::List createdTags; + + bool valid = false; + }; + +public: + TagSelectionComboBoxTest() + { + qRegisterMetaType(); + } + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testInitialState() + { + TestSetup test{true}; + QVERIFY(test.valid); + + QVERIFY(test.widget->currentText().isEmpty()); + QVERIFY(test.widget->selection().isEmpty()); + } + + void testSettingSelectionFromCode() + { + TestSetup test{true}; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + QSignalSpy selectionSpy(test.widget.get(), &TagSelectionComboBox::selectionChanged); + const auto selection = Tag::List{test.createdTags[1], test.createdTags[3]}; + test.widget->setSelection(selection); + + QVERIFY(test.testSelectionMatches(selectionSpy, selection)); + } + + void testSettingSelectionByName() + { + TestSetup test{true}; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + QSignalSpy selectionSpy(test.widget.get(), &TagSelectionComboBox::selectionChanged); + const auto selection = QStringList{test.createdTags[1].name(), test.createdTags[3].name()}; + test.widget->setSelection(selection); + + QVERIFY(test.testSelectionMatches(selectionSpy, {test.createdTags[1], test.createdTags[3]})); + } + + void testSelectionByKeyboard() + { + TestSetup test{true}; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + QSignalSpy selectionSpy(test.widget.get(), &TagSelectionComboBox::selectionChanged); + const auto selection = Tag::List{test.createdTags[1], test.createdTags[3]}; + + QVERIFY(!test.widget->view()->parentWidget()->isVisible()); + QVERIFY(test.toggleDropdown()); + + QTest::keyClick(test.widget->view(), Qt::Key_Down); // from name to tag 1 + QTest::keyClick(test.widget->view(), Qt::Key_Down); // from tag 1 to tag 2 + QTest::keyClick(test.widget->view(), Qt::Key_Space); // select tag 2 + QTest::keyClick(test.widget->view(), Qt::Key_Down); // from tag 2 to tag 3 + QTest::keyClick(test.widget->view(), Qt::Key_Down); // from tag 3 to tag 4 + QTest::keyClick(test.widget->view(), Qt::Key_Space); // select tag 4 + + QTest::keyClick(test.widget->view(), Qt::Key_Escape); // close + QTest::qWait(100); + QVERIFY(!test.widget->view()->parentWidget()->isVisible()); + + QCOMPARE(selectionSpy.size(), 2); // two selections -> two signals + selectionSpy.takeFirst(); // remove the first one + QVERIFY(test.testSelectionMatches(selectionSpy, selection)); + } + + void testNonCheckableSelection() + { + TestSetup test{false}; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + test.widget->setCurrentIndex(1); + QCOMPARE(test.widget->currentData(TagModel::TagRole).value(), test.createdTags[0]); + + QCOMPARE(test.widget->selection(), Tag::List{test.createdTags[0]}); + QCOMPARE(test.widget->selectionNames(), QStringList{test.createdTags[0].name()}); + + test.widget->setSelection({test.createdTags[1]}); + QCOMPARE(test.widget->currentIndex(), 2); + } +}; + +QTEST_AKONADIMAIN(TagSelectionComboBoxTest) + +#include "tagselectioncomboboxtest.moc" diff --git a/autotests/widgets/tagwidgettest.cpp b/autotests/widgets/tagwidgettest.cpp new file mode 100644 index 0000000..bcbebc4 --- /dev/null +++ b/autotests/widgets/tagwidgettest.cpp @@ -0,0 +1,195 @@ +/* + * SPDX-FileCopyrightText: 2020 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "qtest_akonadi.h" +#include + +#include "monitor.h" +#include "tag.h" +#include "tagcreatejob.h" +#include "tagdeletejob.h" +#include "tagmodel.h" +#include "tagselectiondialog.h" +#include "tagwidget.h" + +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +class TagWidgetTest : public QObject +{ + Q_OBJECT + + struct TestSetup { + TestSetup() + { + widget = std::make_unique(); + widget->show(); + + monitor = widget->findChild(); + QVERIFY(monitor); + model = widget->findChild(); + QVERIFY(model); + QSignalSpy modelSpy(model, &TagModel::populated); + QVERIFY(modelSpy.wait()); + + QVERIFY(QTest::qWaitForWindowActive(widget.get())); + + tagView = widget->findChild(QStringLiteral("tagView")); + QVERIFY(tagView); + QVERIFY(tagView->isReadOnly()); // always read-only + editButton = widget->findChild(QStringLiteral("editButton")); + QVERIFY(editButton); + + valid = true; + } + + ~TestSetup() + { + if (!createdTags.empty()) { + auto deleteJob = new TagDeleteJob(createdTags); + AKVERIFYEXEC(deleteJob); + } + } + + bool createTags(int count) + { + const auto doCreateTags = [this, count]() { + QSignalSpy monitorSpy(monitor, &Monitor::tagAdded); + for (int i = 0; i < count; ++i) { + auto job = new TagCreateJob(Tag(QStringLiteral("TestTag-%1").arg(i))); + AKVERIFYEXEC(job); + createdTags.push_back(job->tag()); + } + QTRY_COMPARE(monitorSpy.count(), count); + }; + doCreateTags(); + return createdTags.size() == count; + } + + bool testSelectionMatches(QSignalSpy &selectionSpy, const Tag::List &selection) const + { + QStringList names; + std::transform(selection.begin(), selection.end(), std::back_inserter(names), std::bind(&Tag::name, std::placeholders::_1)); + + AKCOMPARE(widget->selection(), selection); + AKCOMPARE(selectionSpy.size(), 1); + + AKCOMPARE(selectionSpy.at(0).at(0).value(), selection); + AKCOMPARE(tagView->text(), names.join(QStringLiteral(", "))); + return true; + } + + bool selectTagsInDialog(const Tag::List &selection) + { + const auto windows = QApplication::topLevelWidgets(); + for (auto window : windows) { + if (auto dlg = qobject_cast(window)) { + // Set the selection through code, testing selecting tags with mouse is + // out-of-scope for this test, there's a dedicated TagEditWidget test for that. + dlg->setSelection(selection); + auto button = dlg->buttons()->button(QDialogButtonBox::Ok); + AKVERIFY(button); + QTest::mouseClick(button, Qt::LeftButton); + return true; + } + } + + return false; + } + + std::unique_ptr widget; + Monitor *monitor = nullptr; + TagModel *model = nullptr; + + QLineEdit *tagView = nullptr; + QToolButton *editButton = nullptr; + + Tag::List createdTags; + + bool valid = false; + }; + +private Q_SLOTS: + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testInitialState() + { + TestSetup test; + QVERIFY(test.valid); + + QVERIFY(test.tagView->text().isEmpty()); + QVERIFY(test.widget->selection().isEmpty()); + } + + void testSettingSelectionFromCode() + { + TestSetup test; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + QSignalSpy selectionSpy(test.widget.get(), &TagWidget::selectionChanged); + const auto selection = Tag::List{test.createdTags[1], test.createdTags[3]}; + test.widget->setSelection(selection); + + QVERIFY(test.testSelectionMatches(selectionSpy, selection)); + } + + void testSettingSelectionViaDialog() + { + TestSetup test; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + QSignalSpy selectionSpy(test.widget.get(), &TagWidget::selectionChanged); + const auto selection = Tag::List{test.createdTags[1], test.createdTags[3]}; + + bool ok = false; + // Clicking on the Edit button opens the dialog in a blocking way, so + // we need to dispatch the test from event loop + QTimer::singleShot(100, this, [&test, &selection, &ok]() { + QVERIFY(test.selectTagsInDialog(selection)); + ok = true; + }); + QTest::mouseClick(test.editButton, Qt::LeftButton); + QVERIFY(ok); + + QVERIFY(test.testSelectionMatches(selectionSpy, selection)); + } + + void testClearTagsFromCode() + { + TestSetup test; + QVERIFY(test.valid); + QVERIFY(test.createTags(4)); + + const auto selection = Tag::List{test.createdTags[1], test.createdTags[3]}; + test.widget->setSelection(selection); + QCOMPARE(test.widget->selection(), selection); + + QSignalSpy selectionSpy(test.widget.get(), &TagWidget::selectionChanged); + test.widget->clearTags(); + QVERIFY(test.widget->selection().isEmpty()); + QCOMPARE(selectionSpy.size(), 1); + QVERIFY(selectionSpy.at(0).at(0).value().empty()); + QVERIFY(test.tagView->text().isEmpty()); + } +}; + +QTEST_AKONADIMAIN(TagWidgetTest) + +#include "tagwidgettest.moc" diff --git a/autotests/widgets/unittestenv/config.xml b/autotests/widgets/unittestenv/config.xml new file mode 100644 index 0000000..2b5dd0b --- /dev/null +++ b/autotests/widgets/unittestenv/config.xml @@ -0,0 +1,7 @@ + + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + diff --git a/autotests/widgets/unittestenv/xdgconfig/akonadi-firstrunrc b/autotests/widgets/unittestenv/xdgconfig/akonadi-firstrunrc new file mode 100644 index 0000000..c5e90d8 --- /dev/null +++ b/autotests/widgets/unittestenv/xdgconfig/akonadi-firstrunrc @@ -0,0 +1,4 @@ +[ProcessedDefaults] +defaultaddressbook=done +defaultcalendar=done +defaultnotebook=done diff --git a/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_0rc b/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_0rc new file mode 100644 index 0000000..0d9e3cf --- /dev/null +++ b/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_0rc @@ -0,0 +1,4 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res1.xml +FileWatchingEnabled=false + diff --git a/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_1rc b/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_1rc new file mode 100644 index 0000000..87df3c6 --- /dev/null +++ b/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_1rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res2.xml +FileWatchingEnabled=false diff --git a/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_2rc b/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_2rc new file mode 100644 index 0000000..274fbfc --- /dev/null +++ b/autotests/widgets/unittestenv/xdgconfig/akonadi_knut_resource_2rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res3.xml +FileWatchingEnabled=false diff --git a/autotests/widgets/unittestenv/xdglocal/testdata-res1.xml b/autotests/widgets/unittestenv/xdglocal/testdata-res1.xml new file mode 100644 index 0000000..db51834 --- /dev/null +++ b/autotests/widgets/unittestenv/xdglocal/testdata-res1.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + testmailbody + From: <test@user.tst> + \SEEN + \FLAGGED + \DRAFT + + + testmailbody1 + From: <test1@user.tst> + \FLAGGED + tagrid + + + testmailbody2 + From: <test2@user.tst> + + + testmailbody3 + From: <test3@user.tst> + + + testmailbody4 + From: <test4@user.tst> + + + testmailbody5 + From: <test5@user.tst> + + + testmailbody6 + From: <test6@user.tst> + + + testmailbody7 + From: <test7@user.tst> + + + testmailbody8 + From: <test8@user.tst> + + + testmailbody9 + From: <test9@user.tst> + + + testmailbody10 + From: <test10@user.tst> + + + testmailbody11 + From: <test11@user.tst> + + + testmailbody12 + From: <test12@user.tst> + + + testmailbody13 + From: <test13@user.tst> + + + testmailbody14 + From: <test14@user.tst> + + + + + diff --git a/autotests/widgets/unittestenv/xdglocal/testdata-res2.xml b/autotests/widgets/unittestenv/xdglocal/testdata-res2.xml new file mode 100644 index 0000000..b12f3b3 --- /dev/null +++ b/autotests/widgets/unittestenv/xdglocal/testdata-res2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/autotests/widgets/unittestenv/xdglocal/testdata-res3.xml b/autotests/widgets/unittestenv/xdglocal/testdata-res3.xml new file mode 100644 index 0000000..0c3b7a8 --- /dev/null +++ b/autotests/widgets/unittestenv/xdglocal/testdata-res3.xml @@ -0,0 +1,4 @@ + + + + diff --git a/cmake/modules/AkonadiMacros.cmake b/cmake/modules/AkonadiMacros.cmake new file mode 100644 index 0000000..7737364 --- /dev/null +++ b/cmake/modules/AkonadiMacros.cmake @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: BSD-3-Clause + +# Internal server macros +function(akonadi_run_xsltproc) + if (NOT XSLTPROC_EXECUTABLE) + message(FATAL_ERROR "xsltproc executable not found but needed by AKONADI_RUN_XSLTPROC()") + endif() + + set(options ) + set(oneValueArgs XSL XML CLASSNAME BASENAME) + set(multiValueArgs DEPENDS) + cmake_parse_arguments(XSLT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if (NOT XSLT_XSL) + message(FATAL_ERROR "Required argument XSL missing in AKONADI_RUN_XSLTPROC() call") + endif() + if (NOT XSLT_XML) + message(FATAL_ERROR "Required argument XML missing in AKONADI_RUN_XSLTPROC() call") + endif() + if (NOT XSLT_BASENAME) + message(FATAL_ERROR "Required argument BASENAME missing in AKONADI_RUN_XSLTPROC() call") + endif() + + # Workaround xsltproc struggling with spaces in filepaths on Windows + file(RELATIVE_PATH xsl_relpath ${CMAKE_CURRENT_BINARY_DIR} ${XSLT_XSL}) + file(RELATIVE_PATH xml_relpath ${CMAKE_CURRENT_BINARY_DIR} ${XSLT_XML}) + + set(extra_params ) + if (XSLT_CLASSNAME) + set(extra_params --stringparam className ${XSLT_CLASSNAME}) + endif() + + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${XSLT_BASENAME}.h + ${CMAKE_CURRENT_BINARY_DIR}/${XSLT_BASENAME}.cpp + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${XSLT_BASENAME}.h + --stringparam code header + --stringparam fileName ${XSLT_BASENAME} + ${extra_params} + ${xsl_relpath} + ${xml_relpath} + COMMAND ${XSLTPROC_EXECUTABLE} + --output ${XSLT_BASENAME}.cpp + --stringparam code source + --stringparam fileName ${XSLT_BASENAME} + ${extra_params} + ${xsl_relpath} + ${xml_relpath} + DEPENDS ${XSLT_XSL} + ${XSLT_XML} + ${XSLT_DEPENDS} + ) + + set_property(SOURCE + ${CMAKE_CURRENT_BINARY_DIR}/${XSLT_BASENAME}.cpp + ${CMAKE_CURRENT_BINARY_DIR}/${XSLT_BASENAME}.h + PROPERTY SKIP_AUTOMOC TRUE + ) +endfunction() + +macro(akonadi_generate_schema _schemaXml _className _fileBaseName) + if (NOT XSLTPROC_EXECUTABLE) + message(FATAL_ERROR "xsltproc executable not found but needed by AKONADI_GENERATE_SCHEMA()") + endif() + + akonadi_run_xsltproc( + XSL ${Akonadi_SOURCE_DIR}/src/server/storage/schema.xsl + XML ${_schemaXml} + CLASSNAME ${_className} + BASENAME ${_fileBaseName} + ) +endmacro() + +function(akonadi_add_xmllint_test) + if (NOT XMLLINT_EXECUTABLE) + message(FATAL_ERROR "xmllint executable not found but needed by AKONADI_ADD_XMLLINT_SCHEMA()") + endif() + + set(options ) + set(oneValueArgs XML XSD) + set(multiValueArgs ) + cmake_parse_arguments(TEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + file(RELATIVE_PATH xsd_relpath ${CMAKE_CURRENT_BINARY_DIR} ${TEST_XSD}) + file(RELATIVE_PATH xml_relpath ${CMAKE_CURRENT_BINARY_DIR} ${TEST_XML}) + add_test(${TEST_UNPARSED_ARGUMENTS} ${XMLLINT_EXECUTABLE} --noout --schema ${xsd_relpath} ${xml_relpath}) +endfunction() diff --git a/cmake/modules/FindSqlite.cmake b/cmake/modules/FindSqlite.cmake new file mode 100644 index 0000000..2e1716e --- /dev/null +++ b/cmake/modules/FindSqlite.cmake @@ -0,0 +1,118 @@ +# +# - Try to find Sqlite +# Once done this will define +# +# SQLITE_FOUND - system has Sqlite +# SQLITE_INCLUDE_DIR - the Sqlite include directory +# SQLITE_LIBRARIES - Link these to use Sqlite +# SQLITE_MIN_VERSION - The minimum SQLite version +# +# SPDX-FileCopyrightText: 2008 Gilles Caulier +# SPDX-FileCopyrightText: 2010 Christophe Giboudeaux +# SPDX-FileCopyrightText: 2014 Daniel Vrátil +# +# SPDX-License-Identifier: BSD-3-Clause +# +if(NOT SQLITE_MIN_VERSION) + set(SQLITE_MIN_VERSION "3.6.16") +endif(NOT SQLITE_MIN_VERSION) + +if ( SQLITE_INCLUDE_DIR AND SQLITE_LIBRARIES ) + # in cache already + SET(Sqlite_FIND_QUIETLY TRUE) +endif ( SQLITE_INCLUDE_DIR AND SQLITE_LIBRARIES ) + +# use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +if( NOT WIN32 ) + find_package(PkgConfig) + + pkg_check_modules(PC_SQLITE sqlite3) + + set(SQLITE_DEFINITIONS ${PC_SQLITE_CFLAGS_OTHER}) +endif( NOT WIN32 ) + +if(PC_SQLITE_FOUND) + find_path(SQLITE_INCLUDE_DIR + NAMES sqlite3.h + PATHS ${PC_SQLITE_INCLUDEDIR} + NO_DEFAULT_PATH + ) + + find_library(SQLITE_LIBRARIES + NAMES sqlite3 + PATHS ${PC_SQLITE_LIBDIR} + NO_DEFAULT_PATH + ) +else(PC_SQLITE_FOUND) + find_path(SQLITE_INCLUDE_DIR + NAMES sqlite3.h + ) + + find_library(SQLITE_LIBRARIES + NAMES sqlite3 + ) +endif(PC_SQLITE_FOUND) + +if( UNIX ) + find_file(SQLITE_STATIC_LIBRARIES + libsqlite3.a + ${PC_SQLITE_LIBDIR} + ) +else( UNIX ) + # todo find static libs for other systems + # fallback to standard libs + set( SQLITE_STATIC_LIBRARIES ${SQLITE_LIBRARIES} ) +endif( UNIX ) + +if(EXISTS ${SQLITE_INCLUDE_DIR}/sqlite3.h) + file(READ ${SQLITE_INCLUDE_DIR}/sqlite3.h SQLITE3_H_CONTENT) + string(REGEX MATCH "SQLITE_VERSION[ ]*\"[0-9.]*\"\n" SQLITE_VERSION_MATCH "${SQLITE3_H_CONTENT}") + + if(SQLITE_VERSION_MATCH) + string(REGEX REPLACE ".*SQLITE_VERSION[ ]*\"(.*)\"\n" "\\1" SQLITE_VERSION ${SQLITE_VERSION_MATCH}) + + if(SQLITE_VERSION VERSION_LESS "${SQLITE_MIN_VERSION}") + message(STATUS "Sqlite ${SQLITE_VERSION} was found, but at least version ${SQLITE_MIN_VERSION} is required") + set(SQLITE_VERSION_OK FALSE) + else(SQLITE_VERSION VERSION_LESS "${SQLITE_MIN_VERSION}") + set(SQLITE_VERSION_OK TRUE) + endif(SQLITE_VERSION VERSION_LESS "${SQLITE_MIN_VERSION}") + + endif(SQLITE_VERSION_MATCH) + + if (SQLITE_VERSION_OK) + file(WRITE ${CMAKE_BINARY_DIR}/sqlite_check_unlock_notify.cpp + "#include + int main(int argc, char **argv) { + return sqlite3_unlock_notify(0, 0, 0); + }") + try_compile(SQLITE_HAS_UNLOCK_NOTIFY + ${CMAKE_BINARY_DIR}/sqlite_check_unlock_notify + ${CMAKE_BINARY_DIR}/sqlite_check_unlock_notify.cpp + LINK_LIBRARIES ${SQLITE_LIBRARIES} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES:PATH=${SQLITE_INCLUDE_DIR}") + if (NOT SQLITE_HAS_UNLOCK_NOTIFY) + message(STATUS "Sqlite ${SQLITE_VERSION} was found, but it is not compiled with -DSQLITE_ENABLE_UNLOCK_NOTIFY") + endif() + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( Sqlite DEFAULT_MSG + SQLITE_INCLUDE_DIR + SQLITE_LIBRARIES + SQLITE_VERSION_OK + SQLITE_HAS_UNLOCK_NOTIFY) + +if(Sqlite_FOUND AND NOT TARGET Sqlite::Sqlite) + add_library(Sqlite::Sqlite UNKNOWN IMPORTED) + set_target_properties(Sqlite::Sqlite PROPERTIES + IMPORTED_LOCATION "${SQLITE_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${SQLITE_INCLUDE_DIR}" + ) +endif() + +# show the SQLITE_INCLUDE_DIR and SQLITE_LIBRARIES variables only in the advanced view +mark_as_advanced( SQLITE_INCLUDE_DIR SQLITE_LIBRARIES ) + diff --git a/config-akonadi.h.cmake b/config-akonadi.h.cmake new file mode 100644 index 0000000..9063ec7 --- /dev/null +++ b/config-akonadi.h.cmake @@ -0,0 +1,9 @@ +#cmakedefine HAVE_UNISTD_H 1 + +#cmakedefine HAVE_MALLOC_TRIM 1 + +#define AKONADI_DATABASE_BACKEND "@AKONADI_DATABASE_BACKEND@" + +#cmakedefine WITH_3RDPARTY_OPTIONAL 1 + +#cmakedefine WITH_ACCOUNTS 1 diff --git a/docs/client_libraries.md b/docs/client_libraries.md new file mode 100644 index 0000000..f80aa75 --- /dev/null +++ b/docs/client_libraries.md @@ -0,0 +1,684 @@ +# Akonadi client libraries # {#client_libraries} + +[TOC] + +Akonadi client libraries consist of three libraries that provide tools to access +the Akonadi PIM data server: AkonadiCore, AkonadiWidgets and AkonadiAgentBase. +All processes accessing Akonadi, including those which communicate with a remote +server [agents](@ref agents), are considered clients. + + + +# Akonadi Objects # {#objects} + +Akonadi works on two basic object types: collections and items. + +Collections are comparable to folders in a file system and are represented by +the class Akonadi::Collection. Every collection has an associated cache policy +represented by the class Akonadi::CachePolicy which defines what part of its +content is cached for how long. All available ways to work with collections are +listed in the "[Collections](#collections)" section. + +Akonadi items are comparable to files in a file system and are represented by +the class Akonadi::Item. Each item represents a single PIM object such as a mail +or a contact. The actual object it represents is its so-called payload. All +available ways to work with items are listed in the "[Items](#items)" +section. + +Both items and collections are identified by a persistent unique identifier. +Also, they can contain arbitrary attributes (derived from Akonadi::Attribute) to +attach general or application specific meta data to them. + +# Collection retrieval and manipulation # {#collections} + +A collection is represented by the Akonadi::Collection class. + +Classes to retrieve information about collections: + +* Akonadi::CollectionFetchJob +* Akonadi::CollectionStatisticsJob + +Classes to manipulate collections: + +* Akonadi::CollectionCreateJob +* Akonadi::CollectionCopyJob +* Akonadi::CollectionModifyJob +* Akonadi::CollectionDeleteJob + +There is also Akonadi::CollectionModel, which is a self-updating model class which can +be used in combination with Akonadi::CollectionView. Akonadi::CollectionFilterProxyModel +can be used to limit a displayed collection tree to collections supporting a certain +type of PIM items. Akonadi::CollectionPropertiesDialog provides an extensible properties +dialog for collections. Often needed QAction for collection operations are provided by +Akonadi::StandardActionManager. + +# PIM item retrieval and manipulation # {#items} + +PIM items are represented by classes derived from Akonadi::Item. +Items can be retrieved using Akonadi::ItemFetchJob. + +The following classes are provided to manipulate PIM items: + +* Akonadi::ItemCreateJob +* Akonadi::ItemCopyJob +* Akonadi::ItemModifyJob +* Akonadi::ItemDeleteJob + +Akonadi::ItemModel provides a self-updating model class which can be used to display the content +of a collection. Akonadi::ItemView is the base class for a corresponding view. Often needed QAction +for item operations are provided by Akonadi::StandardActionManager. + +# Low-level access to the Akonadi server # {#jobs} + +Accessing the Akonadi server is done using job classes derived from Akonadi::Job. The +communication channel with the server is provided by Akonadi::Session. + +To use server-side transactions, the following jobs are provided: + +* Akonadi::TransactionBeginJob +* Akonadi::TransactionCommitJob +* Akonadi::TransactionRollbackJob + +There also is Akonadi::TransactionSequence which can be used to automatically group +a set of jobs into a single transaction. + + +# Change notifications (Monitor) # {#monitor} + +The Akonadi::Monitor class allows you to monitor specific resources, +collections and PIM items for changes. Akonadi::ChangeRecorder augments this +by providing a way to record and replay change notifications. + +# PIM item serializer # {#serializer} + +The class Akonadi::ItemSerializer is responsible for converting between the stored (binary) representation +of a PIM item and the objects used to handle these items provided by the corresponding libraries (kabc, kcal, etc.). + +Serializer plugins allow you to add support for new kinds of PIM items to Akonadi. +Akonadi::ItemSerializerPlugin can be used as a base class for such a plugin. + +# Agents and Resources # {#resource} + +Agents are independent processes that watch the Akonadi store for changes and react to them if necessary. +Example: The Akonadi Indexing Agent is an agent that watches Akonadi for new emails, calendar events, etc., +retrieves the new items and indexes them into a special database. + +The class Akonadi::AgentBase is the common base class for all agents. It provides commonly needed +functionality such as change monitoring and recording. + +Resources are a special kind of agents. They are used as the actual backend for whatever data the resource represents. +In this situation the akonadi server acts more like a proxy service. It caches data on behalf of its clients +(client here being the Resource), not permanently storing it. The Akonadi server forwards item retrieval requests to the +corresponding resource, if the item is not in the cache. +Example: The imap resource is responsible for storing and fetching emails from an imap server. + +Akonadi::ResourceBase is the base class for them. It provides the +necessary interfaces to the server as well as many convenience functions to make implementing +a new resource as easy as possible. Note that a collection contains items belonging to a single +resource, although there are plans in the future for 'virtual' collections which will contain +the results of a search query spanning multiple resources. + +A resource can support multiple mimetypes. There are two places where a resource can specify +mimetypes: in its desktop files, and in the content mimetypes field of +collections created by it. The ones in the desktop file are used for +filtering agent types, e.g. in the resource creation dialogs. The collection +content mimetypes specify what you can actually put into a collection, which +is not necessarily the same (e.g. the Kolab resource supports contacts and events, but not +in the same folder). + + +# Integration in your Application # {#integration} + +Akonadi::Control provides ways to ensure that the Akonadi server is running, to monitor its availability +and provide help on server-side errors. A more low-level interface to the Akonadi server is provided +by Akonadi::ServerManager. + +A set of standard actions is provided by Akonadi::StandardActionManager. These provide consistent +look and feel across applications. + + +This library provides classes for KDE applications to communicate with the Akonadi server. The most high-level interface to Akonadi is the Models and Views provided in this library. Ready to use models are provided for use with views to interact with a tree of collections, a list of items in a collection, or a combined tree of Collections and items. + +## Collections and Items ## {#collections_and_items} + +In the Akonadi concept, Items are individual objects of PIM data, e.g. emails, contacts, events, notes etc. The data in an item is stored in a typed payload. For example, if an Akonadi Item holds a contact, the contact is available as a KABC::Addressee: + +~~~~~~~~~~~~~{.cpp} +if (item.hasPayload()) { + KABC::Addressee addr = item.payload(); + // use addr in some way... +} +~~~~~~~~~~~~~ + +Additionally, an Item must have a mimetype which corresponds to the type of payload it holds. + +Collections are simply containers of Items. A Collection has a name and a list of mimetypes that it may contain. A collection may for example contain events if it can contain the mimetype 'text/calendar'. A Collection itself (as opposed to its contents) has a mimetype, which is the same for all Collections. A Collection which can itself contain Collections must be able to contain the Collection mimetype. + +~~~~~~~~~~~~~{.cpp} +Collection col; +// This collection can contain events and nested collections. +col.setContentMimetypes({ Akonadi::Collection::mimeType(), + QStringLiteral("text/calendar") }); +~~~~~~~~~~~~~ + +This system makes it simple to create PIM applications. For example, to create an application for viewing and editing events, you simply need to tell %Akonadi to retrieve all items matching the mimetype 'text/calendar'. + +## Convenience Mimetype Accessors ## {#convenience_mimetype_accessors} + +In order to avoid typos, improve readability, and to encapsulate the correct mimetypes for particular pim items, many of the standard classes have an accessor for the kind of mimetype the can handle. For example, you can use KMime::Message::mimeType() for emails, KABC::Addressee::mimeType() for contacts etc. It makes sense to define a similar static function in your own types. + +~~~~~~~~~~~~~{.cpp} +col.setContentMimetypes({ Akonadi::Collection::mimeType(), + KABC::Addressee::mimeType(), + KMime::Message::mimeType() }); +~~~~~~~~~~~~~ + +## Models and Views ## {#models_and_views} +Akonadi models and views are a high level way to interact with the Akonadi server. Most applications will use these classes. See the EntityTreeModel documentation for more information. + +Models provide an interface for viewing, deleting and moving Items and Collections. New Items can also be created by dropping data of the appropriate type on a model. Additionally, the models are updated automatically if another application changes the data or inserts or deletes items etc. + +Akonadi provides several models for particular uses, e.g. the MailModel is used for emails and the ContactsModel is used for showing contacts. Additional specific models can be implemented using EntityTreeModel as a base class. + +A typical use of these would be to create a model and use proxy models to make the view show different parts of the model. For example, show a collection tree in on one side and show items in a selected collection in another view. + +~~~~~~~~~~~~~{.cpp} +mailModel = new MailModel(session, monitor, this); + +collectionTree = new Akonadi::EntityMimeTypeFilterModel(this); +collectionTree->setSourceModel(mailModel); +// Filter out everything that is not a collection. +collectionTree->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType()); +collectionTree->setHeaderSet(Akonadi::EntityTreeModel::CollectionTreeHeaders); + +collectionView = new Akonadi::EntityTreeView(this); +collectionView->setModel(collectionTree); + +itemList = new Akonadi::EntityMimeTypeFilterModel(this); +itemList->setSourceModel(mailModel); +// Filter out collections +itemList->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType()); +itemList->setHeaderSet(Akonadi::EntityTreeModel::ItemListHeaders); + +itemView = new Akonadi::EntityTreeView(this); +itemView->setModel(itemList); +~~~~~~~~~~~~~ + +![An email application using MailModel](/docs/images/mailmodelapp.png "An email application using MailModel") + +The content of the model is determined by the configuration of the Monitor passed into it. The examples below show a use of the EntityTreeModel and some proxy models for a simple heirarchical note collection. As the model is generic, the configuration and proxy models will also work with any other mimetype. + +~~~~~~~~~~~~~{.cpp} +// Configure what should be shown in the model: +Monitor *monitor = new Akonadi::Monitor(this); +monitor->fetchCollection(true); +monitor->setItemFetchScope(scope); +monitor->setCollectionMonitored(Akonadi::Collection::root()); +monitor->setMimeTypeMonitored(MyEntity::mimeType()); + +Akonadi::Session *session = new Akonadi::Session(QByteArray("MyEmailApp-") + QByteArray::number(qrand()), this); +monitor->setSession(session); + +Akonadi::EntityTreeModel *entityTree = new Akonadi::EntityTreeModel(monitor, this); +~~~~~~~~~~~~~ + +![A plain EntityTreeModel in a view](/docs/images/entitytreemodel.png "A plain EntityTreeModel in a view") + +The EntityTreeModel can be further configured for certain behaviours such as fetching of collections and items. + +To create a model of only a collection tree and no items, set that in the model. This is just like CollectionModel: + +~~~~~~~~~~~~~{.cpp} +entityTree->setItemPopulationStrategy(Akonadi::EntityTreeModel::NoItemPopulation); +~~~~~~~~~~~~~ + +![A plain EntityTreeModel which does not fetch items.](/docs/images/entitytreemodel-collections.png "A plain EntityTreeModel which does not fetch items.") + +Or, create a model of only items and not child collections. This is just like ItemModel: + +~~~~~~~~~~~~~{.cpp} +entityTree->setRootCollection(myCollection); +entityTree->setCollectionFetchStrategy(Akonadi::EntityTreeModel::FetchNoCollections); +~~~~~~~~~~~~~ + +Or, to create a model which includes items and first level collections: + +~~~~~~~~~~~~~{.cpp} +entityTree->setCollectionFetchStrategy(Akonadi::EntityTreeModel::FetchFirstLevelCollections); +~~~~~~~~~~~~~ + +The items in the model can also be inserted lazily for performance reasons. The Collection tree is always built immediately. + +Additionally, a KDescendantsProxyModel may be used to alter how the items in the tree are presented. + +~~~~~~~~~~~~~{.cpp} +// ... Create an entityTreeModel +KDescendantsProxyModel *descProxy = new KDescendantsProxyModel(this); +descProxy->setSourceModel(entityTree); +view->setModel(descProxy); +~~~~~~~~~~~~~ + +![A KDescendantsProxyModel wrapping an EntityTreeModel](/docs/images/descendantentitiesproxymodel.png "A KDescendantsProxyModel wrapping an EntityTreeModel") + +KDescendantsProxyModel can also display ancestors of each Entity in the list. + +~~~~~~~~~~~~~{.cpp} +// ... Create an entityTreeModel +KDescendantsProxyModel *descProxy = new KDescendantsProxyModel(this); +descProxy->setSourceModel(entityTree); + +// #### This is new +descProxy->setDisplayAncestorData(true, QLatin1String(" / ")); + +view->setModel(descProxy); +~~~~~~~~~~~~~ + +![A KDescendantsProxyModel with ancestor names.](/docs/images/descendantentitiesproxymodel-withansecnames.png "A KDescendantsProxyModel with ancestor names.") + +This proxy can be combined with a filter to for example remove collections. + +~~~~~~~~~~~~~{.cpp} +// ... Create an entityTreeModel +KDescendantsProxyModel *descProxy = new KDescendantsProxyModel(this); +descProxy->setSourceModel(entityTree); + +// #### This is new. +Akonadi::EntityMimeTypeFilterModel *filterModel = new Akonadi::EntityMimeTypeFilterModel(this); +filterModel->setSourceModel(descProxy); +filterModel->setExclusionFilter({ Akonadi::Collection::mimeType() }); + +view->setModel(filterModel); +~~~~~~~~~~~~~ + +![An EntityMimeTypeFilterModel wrapping a KDescendantsProxyModel wrapping an EntityTreeModel](/docs/images/descendantentitiesproxymodel-colfilter.png "An EntityMimeTypeFilterModel wrapping a KDescendantsProxyModel wrapping an EntityTreeModel") + +It is also possible to show the root item as part of the selectable model: + +~~~~~~~~~~~~~{.cpp} +entityTree->setIncludeRootCollection(true); +~~~~~~~~~~~~~ + +![An EntityTreeModel showing Collection::root](/docs/images/entitytreemodel-showroot.png "An EntityTreeModel showing Collection::root") + +By default the displayed name of the root collection is '[*]', because it doesn't require i18n, and is generic. It can be changed too. + +~~~~~~~~~~~~~{.cpp} +entityTree->setIncludeRootCollection(true); +entityTree->setRootCollectionDisplayName(i18nc("Name of top level for all collections in the application", "[All]")) +~~~~~~~~~~~~~ + +![An EntityTreeModel showing Collection::root with an application specific name.](/docs/images/entitytreemodel-showrootwithname.png "An EntityTreeModel showing Collection::root with an application specific name.") + +These can of course be combined to create an application which uses one EntityTreeModel along with several proxies and views. + +~~~~~~~~~~~~~{.cpp} +// ... create an EntityTreeModel. +Akonadi::EntityMimeTypeFilterModel *collectionTree = new Akonadi::EntityMimeTypeFilterModel(this); +collectionTree->setSourceModel(entityTree); +// Filter to include collections only: +collectionTree->setInclusionFilter({ Akonadi:: Collection::mimeType() }); +Akonadi::EntityTreeView *treeView = new Akonadi::EntityTreeView(this); +treeView->setModel(collectionTree); + +Akonadi::EntityMimeTypeFilterModel *itemList = new Akonadi::EntityMimeTypeFilterModel(this); +itemList->setSourceModel(entityTree); +// Filter *out* collections +itemList->setExclusionFilter({ Akonadi::Collection::mimeType() }); +Akonadi::EntityTreeView *listView = new Akonadi::EntityTreeView(this); +listView->setModel(itemList); +~~~~~~~~~~~~~ + +![A single EntityTreeModel with several views and proxies.](/docs/images/treeandlistapp.png "A single EntityTreeModel with several views and proxies.") + +Or to also show items of child collections in the list: + +~~~~~~~~~~~~~{.cpp} +// ... Create an entityTreeModel +collectionTree = new Akonadi::EntityMimeTypeFilterModel(this); +collectionTree->setSourceModel(entityTree); + +// Include only collections in this proxy model. +collectionTree->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType()); + +treeview->setModel(collectionTree); + +descendedList = new KDescendantsProxyModel(this); +descendedList->setSourceModel(entityTree); + +itemList = new Akonadi::EntityMimeTypeFilterModel(this); +itemList->setSourceModel(descendedList); + +// Exclude collections from the list view. +itemList->addMimeTypeExclusionFilter(Akonadi::Collection::mimeType()); + +listView = new EntityTreeView(this); +listView->setModel(itemList); +~~~~~~~~~~~~~ + +![Showing descendants of all Collections in the list](/docs/images/treeandlistappwithdesclist.png "Showing descendants of all Collections in the list") + +Note that it is important in this case to use the DescendantEntitesProxyModel before the EntityMimeTypeFilterModel. Otherwise, by filtering out the collections first, you would also be filtering out their child items. + +A SelectionProxyModel can be used to simplify managing selection in one view through multiple proxy models to a representation in another view. The selectionModel of the initial view is used to create a proxied model which includes only the selected indexes and their children. + + +~~~~~~~~~~~~~{.cpp} +// ... Create an entityTreeModel +collectionTree = new Akonadi::EntityMimeTypeFilterModel(this); +collectionTree->setSourceModel(entityTree); + +// Include only collections in this proxy model. +collectionTree->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType()); + +treeview->setModel(collectionTree); + +// SelectionProxyModel can handle complex selections: +treeview->setSelectionMode(QAbstractItemView::ExtendedSelection); + +SelectionProxyModel *selProxy = new SelectionProxyModel(treeview->selectionModel(), this); +selProxy->setSourceModel(entityTree); + +Akonadi::EntityTreeView *selView = new Akonadi::EntityTreeView(splitter); +selView->setModel(selProxy); +~~~~~~~~~~~~~ + +![A Selection in one view creating a model for use with another view.](/docs/images/selectionproxymodelsimpleselection.png "A Selection in one view creating a model for use with another view.") + +The SelectionProxyModel can handle complex selections. + +![Non-contiguous selection creating a new simple model in a second view](/docs/images//selectionproxymodelmultipleselection.png "Non-contiguous selection creating a new simple model in a second view.") + +If an index and one or more of its descendants are selected, only the top-most selected index (including all of its descendants) are included in the proxy model. (Though this is configurable. See below) + +![Selecting an item and its descendant](/docs/images/selectionproxymodelmultipleselection-withdescendant.png "Selecting an item and its descendant.") + +SelectionProxyModel allows configuration using the methods setStartWithChildTrees, setOmitDescendants, setIncludeAllSelected. See testapp/proxymodeltestapp to try out the 5 valid configurations. + +Obviously, the SelectionProxyModel may be used in a view, or further processed with other proxy models. See the example_contacts application for example which uses a further KDescendantsProxyModel and EntityMimeTypeFilterModel on top of a SelectionProxyModel. + +The SelectionProxyModel orders its items in the same top-to-bottom order as they appear in the source model. Note that this order may be different to the order in the selection model if there is a QSortFilterProxyModel between the selection and the source model. + +![Ordered items in the SelectionProxyModel](/docs/images/selectionproxymodel-ordered.png "Ordered items in the SelectionProxyModel") + +Details on the actual implementation of lazy population are described on [this page](@ref internals). + +# Jobs and Monitors # {#jobs_and_monitors} + +The lower level way to interact with Akonadi is to use Jobs and Monitors (This is what models use internally). Jobs are used to make changes to akonadi, and in some cases (e.g., a fetch job) emit a signal with data resulting from the job. A Monitor reports changes made to the data stored in Akonadi (e.g., creating, updating, deleting or moving an item or collection ) via signals. + +Typically, an application will configure a monitor to report changes to a particular Collection, mimetype or resource, and then connect to the signals it emits. + +Most applications will use some of the low level api for actions unrelated to a model-tree view, such as creating new items and collections. + +# Tricky details # {#tricky_details} + +## Change Conflicts ## {#change_conflicts} +It is possible that while an application is editing an item, that item gets updated in akonadi. Akonadi will notify the application that that item has changed via a Monitor signal. It is the responsibility of the application to handle the conflict by for example offering the user a dialog to resolve it. Alternatively, the application could ignore the dataChanged signal for that item, and will get another chance to resolve the conflict when trying to save the result back to akonadi. In that case, the ItemModifyJob will fail and report that the revision number of the item on the server differs from its revision number as reported by the job. Again, it is up to the application to handle this case. + +This is something that every application using akonadi will have to handle. + +## Using Item::Id or Collection::Id as an identifier ## {#using_id_as_an_identifier} + +Items and Collections have a id() member which is a unique identifier used by akonadi. It can be useful to use the id() as an identifier when storing Collections or Items. + +However, as an item and a collection can have the same id(), if you need to store both Collections and Items together by a simple identifier, conflicts can occur. + +~~~~~~~~~~~~~{.cpp} +QString getRemoteIdById(Item::Id id) +{ + // Note: + // m_items is QHash + // m_collections is QHash + if (m_items.contains(id)) { + // Oops, we could accidentally match a collection here. + return m_items.value(id).remoteId(); + } else if (m_collections.contains(id)) { + return m_collections.value(id).remoteId(); + } + return QString(); +} +~~~~~~~~~~~~~ + +In this case, it makes more sense to use a normal qint64 as the internal identifier, and use the sign bit to determine if the identifier refers to a Collection or an Item. This is done in the implementation of EntityTreeModel to tell Collections and Items apart. + +~~~~~~~~~~~~~{.cpp} +qstring getremoteidbyinternalidentifier(qint64 internalidentifier) +{ + // note: + // m_items is qhash + // m_collections is qhash + + // if the id is negative, it refers to an item + // otherwise it refers to a collection. + + if (internalidentifier < 0) { + // reverse the sign of the id before using it. + return m_items.value(-internalidentifier).remoteid(); + } else { + return m_collections.value(internalidentifier).remoteid(); + } +} +~~~~~~~~~~~~~ + + +### Unordered Lists ### {#unordered_lists} +Collection and Item both provide a ::List to represent groups of objects. However the objects in the list are usually not ordered in any particular way, even though the API provides methods to work with an ordered list. It makes more sense to think of it as a Set instead of a list in most cases. + +For example, when using an ItemFetchJob to fetch the items in a collection, the items could be in any order when returned from the job. The order that a Monitor emits notices of changes is also indeterminate. By using a Transaction however, it is sometimes possible to retrieve objects in order. Additionally, using s constructor overload in the CollectionFetchJob it is possible to retrieve collections in a particular order. + +~~~~~~~~~~~~~{.cpp} +Collection::List getCollections(const QList &idsToGet) +{ + Collection::List getList; + for (Collection::Id id : idsToGet) { + getList << Collection(id); + } + CollectionFetchJob *job = CollectionFetchJob(getList); + if (job->exec()) { + // job->collections() is in the same order as the ids in idsToGet. + } +} +~~~~~~~~~~~~~ + +# Resources # {#resources} +The KDEPIM module includes resources for handling many types of PIM data, such as imap email, vcard files and vcard directories, ical event files etc. These cover many of the sources for your PIM data, but in the case that you need to use data from another source (for example a website providing a contacts storage service and an api), you simply have to write a new resource. + +https://techbase.kde.org/Development/Tutorials/Akonadi/Resources + +# Serializers # {#serializers} +Serializers provide the functionality of converting raw data, for example from a file, to a strongly typed object of PIM data. For example, the addressee serializer reads data from a file and creates a KABC::Addressee object. + +New serializers can also easily be written if the data you are dealing with is not one of the standard PIM data types. + +# Implementation details # {#implementation_details} + +## Updating Akonadi Models ## {#updating_models} + +NOTE: The details here are only relevant if you are writing a new view using EntityTreeModel, or writing a new model. + +Because communication with Akonadi happens asynchronously, and the models only hold a cached copy of the data on the akonadi server, some typical behaviours of models are not followed by Akonadi models. + +For example, when setting data on a model via a view, most models syncronously update their internal store and notify akonadi to update its view of the data by returning true. + + + +Akonadi models only cache data from the Akonadi server. To update data on an Akonadi::Entity stored in a model, the model makes a request to the Akonadi server to update the model data. At that point the data cached internally in the model is not updated, so false is always returned from setData. If the request to update data on the Akonadi server is successful, an Akonadi::Monitor notifies the model that the data on that item has changed. The model then updates its internal data store and notifies the view that the data has changed. The details of how the Monitor communicates with akonadi are omitted for clarity. + + + +Similarly, in drag and drop operations, most models would update an internal data store and return true from dropMimeData if the drop is successful. + + + +Akonadi models, for the same reason as above, always return false from dropMimeData. At the same time a suitable request is sent to the akonadi server to make the changes resulting from the drop (for example, moving or copying an entity, or adding a new entity to a collection etc). If that request is successful, the Akonadi::Monitor notifies the model that the data is changed and the model updates its internal store and notifies the view that the model data is changed. + + diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 0000000..2ccb74b --- /dev/null +++ b/docs/history.md @@ -0,0 +1,46 @@ +# Historical background # {#history} + +# General # + +During the last 5 years, after the release of KDE 3.0, the requirements of our users +have constantly increased. While it was sufficient that our PIM solution was able to handle 100 contacts, +300 events and maybe 1000 mails in 2001, nowadays users expect the software to be able to +handle a multiple of that. Over the years, the KDE PIM developers tried to catch up with the new +requirements; however, since KDE 3.x had to stay binary compatible, they were limited in their +efforts. + +With the new major release KDE 4.0 it's possible to completely redesign the PIM libraries from +the ground up and use new concepts to face the requirements of 2006 and beyond. + +After some discussion at the annual KDE PIM meeting in Osnabrück in January 2006, the PIM developers +came to the conclusion that a service is needed which acts as a local cache on the user's desktop +and provides search facilities. The name Akonadi comes from a divinity from Ghana and was chosen since +all other nice names were already used by other projects on the Internet ;) + +# Problems with the implementation of KDE 3.x # + +Before digging into the internals of Akonadi, we want to take a look at the implementation of the +old KDE PIM libraries to understand the problems and conceptual shortcomings. + +The main PIM libraries libkabc (contacts) and libkcal (events) where designed at a time when the +address book and calendar were files on the local file system, so there was no reason to think +about access time and mode. The libraries accessed the files synchronously and loaded all data of the +file into memory to be able to perform search queries on the data set. It worked well for local files, +but over time plug-ins for loading data from groupware servers were written, so the synchronous access blocked +applications which used libkabc/libkcal, and loading all 2000 contacts from a server is not only +time consuming but also needs a lot of memory to store them locally. The KDE PIM developers tried to +address the first issue by adding an asynchronous API, but it was not well implemented and was difficult to use. +In the end, the design decisions caused the following problems: + +* Bad Performance +* High Memory Consumption + +Another important but missing thing in the libraries was support for notifications and locking. +The former was partly implemented (at least reflected by the API) but only implemented in the local +file plug-in, so it was in practice unusable. The latter was also partly implemented but never really tested and +lead to deadlocks sometimes, so the following problems appeared as well: + +* Missing Notifications +* Missing Locking + +The main aim of Akonadi is to solve these issues and make use of the goodies which the new design brings. diff --git a/docs/images/bufferedcaching1.png b/docs/images/bufferedcaching1.png new file mode 100644 index 0000000000000000000000000000000000000000..918eaf73a5d8799cd7eb61585af30a84bbe60f6a GIT binary patch literal 37509 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

_ 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ŵy+LP(rfqK?#`LZ9q69znY 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

-Z2L+klf{%z~s^xPBL7yo-HfD-L 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%0DUV~9 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 literal 0 HcmV?d00001 diff --git a/docs/images/bufferedcaching6.png b/docs/images/bufferedcaching6.png new file mode 100644 index 0000000000000000000000000000000000000000..1f97cf6f29cc35755fae8f60667d70c484b5aac3 GIT binary patch literal 49369 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 PmLd&RZIyB*%h3M;Re9!` literal 0 HcmV?d00001 diff --git a/docs/images/descendantentitiesproxymodel.png b/docs/images/descendantentitiesproxymodel.png new file mode 100644 index 0000000000000000000000000000000000000000..01be72f3926be6abc16770410be6f4709424fbd6 GIT binary patch literal 8783 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 CKSC1# literal 0 HcmV?d00001 diff --git a/docs/images/entitytreemodel-collections.png b/docs/images/entitytreemodel-collections.png new file mode 100644 index 0000000000000000000000000000000000000000..526de5735aaa57bb1f3873c6fc2cfc756a6dee3d GIT binary patch literal 4292 zcmbVQc{G%7`!~~sM}$GPEHN`kc9SgGGlMB($rh39dk7_D8#`m)w@~>KC0i={Hid-j zjC~hbM_Jy{`JUf7zyIFzo^w6V{ap8beLmN6pX&JqilY zIr4WKHJJQn78-z&2f!U|;z`!L$WJQ>+UOcjLBZgysjgz+JGPz^sn0xospF7p%8HrQ zOt+aXxt+!5c5>T!5UaGSu5|Nx78bmVvI-=2{+7sI3L`R-jn#d)tr&YQLLGjU@5@WD z6a;wp#i)GYliQ+|k-X0(oCBAX{~iAcIsc`7FILY$e^+9zUq|`uj2dv)ME5(J!<&C9 zp$)nH%i7{LcUdm0wUj<1U>pp}iZ!%~wS-9=;lx3XY{!%+pusoElgnjfo7nLipNoPm?PF_&bHNExE>o34rh zS2g$aHssrSnU1k&u4e3>kvSLBvj_TSgyEfiR=dl9;ZpNk%#LNM8P5Vo*DA20HQDFhiMU)}$g8H2md{r&^uBI2VvP4j+`BMEt5MyLcl)=Ez zQqTst)cSb5J8`MtN>H#`OH~Xn$U+S2zDru(m!oxWV1VDan*L3<`8>@^lXwsA5t+$2 z_Wm7+fv+j4$tr@UHCC1>$$i_ZL1$t#SNMJY{lHE=RDp(8D-E)?`%;R@Ct_WHY|1Q0 z1Knj^Z9_F1HLWhOD^gOMad$Aj6gl=&Z&~-~`ABh%~us z!x9N&IuuyInEEizNxgguWkAjjl$Tnc60Zs)HlZyil|UVy4dp&#O=( zjc!Sq_}Y^h0O?6T`Otf>t->Vs_h1gOgzO473Iz_XhyXIXiI&8&mG%o6>R>`gav2;% zbbcVIqFRw#E_i(WEE1B~lYq^?qEc)vsnRm9$#m`|2Dh`bqK`v2{fu;CQrzR7EGc{# zy|_R5@o|p)QuNWH>|TP8hvm==C5+L%QVX-V^2+PBW!T{|42L|FNcLW8+(%ys%2L*Z06rfqYOeyP1+KIPdmk05@&Y1|% zNw%16>o>HLwIE_bq@I83U_Ns0xxh8A;)?+9{2+m;Y;kIm?^SQkSdGKR&!N&SZ&0a1 zc0!!(4tR-1KKDidEpoOtSkhs##Wr zXyv@a5*0t2n?I$_a{c`D1)UfNZ$gNfadMjr-1QbI$VQVqN!(hCM(>**BVn&3MBz{dyOVB-D72`f9N}x`pLjh6m zTgIn>0e48TwNm$t_@C!MtF}~?fb2uCg^9-ksHelQWHp4QWlNF3)$g0_Fw0<;c*gDW z`O2OzQQ_$#DN&&hQSte)+VMa0wI|lQ#gr_135I8NGQp^xPkI7NPGx@hlV{Hg>b+gzg!0uu005p3!O`QM7W@IBP+?r<4nm}1Lk@xc4Mt`!ntS6KiCZZ6*5X}U*FaRQ@-DF0vwL4^eD8U8aT z^lwi`gdTH1r>tf9plD_$K?Gh9!JBhFY4Uj6gBn%_C-p}| zT;k%zi?=hS-+3-O$*d~jsuV>~E=^k6+TU;a(s3*or}krbSikP#2iO1H^t;~}a2qy~ zSkM{*@d`lTu{09Uq9{PE^IPWihqfo5wfIeI_=oR3>hO(U|NfbCXdxm6&v43(US3%Z zhNG~{UsB%NeFM|JHfU>O0BX>xLQJS&=Eb?WfzksC0fD8s`yf%3&X3(nt2dUY_0tYJ z(+6A^n-wqYb8aRW=M)Ak+5WCGUW%o)>6$t?I+WrILW}bw1ev1>rUH0mpL(%z4+<#w zcpV|r@Rt$83$p&Lo*7El*Rh3~zK7cjsUlfa^c)cvqwYE<3Fy>kCq2~Ql#2hVhnE7Ajt_lO*W7&9s|%u>+|wyF%x!u@yZB=f zAg4@|%0c5WD?*|r%%FYj`&?(dOi}Cy*z~K@cVak9e=dbi%SJG`%xq(J{n;F4&)g^$ zL$X`r?kxKsY;NRM`q9wiVEd{!(~j(15%Pj=j8QB znU%Ga;4FkVYAx{d*(`3IbyaDdbB)yz4Q<{1l?hHxGCH;TIl;(jj>(c^QIt#dO|Fw|P=_3PKzq)|f*1u$vW85(1HuT5I? zgS~UV`GW!ZclN7ep0S)HGN9cELMk$q9eMJ0r#j`!BT%0mQ*S4MGc2iDJmO z(6~4*yn)6m=nVLyx`u`{xz3&DdD$_)d$MFS{lGC0+9)h8JiLV9EQyeI7$`h#>sq;& z5^U>Snptfwn{;?D#Zyl-uuCF{_~^c6{=7sG=(=Zz*NxJ-*+J_XpFGRW<;9zBt!W=m zn1$_q%==t#RBtW0)sNkPjxZec=E%=#DI7v^(}?kOzlo~Pe!7kBDtMSc6H!1R@Fcr> zSV0cRHb{9&fjyBj)Pnm30Zl98F`=i z;ssx~F3o@>ney?3fKKjo?F=@4(+vF>d^EwxJ%0Q9Ns+mSUSj6@B&VDLT6xKC7L6B0 zRIQB>1HZqF*iHMyCv{)_Wh3p>qw$s{fW)C^=Dc26toELrqRydv%QVFx!2e<<0{o7XuKlgMm_CirgUb{U8Go;p|<{} z>2?fztCN>xxZxXjH#vuXb%kVPW9*8F!Lx5xUHE3s#uE-!4}#9~z>b*0=>tLR2Zw=L zN6GOBAg(+F)f;g^o-~0^E$-)sNoK_XB_$=f_xVk|*H^A1tX1_2?)%s5+YZJb3|d?B z@B!A=>frnNNGI}ec31^jf!PzVhI+IPXmnw^GJ;<%oC;Vz!g!MyRnXdJCZh?@dn;J@ z{~9Vw1jV59)0G_>BSJOWA*I$Qvr_K5V`7qPosbo&z>K`0=>C8N6Iq0@@DA(0iWI_F z=1jS8fpTlZChyl=ww#aZ)hlV-lL-K>IDA@u;&5lFGmbr^T9O|}Usz>feF<;5(QWaJ zvV3*baaU>{r@YTx=GSKV^6uA5)1c3WuUc=@QBz#CCkzp>WY=+_vwry0&^3fls=`iM z7<(N96F%&gz!P=PKeomk6$3NtS;%(Br-o_{ja`<@TfCs0yhwrO-x-Yt*M@89Q@Pgg&-biU>57l}2GmqFf8C^pwP|Yx?`AZTXwAC&VmsnF!@Espe@256 z(;+!F+sXUUw3jO2WX2K|*s>X74*~Y0|7+YtF}yyNn=2$Fgo?i`>$gZ-&${hWz{>GK zqL`fEVnS2aW2wMj>~;7{=c5c9y5M z=3CcAF~xJkki1{%nQhXa#KU+P#M@y2If`yoXwkPLn+KHT{NeP4l<&9FQ=6B;XLqh( z44%AOT`*pi?QV-yyJ_-0qc+@m4!Ol}`4ijw*L9(sd}nTt^FDvnbP%R#5}zm@x;a88 zI2g1dIu;yvs~)Fp`WmSu!#=XkUu5BdTCh~P0hd`)q^F{~csVD{FOqcl6j&2o!nO?a zq&Bj}n~{?7Ux|6ODVB#9;LoA?htTV5u7!{eQ`Nvz87*btOe}X#jDVWN8#~bYyS?v1 zH~rXR|AjSbZDGXOUxqR%FyTIM7@oNFydtz&r$bkjm~f38hN+g47+{Ys5LtGunLPW? zO{QK~#ex zL{`tXQpvUu_Oy^3qls$oN7w3g$7D-njOVq2##8flXc?{mVxJ$nz_(=F;!Izda3K%s zarmbYlAM1g`&wAAASjta>v>?xTT@YTT~eTINu>{g4@aFz&`UenYCv8}Q^b7Bt5CC( z6G0ICOoBc`$p{MawkZSokbepOCzUjy$d%1YO=-zCC6ozg^op~8e(}LFn+wB;ke&hL z5cLmFQAm#5+Rj$&*607$VK5Z?|7Aq({~|rIQ1x)2Z{Nc8M1<=^FV=?s@f^$hJujE* z*6h3&Z4@6~VKML`^vPqUN{Dt@ClXE#{k=^4=OWXt_YsuadQ*Cj%#`^di~ OC^T=~Q7>1u4E_)9Sm!AK literal 0 HcmV?d00001 diff --git a/docs/images/entitytreemodel-showroot.png b/docs/images/entitytreemodel-showroot.png new file mode 100644 index 0000000000000000000000000000000000000000..df9804da0a9d4a62131a05800a88d0f37a7c97a8 GIT binary patch literal 9304 zcma*NWmr_-7d8wI4hS=d_r6!{v(`E1ns7~ZMKC@UJ{A@hSXoI<8w(5j9_Dby!<@0W z6t$6<%L5lBLw77J(l?l+1Dm+j=_3{vGrqE%w66EupR@=gHAA|dYrtMtWL7-mCwpzu zeh-;sX?C}dUkBG-jmqaT7K!~NB{G08z?2Ht0bAT}X3;UnoM^Oiojaojq^y6xLx-7LJRpNw<9HBgmIC2YI| zFsvI7tOrwG!HkNQ%qg$lb*@*P55LWspF4)!+S$jtkvmE#y#J)!Y;ty9J`#4t*89LL z$Glt!cHZjYG5Ck}ja-#sBLw(^09fup(hi5_Y)4!xath)+2o{0UKU4}9vt9FnWZD%o zhs6d`K6u3!Lj>xU9WMxEu$UPM4h_S#Hq%s%fIxVJWnObKobEwr23oe`T`FukeP;S5 z3j8C^?6R1sUvQUA#PF_u`uzIET+vrCUJiM=7zzbe#~0g)-hYqRwKKH?||sZEl4rj9-SN23$w-hs;fdwD%~jXx|gwKvQWaKHNzkuwv2X;EG@VV7o8li z0Lru=S2%f~6y@54Sn^P=HRMTi9iJE6YEqfFsJyWHQjEAQxqr; zp`lr=S`;=_L}&AT{JvX{USiin1bRq|A}CSZ4n)*W@SpJDOC)jPW~$=7M?lMPbE#tN zWN2@J6(guuyOX{d0>R7E3`7A4k@o*X+Ytg(MR@IdR1v)a-%c|J%t3VZ`Ov7gtjP_V;Zzn-@9$vgN1=!~Lkgg}IBo3T%L`(lcV|CfxNhdW^0iMI9*DXuo`4xe^FLXMfCE-^2eWFE2F{Nf|s5$jRgwsAMgKtcWL&%nV{=im`g|WT% zJrff-Qz8 zE{=YillZ+UhFDtWWbZjmeiK+7+@yF7546Yn$uQC#8k-Z^IP~Uf*4+H^hj$}Jmn)|fxnutEk*-^|@oTghwuc}!7WFY6pSA<^v zp)@Ol!2}s3J$L3kw*Calh7~VqVO+aF)&QX*n_*Y(XmT*fx94x>GGh~c2+yGfF=&K$ z<;ULgGkv9-cqTytkPB@jgUZC7)uA^7aI-rT-TvWVnwS+%3}Vsv62@z5iScc%m*ow9 zC}O{7GsGOr!k0u^_bmeuqSEz7dMVe~pQMrq0udksCMyzVtA5;F3`{7S319YaPLh>O zWD3}y!fJ89B=0_dL}6ZLzd`8W=~>p(ge{_OO9NF63WvtSu?Bu6WePxr=NK8jzSLvsAiJ5};+DaB_!K*%Ygp z)VHa?SAhQ@vuq9B9v>dx&9_ShB0k8)g;c@!S6Zl$IaT&~>NHQ|bK$;Q0Kj0<^)d)x z2Ud#H`o$yU$*&{Nm>e7@1kyZ8E)bx91Uyq)rpn3Pv1x@whZ~yra#>kfIZ!M}6*9(3 zY)u84m3fb5HNQJu+t{#g`tZtsdrdiNCH|>~CZu~09}f_BQ3c%E3N=_;Z1nP8C;|dG zSaoq5PlK@gXkGz$fVL!>t%42)w|`IUA0t;+SGikhggyF6+XI!pXG#MO zbbcTF>M`pJ-4N!)}NC1JsG{CbYNgQAzH>~^N_9(&I%Tvw(N0+nRG}jydcoubL zkdihEtFXBE(ZeK0CCm~qh<|FtN`J6jmm8>R;n$ri;YUT;@SI+`NPs8;iV05;b~uzC zq!fg0CdCNCIBW@>sdo?6F7W+(T)@!S~YO+G5yQxp+V@%Q|1BN{N1_pFumFU0#;&7;DtWpprb&McgW@58UyZ5$H zOvD&Peut1xI`&d=%)s?QJ5avB`C8Bea#aDR*(O7IyIlxmnQHJAggd_PRgJ@}%NpQEy>QuD?dwkbC2n2{Y2+6RaSC$DT*u^576F*2Y$@wcUaZ-KINf@ zkb_Z4?N!k3_Dr*pzL*AVv$OLv9TqY@T0D!9X{9$TjFRsg5(j7f-~i)0T+V*w*RNj_ zd9BUOvbbmA;xZK?!xH9#N6@{YO1_Io#}(;m;nxjCOzDNCZ}-LmjE&#TR#n#|7@C0z z+oiT45UzS-iJAF5KGDQeSy|(3_hvoQtw-q{^R8OaMsK(>=*T?sMyisj9I^F%9 z;yT#G)2i$WpSMYT_3&bFW~L*m8{T~^MY`{@*Q&||t82%76U_{7mNGvWY z1c9yPr`Zgpnst5gzh@3|(cnrzv==7TIxT9)4g!&_y$v2mgtT!kw2!~I_^I0Q@HT}O z*CF`f7tY<$qz7pq`r=s_KE8UgV6`6UAFmuoi z3wrwx36JNF$?pf~f$N)tcS~L-7gZB~y3GQYMH)}HiXX|zB23IJEHHuoypNqR3pGaG)h`} zFIxc11Tp@Nu~LjdTHX#Hc-B7&`EI8UF_<|yBs4wxG1May`lpa!Hh7R}&BnRc83*`% z-b7oQ5oG>0!lUsd0ke6$cf%Pp ze`(wiaVAcnZ}dpuKSe! zFLeK;38C5l9EleH(^IX?g8;)Vh0i2!=-9US$+*_Vy5yL)cLJcHxmupHeqNhq;#ts4ijgKG0 z?&o&W`8=&jb>EDx_%_IxtrA7WQd!a7#hNY^{96Clz$P8E%aPQ(cXDp8r9;22PeV#9 zA~O3t`b3&&I7uh=3#J7=;w1$1AkxQ)vk0dI*UD)5HRO1CfVUQV#;I8iyF$B?BcW;E z1d^<`8xLMSv->=8&yGy~e)TwnZ2VkG%L3r2m^Y#sg&KXH6*ct+h*VEOC`SV4Rr7d8 zMMNyB1KZx?5j5g3(sT9O^Ca=q(w)wYi#FOXG**fXEG~Nu{aFxCU923uYB_sn+PdfQ z)|QAoW&h&hVw9&c1e77f#>3>JQN3J2-;;1sV?u)W{{HChSA@>z+i|IiVH_ENaNH9j zsxT_NAYc~o-d99ELdurjj-H503@AYKN6 z(`*SYqW9h|V!3msMSHnbxVNF=5BF-zLp$nU^*gaURY{8`A@_MR3%$IfMZy*(_q z`p3(+x!-fDzSq@NRczb&)JIKgq!@{cG`@TH?lt+19*q6M`{oMAjZ8>1t;%~eKbFbm z8I`0H$Gvs5EUHx@geo9+xo&1A>uhR`{UL4S6NMUTR5A5 z6m}*yV<3GdE(l@FMZq}oZiBE;D${prTCoz_>_q-O_Gbd}!422ZAPyE%?<%NR2h~X* z!!pZ4l)87_{c$O04MM7;4|yR9C@F5LwY4S%HHSYRD=TA4qRwr%D?gWMJtXFHI-uxduZj)S89$P9%}lvDMkW6+Q+sBZZq*VxF-TpX^EP? z=y2z_5a;vdkK;m)zZ6~~Ln$8ED^RsPTVC_4n9mn;{~bxLOh5M(JGS&g7J_Ud@O4|> zbZMiLSHgT@iD^+m*~`zLjLkH4?Muo5elM+Ho`-}mkb|y)g<4Nwda$kqU$^`=wSiTZeQD{FsBtCaW?%euoN-hH z<3}LjOBn^E;vnN~sVP64cVuQ_VssKo_(@-PY6eXM&Tui&v9Fz;^zuAeX!QDM4d~id zaSi5hTO$Y4NL9z&6Piq0ar)6 zi!BRCT*v3s{X{@L7|UNPrpVSqqKA5^2D@@J1}v*xN`RXJE0x5N$WX!x;Wq#Km%zwx zpYSRDf_XIO>dXK}L}Hmy;y89m4tU`2*+7SQ9zp03e%M2D%7JI>ZVzeKfXZObj<)Rq z8UicFgVpxrx8ew{`JjuLe0OFmZK8h6FiNSObF5h! zy)`pmMF1`-W%0orS-_|2se!=1_yt*rjXsqhiqVhdagS?b0SU%mr7bM7S;A-pZ0+pc zKiInn6y~WlvXrjmZ`$4bj;MGS^)816Dhue7pq??PH^2Y*TjcrK-tc0D6(0#!&HPY? zWa)#!5ls04KA+)8;d@8U=Qb4z`CIy`zM`(q08^0ga4@y(u9V$Xvj5*tcr;L5QJmkq zxQxSLvd!GnB&TbAecct`V{mhnhIXBlXe-v??}yxJ3&1ELIL!AYh#ybiV6Y^@Ih*3N z-a7~#GBIIz;F@0MA4dx-Kdu0QWlDkMm1uSj4rcMTr2E}knv3J|9L;iI)jM*j+N{7D z^Q|X#E4P&iwdi}Ql1gMuHd1%2?1@wR@VVembF9Wl}smrnt#uxWbpwrPS!I zF_j5=?LQ4+e2e12WYndmoftuzB!~5Mu29)Kujt=^JC-oKMy^v5+cwtJ$;YMd>q89Q zhEtt~c43xDzCAEOk=ULXsL)U z3R&;k2V)YDPzxX}Hx}5dZxtguAw|zmBtV1*#;lV8XaNu-QWJ3c_rJs)u3LQ0%UH^C zsJ>r!J=4`F-S=CRj0z7tSRnmpl|nD) zJ69#_HZCsRi;dy(O<;{4O-$!EL}pH$`|T?{c!SPY#7G-WTwGj>q*ZsmR(5C)2VKVL z8Bal8NHN>kK+;6(7KKKsgoMYq=r?uqIAl8t@l}hVzle9aU_{CmE(5Npth69W@VA&* z+w0C#Tl9auR5ErzNpZsaGm#?Cu{8(AyGPQF^yuolt>#0C{9-fX!;yY)W?R_0t=#-16C6?vr*+Ev(`o}K*m}OHEZNqDE-iAYH=@g z%1iD9tAPD=e)=dP7%l!ydI5SG9YU}%l&16J@wRhrRur?m z+xAPVk+hJJ{Q)HaN3B9MM4jEw#3YTSj2PTPEa2(TKHw_K&UPTmPO(?~TZY@+w0QNz zMpTmDs$QOa@>^9DCnsm&tn78MZdI&=we4fyJO=(NZT>dF`ynwdvy!PK2GPJ_Ri(kC z6rJou`FHl@y9Fb6~h!Fe+cp?PNOUYZ0FeozELLGi7gXuhCEH!EEn_aUaKatGZVN z0nTa<)53v^ukpaG6AvvoI?<;)^Ze3ywwg9)BP%b?^HD2#-D7l1Xji%Oa=5^%?so5W=VV@fXM*<7S6~6+@>^=GmFNYfM@%`6BUkHt@gqy_^Pu-ez zj2d;vdH(U7klFEYZi|%`O;?2A4Vl? z)u)0xPOxCQt{*H;c}*dnLYTtGi>ipO=RQew!dqDGPXtPPQN8NmKIv!z@8jrYp~ja} zS{w$bTZIqmo%_Um1dlp5jE+ixwt?z~wN9K7+qx`+DFZuvE&mSeiBoN z`m*46$&awIg*seShNDlL+_njp<967%H>QCto_*m!F(sG5btk+2JEbTODz^^qWjY0J z1N8;L9&#LMp_5{lkq847_%rEPVCoXP6-vwIE|I3;-Ze@cDj3?5^DR?V*Y zsx!!gZs@_@7xEes)LQD1gvJv8@qSGx%U<5_Wy^*yU11zE=-)9Pqs_@|Mz7p1=?J{^$}gS7iRKrP@B>L8x&0eEI=Db|Nk zLSUrt`1@#Ve3J=^>{gz984O2TNq(ufnE&>K^3wJS>S2lL^^pA?)s(znvU03jOAw-2 z3kC$N`#u1?%WeSy|EZxlnM^t*o5|-!BHUWq5vU1ucxFELsnPQ;DPb6*D=^l1ZU`eO zZHUZ9Ah4rTh5cULsaneCu7L?1Zk1cyv79RS6&DwmQ}lJgPo&R{_#xj4mID9r=$fNi|?S9kf^bMux_j1GXbId0~(TR6_r51%M9h`4cH*k-$(z( zUr77F%tcS-`+_1+nlI!25M|SXAQx4}&vkk?Zo4pnGYpgV9sH@~L_ZsW4GOZ*4x;W= zW}bg4A9uV4g{s{Jwis-jt6hMWkgIJ|@YB*%_|frH@uT)A?DHD%z z6Pe{D-lxfV1+b!ym9;)@p|OeMxr1LB9#ysD$$6oKeg@!T5o1g^sei!X&D2;}<6qmG zab-KuRG)X&qKE>YsEO;?F zpw5nCDX$?4Dd@aJ3e<83Bw*L>EwD)#(71O0iVxSa5hJIz*8Zfb#+j(k{;UXyl)~)A zd+NW%aH^7?RAi7e*9J&lC$kCYxlw_XdNLCnot$<}%WbK?!XG=d(_@}kuR^g=B1sny zpRK*r2l-VHspJylQfj;*cr}sudeBGc5eclYK+rKarS0a^VFy6!6Ya6ZEZ? zi;e*S*Lj(tkIZ4cP^q8?*~_A)T>5X{zI{)3vc4gAGY$M+|6^RqA|}bWXN7uxG1Iu9 zER5qb-M2BmzT;y{MghK*v@hvqzcy=^Zy^~;SKH`kkxiJSGTc6G@Z2e!W#h$}+95qT zJ9{SzuS`}oxr(f>W`1K{g~VoB=0_rSoZ1+LGKP_eMwm!!;}ONjt>nRvlDB_1N4IAQ zC}?_6l<&z;Cc_Z7BnPfbejAPru6raDxOKEhG;r{n>XOCIVMuFUarI{vqJ^jJ0yxoS zGemcZ?}I2+jvnE|(t=4q_x-Zm>OoJrTlU2Wuz-U5#tjoc z<}t6&Q=Y$wv8~(f5AeBEj%w2+1EOcMp<$n#7V5BpON0fewq}snwBH4<#EL|E|gYLq*W0jpRg(`gM7>UX zV?QSMy4Dw$mTExq{|+*i2cP_=epe6y4+tx`z2xLXB9Sx{Kun!o1I9sPFf~>%CJ4bi z8!oMlDH!3{OaYiGwjf0ebVF)(dT0?%Pfp4=U%5AWx}sUVu(dkc00(;=5)3AD*>*IF6$}_jY7nZD6(kCYE{X*JLCO^SOBP-=Zv6lirA| z{r3i>Th-zqmZ!!bbMiXroYe-R+Xt!IJa)iy!&uXsE?ZoHLd$Cbh(RW6mq(N^;bMNE zR?hS(PYiVsZ%IUO|L$Vx;mrb3p%QmJ;-NejMpU6Mi+Nkk#RyO}6%8^})As?U-8e-? z>Fi@FPAjsbAJDuY&8cTaF zrbmx-kujSb^xTqvU9X3pB=X|V?_@`!yJggK@PD%G50rhwEEr6UwGalgvb|m5$ZLm! z+fzs8FW>`B7*P55kuuK>YHy6z^(5u3C*Gqjq0lV1B8#@W36_^m9w10Hd|$%x{}LAS zz%j^g&mmg%)!l&@ecYdnB40sgCV-!eZk;BG$892Cahv~*)KaNJggqx31v1V_$x(;d z0v!e)PnXwN z@}YmabtR4w0E3?=$xMIl`yGkP0W0LZFD`tev~_3Wq{kCA`B6EJPo_}TD%6{XlC|SC zp=|SH%3i-0+z?XUv=X#W>oDIq_gI_Z zWvEGk?f;gq^PAm??G_IOr@FB^glCm(fx(#zKOV#Pk?VspsbeUCex)oJy$0i z9kRRJJelbC(qF;fMm+8h{9#0k&s+BdmS7@`ajsv`RlQM%w-oySLBz$y*Qx6+>k}X& z9w<1n+@8inKWT99T6kqB_Ak{x1LokqCi>Alhbu5ApN@a(EKT&6WJ`RT8`kxQ#P~ul z>odGm{uu@;L2nz*JbkU+;7MCj#SYz+8R6C*XT5{9m=<&EDklBRXmI?8OQ;tT@Nq4yNFrLA}%UK8wa1&QR!+;f2QlB6z~ryot*m zyvJUEwu{?IqfG54^)TUQt)rUIo}Qe^Oa;RX*)1gl8@Q-QHRVn8d#PCTEV--&UG(~1 z$0n(>Y&Y0yx(MXwmOT4Djo#^zu-Wk}d2lC`BJ(hrw-hw4!S`saz0esV?@vv~kX(H$ zLeounki|5Xg8bk5>fWiou`toc_7RFlO{b|xPkM}Ib~lvVJJjSw>R`oMV2n9W|FYe{ z^2>F|kXBH+QhrIFJrt4e&Q)$Azs)N0arW%+T{QRsE`*H(I z{NIf}KdXKEK8wcxPyJ!pyR!2oy=~wQ*wlh9zzM7=#y2dkvlD>LHL4x4W8V-GU ztb>uqG!@DH%`zf`$(GwCeKI%C~uO$2}%YQt1!c7?{aJnW9mh-}d_G zB=mQGmwXT1-V>V_ITCB}+WnpJ`w}_N?KS9HQc=S9Ik9B;2Q}B*#Y+JJT|Mm%j zio$VYLjM&K-LP}&l;{s{6XingEZK3VqE3VgbTnvhLU7;S-%NN^r%GLeAw#CYQ(`Gy zN*$ox%|AAor?oiocF0&urnxF7;CsPLD6Vb9ko{Ym6}xd;nV@ek)(_YHR8)AeD34&y zh}WEy`_yrr#0l^xhbeAax+QpI5PxKe*J*n>v}c&_8J_OgegC=7KQKv{+s}WHePJ>l z`;nJPn-Y&*%=QrvLC+|ok(7UB^(}_}n)K~h08=@GwQ-mQLsWby$NdMj6pW0<4-ZvJ zSPDXcqq*)M3QgUM6G~A-o&oJFxi7HANdnzI;Bm9#VY16pJJj^%CDP--76R~ooiE;n zq%sjb2>X!-VP{l-SQq$$;vl~l0axxoLMcT)=X4LRepZ>UZll_G4;D5ZT4mqmW?_^M zjVU^_DiYs3Tcp%r`G6iCaKr`X(<_gRw3o9zl~E{tct=A>X5}@Q-qVO`+G)(2XPxSy zF86##kA3;h=4)A6hdfi#m(RbGmV}ZDfRl>A;OeP| zyg9B${WO$)T8njn<@Uq9MXMMfcN`iWGIAB zhaFxt5Yux3)f~(>N72>9A-A03Z~VK>p#VOsamaiP6TI7;J4%x;==)DFHKjhzxxTN9 z{>Rap592STv#6c!y~|y_`X0Y*b8?fAn<5!@IN0Ew9-Ao##;7DBX_Vk^kzax3m34pD z)JkILb3qIE{){9_^^BF(82}C*ryxblF1B?5(sUJ(yeOns;sfoy1?!3Va?zExcXTpPUnWB(xEf}rPl)jEmlRh7GXHeL7_c+w|BXv5`e&X# zfA8|+9m`0ewjXplc^aKU{8-b0k|$bFw-a!@Xc zgpZl~egq}YGpEyv%HW=6!3hq|#)wi^%HQ7~7(T&xGfM>kpP9lFXIEO(tA~*yhJ^4l za?J)lY`?E?fd=8qmpw33Q>+1@2DhznaiRNzvfx7F9jH*$*BFXNVMLBJ+q@1Kd!k|@ zrid{YcR9P3_aRwd{%o5-T|#NX@hwBJgW^eOO?Rf6cNPM!yOyE@NzyB_O%*CJ8BlYL z*35B8$i~l=PtQNzHL2Mgy^QyDb;mDGd&7TjpD^pYMTm&MXa4uP-;flL)NFD*YackP zcYFjgfY~VH<-`Z8=mI_Lrl$jhhr7lNTR&@2cBU-KYYGQ0wN0H9<5GV>);q(^2z!&! zzKI`tIU?d9lIkCP!PYu{1j}7gzM#ILI_`Kc){pNUKT=F45Ou&t)Yzf$1q?3-4qDgU zwVLFbjUO6LmleQ%#!Ril(>orF0N%;o^V+No?%lzKGa?H-s(uHz5eBfe`twV|wrPrr zn#I3^spF*%CPWY$O}oQ+j{2rah`XhJSt#Tzt;q`mzd}AJeYG!XDG-x~pC%kaU2FRC z`)6r=o7PGqaW+I3tMKe%Y}%EOlAtKi72Zmi^#;~XC_(Uy?VV*|95RJ5ew;Q<00=sA zDF{O^*-25&4j35Iqf1+Ej_L;??Sx3!`w$o#ynE%;TD;{pll^E~89Q+L_zCg{?A6u# zJ7KbKxyQ(O+xE=Ek%|(@GHmg|cKw9o5HiKIxiBQ~DUO0AE{s#DhmbvZlQ1lw9_TCq z4i4ncH7}*5^(ZdZH^)o0pRlp%TGW=6A?zV7cCTX;9%T<+p;{S+($Ha0f2 zzgQ?UEVZ^`v_l~102Keer|%C@?e#K-uZBO+PRPoNrcTBqsno3xOkpGiN{}9B`;2S? zU0e|HpfHhGk3Z|A(C19>SG-5pKZ3gV5E0ABx+!h*7Z*L6gk-tlNt^No*%u6GWuf0m}FSz5O8a1 z@U1yoNB#~ugY zNHN0|Y#AId;liAqCUO^72u9h#!5C3Kz^PvR_{symy?U>0<6ybVyRW{!{vO$e1n`_G zHWWe@mcn>@+h{W>Oh5mP%&8w%c$5NBRU!ahSt_s?q~wS`XHw6ALKg2ou-o4KT_Z2e zcOY70Zjcfj{9gV#yyRjp1x9BZ`>Qcn$x zvWoBJDPL|aZL+Z8ZnE@q=2mVI*lqV zCGb7S$jB&QKt2Tgc2}(c*W+yeS*YHTe9s{%FEMeC*^jY>cr<8kFafV|-R0+UJ_udK zuI+TG-kYP96?22m^*-49_W~0gm}w^S{s~B$j*gRSC{S!*vM6w;2M2j>7LDmjK5?Ib z(_i9e<@b-2pQj}9jq}SyQrN@__Ra~@3YWXuWw|n-KEG-#2dB>B;>h|8C3d!Lvcx?C zD<+_+Mw30mn9)+LDNkI^t1UQ4rxKxF95iMzLJStTQU|;5%yza{Lc^3OSVqlJ#FQr| znOpMqPXfDao}`lL1^Ro;MwxyAYH!N+-UF^SfZG6V`_!G_(hnga49J9p587MQO@_6l z^$@muO@23$G-j>Wo*?H>#C9xC22kr_=T8vv!jeT=h4d4h$7UYaSNg)%FcJZ z6V?9`Z`QUb1mY{&ZBM~PYG5$iQE~z!@6C!`e>j+-#o|NIdt*tGJ zd_iMeznh-3k zROw0^OKVeVW5uRLgm0Vjnl(#0fXEDk?&^!2zTL|-Ky03Fz$pp2wS)mfn0p4GEmaxu1D1negb=sZOQ&5rgZ+G9DiFUaVeGo+S%4kuBGUEYCM5 zE0y_A3D(bH^70?w&evN%xu*mF84_u9*4vyEUdvOz5f);OP2SqRnF=wMpu<`h2 ze0S?Dr-e^jBKN++Rr_HX53Am-sRjns8?~lEWS)cr3MrbWcUD6EHAU89Lv38VY8ZP z$@O3>$_3xRe8VM%`1FJicRn)436HvE1%E`LP@RQ^sM0r&xVgC}D-4*M&N?G$%F7cb z(u6o|RoVNoX4OdYkXdT!&xpEm@6dz+J{-nYHg79RDo)_Ix(VswNdHsv75wq1Emuz- zA5=nR1KMDbzblm3f%+dLmKCD3?tB7*V)u9+pr|Me8?|3*c-9wn|J+(Tjug_J`Gszt zlxMzyzc7qXwyB-$h3Dw0#Qe@a=hUS~LL|b6)^_;XvgCN!r3&V)V`IA{uU-~}(n-03 zO%P;e)zO;SBL$#&CF^wWquTSap#RkG^K`~^nBwIj8!D{Wq1 z&(>+#CucDU=@oH?3ke7~`k6t449N3jZL;wsPo7UXoTA$0GH^>Irc=RSI(iD+3Odl- zyzzyK`O4VXSc1?uy8@{Fn{r+2&?{rMx+TIdypP}6)f+k#19emH-f}06y!BG4|C(C{ zde-#6Aj#HgJ;T4COEY<@kZyw5APw~0Uq3zQcetF19ujuk(KQHCXDH4HhY;n|&ft1v zzseN%7$G^1LcY}sWE0MO;ztjzxSMmdN2%&LO56cGu zt5Z1P;2(SA{>TamU+(GQUGPIRO_$ytQ(Mh5<4*4tPtRlH5PX=e!SBd4RVJ9aW7zm> z9fKpt93-RRayx7@PcRuYV<2QBTLBBg% z$|2=%K8cyc+J(vh10*KYUcAnAzgd|G1Ma8}y)k;yx`f#CJuoqa6~M1_Z9GR}jT~{w zU3`ffYCGhMpGr*546TtU#DHy{OWxx)@McUcW7;`bH3Q}HrkN&>4keJs=nddLO%;zC#het=Yf(I=4Ib+uTJng)+?o(`gw@B#4{@?Yx7vdu3|6+Iv==Fdw8T{~^GS zIobV=cmDO0YKD`2=W9Zf@Ie_qJOWgI#&^p>yO8P-4xD9}p&=bu)M3#No5a4@B&HLU z{h)6DT)yme8_d?W{I-XbZ*Fe3qOhMrH`eIuAI9CO5;wim*Oe~SdNo0U?oIDY(JzWi zlOUJH=`Kh_GXYqe5dQ&@K#0N;qDZeQNQBu_RQY~c64b!Mak@H!>ghwKFm&r`hoFD| zwKH^Y`*1smy#c36OJ4;bYn=#K(fy7tkRW9^-yO=Rq7Rxse})9AqWQMbBm9NprW^*Ua;p;b)~*Xp4!?|{+QTw_-SX|poB!C6e@ko$Cq!0 zG+oL3-OuzqjK6M=!MlJDwI!38k#2+ci=3Tap@jUhBd9gI=SD_Xl|D?nE~uC#x`WrK zS?mG)15)ypzW%IQml~a)%w=^3rcYZ-cBZ{*Z(c7!^oT3`^equ9L zl%g^^qRgSb!CEEOTldnUlcu0ivgO#U#rwKjNI$vmTTaf8q0MR8!5|13_xt}K;S#Vq zFh$opzvA>H0bl)kwPA|VV1S~J%Jn?}P*wuYn`G2B$+SdH!+jW+N9UsZL)o&Mt8!V7MhY8NxuEX|8cp5ag*l99neaH4hNvS@%;Yv{0NQ` zGg14?wd+RUI{jYf(6vAJw_7C@e1dD<5#6B?-$EEP=RbvI?yYj(hzSg9z4WWPsd0n1 zsB2+T-_UsTs`Il^2NAqg_=Bl~^Mj)=B9yqhkgNLL2V;gLE1Y1G7#WP={Fk9iKQ+jf z>F$ub^X79WYzP7w-@d$5f@IH7=wdBbIxiH?itz#mWcGX4&FL`s{f}OSJy$5PK8bl= zmSxA)-Uo`sEzsrW!P(ROg&g}8m3ha3$3U}<=T@mR(0}x34Ze00ztuJAtrahyj()9u z1W(Us*aNbKojd=e#ja?yBJ2GYVjTBu(pxSy=BphBSA)IWonqIHz9p?(90*8#sdgX! zNyz9~!Dse<)2<}@xKq1_cSq+!SxFiWZ#5fK^K_dT*(3W_eRVlt(WE9ywbRW#8cL$V zb~@$^D2I?p2&Fi>{SlRrO%DI!I2?ST>iX(~x2ZZG6c%=FjRsI&LDE4Tflck;So{S7o zBhC_byOx)lb4ZHTT)qCOaMIU2+bTm#s=nl{W=S~Q2f}tU8ZsZRdz<}{$p-tgsAB@V zL4dhy;LN-uU{@@$Aoy2Hd^=1jI5>Eah~}s@Z}2{{Zu{%8mUZ8UuQ^&?d5Vf;KpYF4 zMG;hpOCkOD*mDGO)X3eEdn{QtTKLE#Q%3s0+CxY%zLM)d)BX^{_mO{7*sWB*jKK_r6bTH;SjYm#OVUF~RTS!}vz{yG@OGxsLcTzMYN1cLgsuGErGVgod zqjGCPAGs_mE9+G*!xLka#y#@wJ;~82hXz01wu_WQ9M;JG1a^+&(JCDl8>MihM#g9Y zOB(txOvn~VbbKEj*9HJcTxC2*uvqjFw(<(b4=QNbnE~kWYAfLjNoGY~bKI<0c|}vX zxORj$I_d!${0}8kf~`=~;?hxuP9c`lBL%V!ECpWeFn%(@JMamd8Ow#7f`WqctMeoR zs-^UU7W9Xqj|e?Iy_sJewmUlsQ3I-}9)fBy*$}(R!jPHV;7C36eqE*D$V+aF`TXcS zN!zwO9%2RE$ z3mwi4upYElLCFwH@%zVT3c@EUt`Vu~d4~F5AKUkDj`O2d(?x1JU_(B*y3~nZ^&9m) zb0_sTH)&*Y!hbbfY&ztZJf5A4U67^f7ms02@v!Nn>CR>TyNS(jSZ5rd_53A+yciKc z`C;G-(LjI(`um>=l-w%IWYY*8?WF`vxQgC@B>e7Ab5eO0MwU%tl z4fNoIuK=!aG6MrJPK@kLUR8ra**8(zab+1Y%fqdBt4<$Bo1YG+M7IZXn0~pfQs<=(ub-rz#4KgIA&~$q zrX7~;6VjxN0w!RcFf;7}Zn5`|{VoNY{fl!7KHc_{m0#*iu^1&k)FgWH?~pOrPF7^- zpLEAE`=nJp2vRxlb7ZvRT4=JJu0~3irQlbDhld+?a`EVa`GSXOuDf%QZ7IppB&FlL zqeDY62`hk`x|tdC6s=3BqLk4ZTmhv#-59uP1`>I}lnx=B<%KF4N`^p9LVwy(GM5hM z9w#w`elKEUJx3(G=2~wOcO_i&*5)>A;Pd(QWocc}uJKNtsq6?2`Da2xz zWEpmU=A@UX@zeh-|7H1eHu9Kq&o;&uRY||OM(EQ>Rp>s07g~k_u)9Fm^I?WjF4QT{ zZ>7-0d?fagt`9KnR5=$WCqZUI;(PLEpFx+S2G7vrNNP#M^ThOk#d;^~4usgDBb%`Y zhb$1~$0_s@tlI#5`*F;j1e{jxRt(y9|3mLhM*5fGq&Q!RxlwYUg|7Mr9<9idwyRT1 zeWYpy?mFQIKUsi=)Wt9_m`>Gqk}m`{(R7Ap%4NOQfP7+q7Q;b(7KjEO%H%5pA z2-57tp9<(v_;cbo_YBBS4KfqJ_Q(FSSo_!ah`fRTCCeeWuF3L#&e;h&{Bji90c*?nB(+_~xli#jQQEhAV zOpO74Z2BLNs=fEoycK$I9YQL>90(yrKm0)4h5tb)#AYJrw`vJ(>EUea$oeMVzM&Hn zcpz;1STE!oF+lzYY&UFS*^)a)iX9{-OKpY96cJe9cB7Hj@u6e>Qd)-PSdhBd8 zi$VfuLBl;qqc9fl9dCiRW`y6`Hi^tZ^JB(NFS!3bp+Tx9?R#z^=qL@Peo2DjhJ*VT z!zU(kR+qeH8Ej~Vtn6SY+SUK{(pYXVbZi5uq}p0WXHYpwsxK}j-2Gt{Bq(?;kXwNR#3n z99XJVp$HP4VIbS`tr%u}viwJ#S4aU~kAAcOCR*UCdU^$PA+=|KhaAI9*o}89qoR#a z%zQ*hbQQE|jN_-o`hSbM*?fDeAWw!RX~1smZ;gwbZN8n@VN(QMHdW5%$+tD*Rfr(} zj2&O27nHqs=K~ERv43j+Vm>!FhqCvI z$Jpg&k-?>DXn4+a_Kmm>hAt8 z^q?FNr!8D`CI0lr^zO1b<DFv8B&>CI zbEWM;B6$*iHxf6Y2L}`}7j-H=^cVJmhsN!sm&2Y7@UH`R_%JX3?DeBNTp>h@4|7tC znLGxty?`acT8{_1aaLe7=2*QrE8xvB8ki4L1RD)srK-usV2QB~_)B6t$Yo(#VhpSU z3kXTQ6`wlE_@zK1a2kNZKY^SHLNASOwCSeuLTls?c$Tv(wt_Lex- zjRt(5r^7Bp?cLvUSq2-}=s|l2;9xcXMSHx$#w+Icy)Ne8+}$|(Ds);FYlH0U(`ZDr z|64srv)k$J$u@oHz+gYNz>1R3OJBTJgV*SU970CGHD`2mV)5&?!GmG-iZO*0odm+N zn%F);ugv=&Zyi7=A^k@;_$S}}6|9N*4Vu}-H1yE_)8Rme{pknTGxQ(;otbC10d~M$ zAZ&&B0~qK4|KDef^u_HQ#1V$UVrF|3<`&4Vx ztd^i*&=KnHcz(G{YN?U@CjNKL@Qj)tn!1xQf7O`T9)&l+QSh3|GGPg!k**#NC?z_u hNePrBczRxAqv)xK=Fg2>(f`Q8P<){(TOnf{_+Oscte*e? literal 0 HcmV?d00001 diff --git a/docs/images/entitytreemodel.png b/docs/images/entitytreemodel.png new file mode 100644 index 0000000000000000000000000000000000000000..2b4612150efd987a16a60adbf985615fe5b70318 GIT binary patch literal 9035 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/mailmodelapp.png b/docs/images/mailmodelapp.png new file mode 100644 index 0000000000000000000000000000000000000000..23b86e45d3f33cebc699c1172179e56714fc20c4 GIT binary patch literal 13528 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 AN&o-= literal 0 HcmV?d00001 diff --git a/docs/images/selectionproxymodel-ordered.png b/docs/images/selectionproxymodel-ordered.png new file mode 100644 index 0000000000000000000000000000000000000000..5c79d5539b4d056a6dbc4bf7f0e521cd8a870ab3 GIT binary patch literal 16296 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 A00000 literal 0 HcmV?d00001 diff --git a/docs/images/selectionproxymodelmultipleselection-withdescendant.png b/docs/images/selectionproxymodelmultipleselection-withdescendant.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7a69d980ed56c263429404c56b7d0a18392bdd GIT binary patch literal 10232 zcmaJ{by(Bwy9Z>%=old_AjqV<1SUucOaw=_fOL0`l15rOrAv|S?(UE-X@t>urtf=x z=UnHHbA7LEPu=(DbH{T(&vq>osw|5Kd<;ZELBW%klU7ARLBmA0JF(D^@1L2OcaR_0 zc5<4IC@2KPe_p5zsb{<>C@jVD(o$+}3kT_;+9W$4vEqxVp3qk<|FRGLP?HbNz_g2Xd`A2+`Kj62Hm>qG-+$BTR@PNAi#f zNqSQ(Rr?WlpHZ~Aw_$D6Xiq)yKZhMI>8EwuV^AKL+|3#v+aRZYxoShLv~Udrt)IsL`AJk40XO8tWhLp}=&hBi|u@sz*a8Vi9$HrkvjM zRS^LU^L^UVyBn^!nJq+#ldW}E;}VD*Gcn(JCG4&D@2j}EqiJ6Zc|Yi=oZ&JE!UQ>S znK+|KeWA(ZOx!0ko6Yzyf3m@Bv!-mjx+(_-zXx%dbf3zWKj6|P1<;eWA4<;*knAmI z@aHozi+i30%3g;DKrvabrZ!E!qsMJgn1Cp#o`jq}WlN0O-1?x~NhxNth%dvcs?gWd0N$ z5llIJ=F_$Aw8v-vkuCd1M*0Vy@-v<6LEo0Q^=LrU>=ZB-U%=?-z|cWI=#_7iPrq77 z+RG)|0y*hWPQMN5DwvXqPpTDo^7~y||U+{!%H#)(G z%e!Flt+{fU{^Sm$%@S;i5`0%|=Q{Og5?gyM-g+jNKvwiXHVFMOkXldkXN&Np%a3Pk zh!*?pZGA0+`ls1y+-r=folc2gGk>kaZV3uLWQ?S)Di}Msf#zAZm47qjzfR9)V;QqK z3+{rcwdQ_g;N$1FnQ!WM>TpxUarE_-z;Wa?w_U6O-8k2wwX(n@8R(~o;5A@LE~k35 zq~WG!?}o#c+bvy@LCPc-q1QA3vb-*HhI25Bbn#S<$n-#egv+hucPQhd7lksodD&8x zj=fPbI5hDGKfae#Rb|hrq;7<5mXGmE;!aObBTW&3_`yew%(4BIkC#DN!y~OnhWsm; zZ;Fme`%cn%XMTWr?g8J35raYP_-8)D>XOF=L;Hh0i|K>iD`3C#--!D1@}stg`^6d? z@!dnbZanb{`=A>SC8T;WT4ahv5oWC#hLp&FI%bb#g$?lNspS?xO8{3%V zl?l1;Ll=i6=oy2>Y%F<^>ALs6x~}lp&3+db1Gnx1jV!4hdegNVcW z@+K25DiR-wi__?$W;cNgyV^D!kTpRFD`B-`L{jtjE)c5$tpN>=tN_Rce0LAXgNr*5 zh!2Y~G=M&JYCtQa=fCEFLjxs)z*93*=CPkLMz-u!pZVGo{zL(&jP{KY!v{bn7=h9Y zr;2R0r=vj#|7)zXnRUAc6d-5-gsdakuV@~Z(|5LIy)M*|Vmx;Z^1M9yNdd@r4}3>c z&xue;8qwIYwz3j)+8tx);SG6yRWC?(`@X{jol?&0I9$A=c;w5Oh;DX+R$PQ^w2+km zHp6a2WF!7h#=tg9TuO|d6;kNQNt2v5)_o<+Pq$-DZG79UBj8m;<%LG`3}aINOuH-W zYp-uAdK@MgI~HQ!`x)JwF#wx&{AE#xj2TeA5$!s8zgn4&;rY3IBHj=4*G{yxd{u9P zrBGq?*JRj;?cSsCnXYQoTvH(}rA%WB5&N{m;{HV+!NoL#uMB#j^V^F9J?7C=#xpXV zo3tMp8}8qVbkQ(BKMh^vQb3Ev?%yF*3|P;K#O29?X6s-YRIDBxltvSpdXY@-XAv#TU; ziERmCr20wvN@)>q%ZL?yIb$ljp~sbJ(jDq=lzymmR|N846;+Dg%MbRj?jAeKiSEqy z=NRQ|Tu&zAoxRyC!vH$B=|~|1vPf6)8^=c0`d}T~=5jNAA~C6!piv9_K)iGtmvo@A zDJKGpypFCN{W)(OOxcV%c%g#z3=qv}bzL#qkFWC7F+%|S014~qo>+w>jGp=+0wY5k)kvy>~=)O;zfb^$d zJ^Tg4?pJ+o61W;?fADL?{YmH^9H*j-%VW0F=5U_Rw7F1sLd~PsGAhW^tE~DL_c&|8 zq?1c(yc*{Np|N{O^%QYaQ4^Dpn;fg78LuE@NS=HIgPMGd_= z8>W3)@zI9E3PckIEw!*M=$?&Jrg&P%5i{}jC#-P0LWKbxKC^5n5<<_y@PbIaRZv!E zmR^LsAUOaRBMXW-1Ue_`AeW6jzoL-rHcwhnz&@;};U8E0Vx?gFru4>qp~?&H1j%6? zoAEOKn)NavHuF3o-`apE+gNT32>-J5Yp~4Jy19qc<&Onw<*VtecFkIN8bZIb2m`v~ znLWv_Kw!U>_H=g0O1{7)Sg zWq6$1Ot#qUCDE?~*Wd+^)6a#i+l?~(WWdneHKZK{8nIBO55<=AoTCxAkN>p+-l$)! z=Lsk(%y6gsk)K-r?f~0_#(KznG+!Why5jhiGM8r6z-f)2SI3vU1QPP@85eUgoiI(Y z)9}rw=`Uj(OPBqRU+{jHNMvs7eHCvXWMj+0=S%U@8~yWP>)Rc#phwBV-ULQhNmW6S zzoG}>15>4U`AZkC%@((uuB*k1xau)Gu6zjt^)-@|lPvL=ts$Avs9)15b}iLRNv*tL z`v|ID;k4fcUSB;x*MZp=DnZiFP5qaHPXGYOI8pk`fwY}TtpO}~RfX^1h$jK)tZKyN zlCQ89%Cn*gXE5^if7e4qMNRzcd+$v@N3g!Q->U<(gaVpO(extsZG{I|N?@Oq%>BR- z+SDGuSVZIY8g0LI^F+AzfzWV&fbX0dUH07``@8l6OBfeL|;OoQuPM^C_ux)i(M6&-o+4z z?jx6-=cmvngr8R~n$MKiO`*U*`iMs1*&bBOqCja0o4`20I7^b1FqJRx^~XpVxqxAM zDv8acj6`NlPVI_mqcM; z*e0wJp)F|p`25DU3TE&Yi^@W@#UH$@!p^C)xE+=bzB54-n`$%ptW4II?5?Tl_$$j< zlZdO&-?Umkx!nP&^wxyN;*JYJO}Jap*X-Vt$%fD^cLPBSuI0Wmaja zetg#85Qm+z(!1O3w{f9WTh-o!AL^3*_JcfIbGCVN#wYeKX{_>&cYR!bctJ=UK9p6^ z(6N7@Xx+uS^x|(Rm#RhN9C}!VcAE6OEhXtl$5W+SJDt%VPnzEUQOD!HNOEK&*rb|d z#ma3q-!1=Hkm=T;3HQi&=*ll>AQfEsEKn)reW?xuM42_e*PzqP`kOFm0R{cU3xe^cHbUDe^HJb{B)=H zq@QD{wUOZF&%Qe|DG1kceQ5JF!N=PZ9g`H_3JF&uCZn4^YTA0|QKWDhdsvIRB^yT| zezpHD^SjR)K7rHYqBjwgG?h?yM}R=k$$o=gO~G)q_~lvDWh2d6k*kd8iteqB@`2Bf z%ayCihwf;W1_%(FVQVwpI5m;Xhdj6iR%hZ!b8+(_mLYiZN_|H_EDojl$9>S8aXoByY1>DqKyMJdf{ zFA@@>9>)^~d9Pn_qL+GF&>K>l@fM~#)>OS^`-_}ous>C_!el2UrM)6jJ>*93VPE_Y zU9&b~_6jPICDYAzP)bS)k~iJRvm916#0Aig+h0Lj6RgkXQI6B|8h@AAAUn9@lL_*A znhmx)1_f@_i8p(OX~W6g+RxB*QG*n0E9js1;>D_kXYd5KXLKjNfJsJC0X+d)5Qimt zY=k>x^i(~`d++wT+K~j(iY?bxFl=?-K4IxXf(FC}af6Zps_(g*lg>~s}K*=L`d35lX= zfb-!sqqR~C%EgNG;?agaMXs11H44~{#f0@ekD|(@Lh@=rtJRazh@v7Dh9Q-cIxI=I z8Es_4`qcjEje#Z>z-_;=K5W%V5#v(}J}xzV8I>xhVI@`vdFrsV8~tIkE*qTJ4D$oT zzl)+`S{twrD2Um3`k0K0Kva%$@NX};yln(jTtdK12}<0+kcpxe#6>)>Illn**}sgU zLIAB4pOhh4c6{2H*|?+=38EIPs+@DP3mV$2^S-h0*;5yFSQ-NbzJRTc*atAB3E8l` zo?U9Ugk4x7k?{XP>bL*(WB%fxNw@4N51d-i2{6(OucQ`abx@|RWlRa93bZK>2omkH zA9O&jZ-%^bSjx%!In)#MZCCK^r;VpJu)?(&Fcy+86IeQ$qSFBBC&1g6^&NHN$rOV5 z6Wr+>15!x3?-LE$12DOO{w@G~Vx%$hiQ%-|f1->r{60koRoI5Lst)Z^j{#{9t3&AS zGh>PaO)F${q0&fU!-_V{KZz0ge-fic0o;qn&Es~e#EHHSg0dcfpoNr$|72K~E-JSw zs}rEz6L2)jMjA$-DVN>0%rf=c5zr;K=Gv7DHj%18$-E*9?=}(8E`7Jy)z!7RnYV1? zDnzZ!5;(a9wIBZoo%DykY9uXye3(;{hlc{-Rp+noJvZ zDXux&-00ePvxWT9<$dzjmQOg%reYRn(ru?sN+7!E_eN{{Hd*QKxv&34-zbQRn`n$sVhCyn@4s4bD}I=Kbv+XLV;8~E%b^}Oz}q=@JuGd6ex9V%u!yR z-7?A zja#)^bzPi61;{+0hkA#Ch0<6?VrMtEg+LFGN`8vl8|ebr z;48|Jy2soCIL>75j*&2BADdrw$5l@Tmdy%qYE&P-?k$XC(p^9-eC+^uiiWa=nZXEY z838y?kQSZgL5&VTt5{ODTyqB_885Ee#_Vh}b)N1A)8r8a#hzR>y@+)IWC+=uL;Ea0 z>6d8633@KeCZ6yW^?h>ng{sYOXAKVjXyBFz86bqx-HzkfI^XUVz!yCFziHcg=CHQt zNV!FjmQD51_QgR$t6+Y^Pnz{q2+^&`+6{T-vD4@YvFes>>L`Ih29N2e1-}drd9*mm z8YeM|>9Q3sz0<3fCdSJZs*QhqbGJjVl9_fwN_%j+1?(`+t7}9)xn?nx4Ge_2 z)pPsu=pv)hP{kb*?#Gc`Q0uFat!62^=HVut4J}UNctVZe$+sqCll4KB*%d&+7L&#u zn8VusFGDw#e;IPLoAcdxwV*KX5@!ixy$xF;e@zm#fsyh8bU@F}wh&(#uxVb>)EbZ{ za%OY{HoGv#>8OXFP(0ImcA62x5=6XJkG6K|#~s78XD4K?cyfDhfN^?D8(o{9FM}0D zRkruGTP1bp;p=JCIMoS-sEQ41PDn@1zV^<@%~}Qy&m$sw>`6)OjUTfmdF8BOm@qg4I%p5WPkqKn0ukLa3yV9u+ za*^-TC70cmYcT-A_3$>5vt6Q?w)mC>^ZR8I*N9;UqV(aWTB8@bMK^bsEYh493``mV(; z-`_7rOK^q}64Iyk-S(ZF7d!393yk()Obn=gpbwh?&;5t$qNslNv zRMBS#?UD>?Hz>uzgppM-ufuo*REa!Ei~JxAAlk+QeA$wjn{Q2xs$s994>Cbd=eS(* z(183}jzMH)-`~A|V{Xmv;puMecr)Kbgl9+?L7N&}rdsf|qehA?3!2LdEBr(STF{e6 zUXVYCW~BG#kx>Ooad}uht^rz6%n8T=NG+VUfgsgf1vB#HZ~oC{Tiiw67=}z4MtfWN zFDEY6hw5B%OP2BKKe44c= zS4`ijT`RS!Q%~bQybNnuBHq3veSU7%dV)viP5o1x>t)EA2jT(Lp7(l@mpAFy^!FUv z;qKaD9-5V#0m_ApBwJPtkd)0$uNV6+xAgD5%a7ON>9n#h z9h~O&F>xN3Xb%lfY`?Y7ez2TRIJ}I^X*fl!?V#1_KqD&2>N0_Nd?n_uw?0^U2mH8d zl2%=rP}g%XN^d2BjJ+=!TILZyU+y9{v<|O2+L^dgf0%SlnC-0IUBjiqKxS3sf{s6! zoArA$Jc`y%!SuiRkdbp_&lsgviewMS;cW}1hr78rMnyyz$z%`{2pXpHTuisch=VSs z%G+}noR(69JYhFirN)lad?kfPK0++4Pg?_4R^FGHwst<2H8o%JZ8*73pS>u5qr~64 zqqLmC5|TOINg;p>gR4nqzxq(#mavL8eeCi!Y( zqHcCDnWA@hz4c(!)_Jk9-w-)R>?heb)!rVHv!-EZO& zp9*T~@S<>$DMc{pu9VRV5{j9qVR}$>2K!qFAYmz>?d+CS?HV^)R)IiWakvYP5Hn+l zJi7oE<0)x4$A$Nb%q73T=|so9z67`mEB@lt(W~Q9zhNS)e$kYOZ>L!j{w(+7bV`Rx zBA_MNPTt70i7eR%#GMVM# z>{T@R2W2LoJ-Pa!n!+Re-4%z#jFn5xF9jDRS{1lV(4f~76(C0RL_&TO5C2`Rm_Ifx zWd-1izRdNY|5#hHm$Rd9UcCsNa*SM3@##i=DY?o=s%)C!HXssFMi%f1x|?wc9O?$` zgVNu%EZ{2l3ymUo{gObOTUZc4Q3E^~^eEbTL4hNUUI`I8Qon4p4Q5**KCIDGa2eP1h>GGBRS+hNVC z;$Cq#5DG)n(Om|S*8tqv=UmQaltNpip4?{KUm##E86;BVf5Na9hWW<72@6?tRHZnG zvC0z?Yig?9T*~QZ21Uz;EqH~5H|{=upHog7;3%W$;EMqNuqq*)eP2DqGaC9bFdX2bo| zBqN>K{MMBGz?uZ3$O4wQEkGi4J#~#aYovH}WU&Y;$4p|-2~JQvFYNPj$BbPj2Is4(w!#~eZQsf4|hX&tw3{J zmDW0HcFo8TOlcILQTzz>d>o0n1I(*~B%rq$B(Cr}?gz1ab5#8{LSVnJw{({B>T?e| z_b#`MBqTv?rTBYY;i?Bw43ISs4z@?E>iDZIS55Wd>U_eFq#d6|$A*@HRAQj47%XzE zQ^`ab^x4pA7CD;)@X7UEI4-&&KIuQ;RF*sLJ)MkvFxybJWRlb=vpZmN^pV^jV_YZOPZc0c>tHW!IR^%k`e#_rTUFA3HvHK~VHP*#L=UbJywQ~~0z{Ha~eemSu zYrxkJma4Vvh+}YyfX!J4dnTO@m%RZ-qkP{BLh*1?rBp6QrMp_AZ;!To)cT74b3pMF zIaLbh=Re6FQ*r=2y_iZ?&7RF8TDjiNH1tu73NIbIz3&0oQnPPZ4mFF5tZg+rq8S4h ztnYiJN!~FptGUEc`=Nr=_}&V8kgk;eO8t7eX&uyV1YJ`02BTE2rM1uKHSseY*v{AH zYlX|O)F+Z2(tSOSYWMd*gX8J8KvgC3v>B~xc5n}Z!#`6#qAARe z2=z)2so) z`wROzlrfS|Gblkxddu%~(-mM}fB0&aFT9|j7}R`|xBoNvl+@_c8`eLCw5E`d>K0#D zoSzMpJoVPI&@)Ky{dTNh>%Wdi6DvOI6o^eZ~+H2M|brf3!Y z*>m&3ERsA9<%2ftST1)|%V|o05$d1A)0s_ds`-zO>+9=?LW5d!^x5ckY*pMwyca#& z4Jt3aUaazBlnuC z$Z4B*XIN!@N5zYYZA?YRqk3B3R(Df2?2D2f#qIHn30w7` zJR&}D5etyLk8ZwowSE${A#~?D*Uk-vgm6H@0M_jm;Q_1B4Wz|14Xh@1WAY8pK;tqY zPe{9~)}h7pW#K|%KpV9bPZ>}ziATlIA6F?K28*v`~u7Z8rIHfahef4TqgJ) ztnEzwg`G3v{X17PH}nSq&MttI&jS8ws7ZmK0Bx&6IGZ2402EN!jZutJGbagZBU{v~ znL`}G4oB4N?F=qC!B$m&$n~+;WGGuYuJx^zltil>Wqb(#cMw*Y8Rp|(|jFQ%CydFIq)tBvnJ>m0+`-F;mx&Ifuf z4>EbJ|H4$V*F11iFRO^x%>D7w9GUj-e`lJIGyiJy)3&-VG5AZss%riZ0Me4b0_|j} z>;mdgfLc2l)&CgUMn+YLHfQumebv?Ta%4dbrPr`-rCkR0LAodo2*-l68ewAQ>+yWl zaN7Igmg@AT_!X2~s!Iymj`MLft9pje_=8qaPfGY$7{3Oq2vc=DANR#vJHs$V$hH`P zuXzD)dZ%LJ-M=18sqB82n(F0vJLF$x348GFDDK;CW;x39QUAw4*4jCRdvVVO&L8`V z|JpsH@!ig~ zH^tny|L^dek@lVM2*O!*gZHnxZ281VGVNzR#kY6&(mozFs_f*gt-cP1uD3mt{i?%% zWdiNO%LGIp)(5K6X~MsmgWuWi9@GnS@!S5zPwD;ZeH8V|ZqvU5kE{M|SXE^s?fCzn zVn4P*w7)T9%K{?P=>L6ww*o`TTNdf4^!&!K^sdaGYhH3UDKVYZFQEm#I_Y2`3C*X2 zXs@i!eQ^xHtccX{5!==o)ITZ4}Cb~QNcfyM>_>auV#szfy zP>3@Htg!_ZxlA|c&f(%mtD2nf<3NDKl}BHi8HpmcXhOLNDM z-*wl$f52ULojGS_-gw?!&))lo6QQCci-Sdq1pokW$h9J{5cY6`g6N=dlZ7#T=P@&6?)U;B);U2%Q!^q!(ifr zPY)rzh*g*G9t3UgLeiIek6x{6vRHzPfqUkKksfFByR^xC6K}|*K+C*cdrF5Q;*UQ?^X(tvFcg6}X!2XQHxxPw%IF0m-W%fLUyxqt zz|u+QY^IbbCi<%(6f`at`NY$!h22P&n$L3Hwt7mJR2(a5;q@!@>Ezu`2Gv@SXgd)j^mp{}QH&070LQDWdK)hY3oWJTx8?MjW zh*$ru%g(E>j~&#w?&hhZS=CKSyh;d(1riD;irAx+0mD!v?I27}rqAj0H|75aKngNO z)CLuYf}vnMFc1tx>jZQH+My6CaM8#uWRFZxRv0h*7AMWypn&Fnf!Zu&>)SU;T-nV^ z7j*~%A8(=qk?|8?Vhr0Kq~~g0!+yVmvLQu9ElL?1E`5%1zK+|?KrZ@S0GtSaNV(SeQTA`gg!~I8&3I9^JV9l>A=jTyhn;pqtr!FG0L`R`H|evT6QeUOsJZ+-7?1@$C7e{CRw(9 zDC@(6KOM50Zv1KVcjL?SvKFyo6DTmy35X7gHA7fyRiFjIe5q zykco{(aBD848GM>j%?o#5qEsOlCL1<`N5B+(%RozWmhym4$DrwvJC^@lGayOyL)8G$oUa`UobW5 zXX5fc+^3qIrR#=(fzo<~(uwCnRFj70A&t6I$u&Wpl_)4F zk!I!o$Z}X~;Mx7TP6{vEa$ZUbJYuk2OKk4724FsJru&9Xy7CK`moU2@0a} zjNuX{g9kE=V`98z z2*yKQxGy_9TfH4gghRszz2c?r`X&0Pl_)_2>^nF>5-jN`#0oVy&1T#rhy_R}|FsjL zJQvztqQ_UA?%#PSA|oTS-V;5^l~%lc&F9`_u+gW&L=h%oNaS|(?pHFbC*o$NLZo~3y z)eJ=pN_A~#wk5Yy>*-GgEz-LEB+mh1@OKcuHW?N2_cG$qY0#_TjZ`&}mbB-Z8!ffw z66Tn0OrnbhitfqpjV4=~d~_#>l(8G8g=X%*-uS2^I?bE<%eQ=%)o5=Vv%Y)k?G|Sp0voNtr-) zhA@i$JXoVmF=2!lDW^H?)(=~FlB`_Zw;Iq66twIgRb0n4XCV5>biG#&Iqcov6Sv+^ zb)0z}l6hTNmd!WNdS2U?E_;ZS*0;Nq>-d-CH9BGn&vL}E`AiR6OX_HK&RT5!v<)!E z77`Q+Juqvn<;52KHAX*q6>mDCB+-+#t?Tp3@^zy*6BxKiCgDs-io!$Mv7#kYQ?rzv zC7lF3Qr!&rkSYGb)Z{!6VNm|NMl(j?r6Q`*G`KAB&_>AsW@fg8eXW|1ewpG4%Pgye zjZFJUdH9YtzRX&B2r04Ex5)2FVR~96#vxR*u;e``{lFmtBMgI&L?gUBWoQvH%8;2M z?TSvWFxm2RyivZI$fa}9Ha&`joM{}@Kl{87B6ZKpwP{;V;x3xj^26BNxmEo>)V(-h z6&v{DBXowDwQOj!oA7fYr^>0NX~sqR(lwf+JzZ_VBk(@;{cV7Q5PJkCSSAU7g^3&W z9a@L@rl#cPC^Ld$4ETa%mj5QOmNom6<>vA}@Nw?fQ@QU(o=nQOvU!fuFEf9({}Eha zc%Q<*yuc+`EIBuhS^djnn%G0rD`+=V>Nx0njf*7End=LMkHNW27^#p+kc>Dj9tG%Y zPtR&IHK~=b|I+DkbuT^lzEXIrTs)fE{d)XGgg%JUT2aTmUNFD*%7HzrhCipel--CZ zz)UEv9-WZj?Q7#N(Vm2_S1u25CJX7y=2CW$AyZ#2s&>e$U*OFq5K~G*C0bggYdo(>(Y2$ir`pLeT&U3?;B=Qez zrJqW8*(Gg#f)bh#l96PUof^%Yn8r#Z)wxGjU8l_+yUtmOg* z#xh<=@3#?ydx_H@j_$Nq&Y2#y7ZI2Di6E`fVFs~AzimEug_|Q0hgzjqB0~Lwlvfis zQYYA%#>BwOP(=mr;aN89kVjm#r|7K>Jn!r6KUuunm?!ZPAhUWBM!%+^MhJ9tM8obM zTo87Qy#M(Yd7$IR_6Ww%=6U6!&oK&BEbnm zZbQz`&jrqi-e9HQ4^=D{KdV=&KbKo?brodA2>l&_;(&%0*>2=dElI`D^cPUkjq{j1 zs_I(3-h4N(4!hMsl15~pTb@nDPQ@kXBMRj$)EL_c6J^QYcJFMJY66h(&#-&S> zS8eOkf%zm=HX(En>+bLfG2pinL`MmLBa{fUwL*|*%%(Kt=ctCrdQ3=~?>GaIp( z(N-<6I0#}*!g(C_dQJ+(wZqej8(|B71Y%KkwK)?*O;;$|Muh!ByjYa6(=TpBwT7~T zdAFp>2#E055_i4Y7gP4xZEFn}-+EUWN(97@f-Anea8L`)y3#a@Gx2(G9>~zb2LN6AR| z#mEgbh7$x4mjVovN#4hNsCq z&AgblV%o!Aa%f1YFlS=B;xX1b&6co(e@<_f9zmdV(4*srzcxl!n07SphjVHEcJ3zM zE40_5p20lh9zN#G;5&!umG4I0*nN@bLVO%9ANko=cmSLJrthtudAhHe@U|ejzowMb zRT465Xg`IoZdvH!|6}d`?Y(`+f=Wr$faw>Pq57OiLb9mt%^K&8I=skX4nbfnN}c$W zSV2KaWeqU+M`lltWecLXx+G)JUd`4`CI`QxX+7SO7h8W_5FyCLe7J@zQXvXC>6w|z<@j6=4)5SJLbps(D4@}D*8K?JC%-do$vKo-V z=X&do3_gALgalu$!YN6{_P7)t{ft%d8vEz+^jn`qOuo@5OFU}wLIem~v!m3R+=RGv zz8yI7UF|+HrB1}^n}3w}M4UZy_mwMX9VJ_iPOV^%?2Df6JSpiHfpoz-#i^z7!fAY; zVDm2HJN=0RH3t7cSNXM~w!-}%myT8kU#v|uNam{up?o5mX@}}Ty z%4!(-AllrlYb9~@#%e`?Gn_;9`?U;~AM>3VGKMXCAJE#BK9|_h^oJf~r|jO7VG*V@ z*to}>F1qc@sl$tMM>odeCCmJ$X9z4`@}s%KJo!aww7Tj0I4ZRBsTeI-9e9c?=J;(b zH|n1KXZboBIzp_~M92?}_G?ZXAO=8Ml{6Xzdc!~v>ODXoZD8Emzm-^0`(2?xoP_%tA{zx3gDrQz|AhYHT~AByc4%`$#f% zlX9%7!5_5qKc`zcSU<@vuTpFPS;no){YTC&8Dl(x%7~Y~Rxz~Q09;HTAK#>RZ0Bef zIB)DJGVXpjcrE>08XxahE3D>zBlVfsU;;3rR{jx1@eTGcVJa)e0r=<;d}%1(v7VaDAq_!0P>Dxj(waau>Ee-vU!Xn9MC@6g=H2*kC5Z-MXOvOZoU<}d> zs%aNS!#K-)O=^6u?v!dB&`0!P@YTTS=j_2(O&eOMnwHqAILAY65fDEQjRw{{GJgA6 zC8XcC4(};~GoU4{?c(H0l`-evhm*cxDenD%*N(b#G>)Z|=I#gn*7M9}WH!=#S7N8< zRFwP5jP+Wg?p+k7iMbhl_Vn(k5xz|NZ=FDC!;+kR=2t9gGvCNvWi|!{dn)yHxD*3mC>T^kL#?T-TbX_o`rJ$gXjNQf~Dl<|*JoDZ`H(FbHx2u-j z0bH&;UNLc4nSW=dOeve2Cs&f2Cv5*)sF8V^2yN-jR8_tldGZjtlEF(t;n&(GF4+F~!J%Kc>Da&ua*h?$k_o!mYt*tzP5Zg6mA7+1k0$p>FTuJ@0sF9}B zY~Q=vAIYq`LGtq@L%zwCK}RKe!1NefZC8y>ZZBr{7I6k#sWe3)k2E7YgwcBYL~mXY zZBZn{4`o4LT~~8y$bPtQVC(Ff(p}z+82D2ino}gw>b_dl+CD`)X%u};;~+uhYQ_>o zm_)HmPI~(JBAQ_syy$lRL5U*G;wBt&etGV7;t9$k3@6IkLj%3%PW~;d_sVi z-t4;7^K1P#rXM(hm(ywCNfgAZgzM_6aVe0#8U;xF+o_xU-T0~xh78Rj6(QLd$d#fM zGQIP2ISI1`#RObk==e)XO{8w5rIA9)uVCC7&O*vbwNK+^Bz3bM`_60Y@$mfghWKa5a^%yxo$v@ zG>!9I-s>>}D2G33RRi^UCq4Dk`5#ox$6M|DBDqiC87O2*NgtQ_f7OV5Xk}48p7S+A zmi6kpVj;Y!SD%tpEaP65W8Dm#=`FDo= zt0h{onGEM>Ds~KW2iYS4g9$u>W-jqsSgx2r29mR=aMK_Mi$2UXw;meMj$4>GCvvWe z?F*BV6{~5Gk`)FaaM_IoShtEDR%0Q@n_sEg4agV?3#|RV{pjX5Z_7m)0*)EcK5g&T zrH^K?e%b;#ck{gcs+W5EY92=SWs-#OZg@bUTy8_I7~=OURLROS`$xFHtJXlw1r1MO ziVKW)6zdan;^#56LpUm-a5%CFHtuS(S{#^Z?wrXVs{sp4>_!=Y$99K$;s$qH4!YZI z2d9QuxFw`GA&#{$p~Z^lQuDA}u6!;JPVs!Me5(40GfTVMHy;?RnLvQj`Xo5VXWtrA zYrQZxq>AiF6}QkBcBgmp4Fy$-WeKE26j z2(ZH$tO=yRelsQDoy-!*TrzbAKhkdS?er1jl1G7zR37)<4ofx>&0=_rBez87ND%zj z!a&f`Y-F}Q$wb5?Nh1U?nF^FM?3uHYdaA)R zit=WRQlE3@Xiu^hw$bG*7!us7qe}e@ivrU$cIEv@o0^gp67A|W-OS(i?Jld?_0hfv zyk7ShqA-T$_JvIQWS|MI=+pxxlu0irK)}$^qJn#1|Wb6`3Z^G0}m|Ui7Rd_kK_>Z{~dcM&1lbgZAe6LU0YTJA9T8M zBwL}Dj70(LfFI;-Yzja3wch@kjtn@pj?AiAb44EChlGr6NAm<}XlR(yJZ8?h3E#gc z{W}%%Jq4~*yb0(d-N6a0DeSj~Vrc1uss}I=;YgUgq}Hh0b)G%D*~YDM5)Y0D#vP7qJAC`{kfU6n+5*|-5>QToMSZ7f&5 z$fR7(M47L29T_?WF+O5g_wH(0xO!Zc$>TwDm5t5A9|oE|iS%sJ^Nqr6`6j8MxIQHGfU-z+rr2f-4RJAh=d6sAx_dHQXz` zNTNPDJCs`A`D(7)F}2c)7R$TH`QhrmySPqMwL1G0-=woC2KB;viG&a97w?4H#Q&9S z+Nkq8*33B<(FUaM-Y+m1~yzM8LXm`o(Q4 z4(D=d16Qp%dd*Xq4CwAXfF$VP5WIux#818(`&Sx1TsK#M92@XKvQFu>NfE#t=R8b| zHihimM>k4Wde#5_yy_^P88(eXDX7hG&?hz8$_*cNirN|e+4n@P%A`^n?JehQorD!>k#uH`PX*N zBtL*b4pfDNi-j8ok5CnYS%I1|IV3E=S}U{{9KhYT6AnYE1~)%Vp1j5Wk5Jt1pb%cN zPAE}vNy)v7?fmBJlsUo11|h-3^GWNPx`iciD)D^z&+$Ybuh=pe#dOjWT2D8q+>kL1 zAj!?)LO2AOi{qg1pihyFsq|S^s!HKufC{Ck2tu`!l@f&M?d1Xybt~Nrj#K%f^%uLo z-@1juURGv)|IQ~XGuSZSYIv)?hDoGmHDq+Z`}3=wEK66gqJhQifz64i*_^+!xAH2A~?|7cl&e=`33zh?NQ&r4m*dLyFh!;6^ zB&FUBF$-$9esen7mrc7}WZfUnC}d}0TJRh1g+Jo@bS8j9wNT8FStlEu`x|WS+hz@$ zO+Ui5(DGC+m7JZPn^Q9TDw;I6{S*{@PzI)xlV7)h5|(*P|JF|Po&VUXos%B>`C=x) zQ#1Wcb+(0rGESqmO{8~Qj~TZbzpP(fsOKIjz?V#MWXa!NoXu9O1I0d0>upy$swJIuLPN6TS|H>l0+E2mclj5{iB zmqk;g24eS1^Z9QJNosqEqh;u0+pC!?o2SoQvaSkR=GQFbaW1p6zD~vu2H|cJzu?4i zMrKq%Mtij&a1V3H(iI8;Ga@pBbm7*)GfZx2T`z5fkCwCQ?Nu}EH%jk`my;J?<mR z?hZXkkxlALn>7~M4B}SpfZtPB3u1p(Q^XcvF|C2zdk*uhmJ$ya5ki{VT7LQh9JL2A zaai9APtJZtTJZJqfxnF~vp*haRlhJv{m+eFBx6M7-KNwebVsbsrci^ttSC*-y;`vj zlNz62?0IkH4Ny~T(De95*%v0@zX|?Ju6Xnz#<7>+(8ed{FQY!x+oG)izZR_m!>5yvhxc;dtwS0%!z&;d($F9n z-+tVj+XUWmPw4kM9;4pFTzecM{7Tuq%11o#jgCR?sc`K@V&-NWor#_#sH6CAokT$< z4NeGnBV^qSL18w>7~T!lL`UjFvPg2*y>#CwN#5&B{wM^A(rN$`Nt37q*n><|2aL17 zGmyCLnVXk|>!hl5&|ou8zOf7Fc3RwYyAXM?4DOIoU2ci#y*q3a^$l$8#O?wF4+8!^ z+9pU$0qqvXyI{_s6`0)G#H{UwkE3@H-#nx>pL$-b*^p^E8WDjKE-{ciG9b%ETUp$g z!ndh=vFc}5LC;m=|AIt<0Eib-#h%f<=c(tPIv!Gi&&>(ol-r-s`L3zI*Qb;r{J|LQ z4Okrfw!w`^nt z-0qisdJ^>y^4EmU;XAXcYOB@rK{1Pei&^^h1wT^$u@Y+B!p-`6oceF&_=L`;*Vo;7 z;=7~a`96#^ND$;MK9B3)mcZ9mGGll^IdSlV+wKqssVN|NL>$~!hZBzS|KMsTyLC^H z6bJM;&t#`dYxnVxOE8VBV$on;Gnsx&3B@tXvpV(Xf3ZM_$J>Ubh#zegw0<$@v_{_XSAKuSbM1ONa?l@w*Q007V} zT)Ue9gnQ#l)%D>jLT5#NHvj-idGi37(+{2i0Q5~tvQj!;lUtc#y4t$1?lXwhF9JOS z9rA=4t_Wi3LOuN6j{!v5HP!qK&$(0;a@7l#FS%S{T_FVXGPUQh z!Hmn7&Ki!jWljcu`(YVF{ridR-TU5B6w7B>EL=}iWA&Hw@l#PYJJCYD8cmpDnNkv+ zU;|FB{zyxxxL)KdN%B`IZyq1B4x8%M=mAvtKKDD&OaH_voVn&%JuW-Y{8c7 z0``DRi=kP6_rE8XQB7k|J|rc1L8RYz#Tl zBH_ug1|cEU*p29vXR>!yu>oV_DXFos?%Q-Mw6&Ad?Dr`Q-Bb953zWY-Zxf=%h&A9hmMOA}`$GLn5=oYvQTSMmbC-C+}w)7Ov|= zXqtDE;re2oe_rDAJ^mx-eHqZwx#eA8!V${wJ4 ztj-53jfcKy3)P4JMSNQe>qyb2**h$j&u`9Zxq2M1Jb@ZfQ`WgTtW%{lLq7;GWdrkmk`|ZW|QlATwcIwZS%SN)KSv zXq|d8Zb7_uc)arn@En-SOXtLbH~O%~NFdH34G5Mrmj+4mP{y&^NL)D> zEi||l`!YrQ+DtT*-^--{An4{Z`fzhT*hhw)_b(8O3pQREvtsn`iP|s!6<(?FXvivS zp0OX@4f!QIZH05tyWMjvWzvmRM9O%GojH z@r-WF)N%2hKx%LIB`;_LZEq)A6ZAgpnGBhHruYkzT7g5Gvm3}c6gF6<(y)OrkFTVP z_T)X#kD1kCYP+2liBI=#4R~ezVZHYdp}i4Q$+5Oh_l=)lI4ZEV*rCe`EGARsY08lH zjr|o7`?n(Ar|4g=uRD!+%Tcp^{ow~DZ|`PIgsPLx`9KJaU?MJ3^7o&OZ4VU_`%5Sth=Sl_6ILL&d_gPNelkxxyfCAK6a{?8_nG7W% zz7&lKKdB-T{PNWFd?&`bRu2|8m%0nY3k>5ogF5owHb^Cv|F%VusYS+r?ao(bKB*%e z6gJMJj8#lXP35GH7-}x&P+@bRR+&FlXJTZt5W*3H5s(hN*?~nTK;X5~#Vfp4+a-65vfm40(?PBFrg)l*nOq0hP96Cm z!L)Ivj10;~<-_1H@E_cRr@%l{#4yme{+WLFi-y?dO%itT9f65@p`)U_$EAXjd-nY5 zzH1Kq_-RrD=*fxeCEYDSik(wv%I&rDo81W*5f9+PLqqr(=lm@H6O8|fN;J8PzAY!# zb(-zL92lU>ctpOwgU<+RDS^!|k`3`{-pz?o4 z5aR5jc=?xEZQV?LF%y|fK@4@t?$Gbqo*eVY!*FEI+Cpr9}mMZ9~lrTY!D|JdJL_SgClkln@ zANQnN;_{5jOYj>l0*s`6R8Bo8Kt7-k%6Q>1k-p-nK;K?Wv6Fp0#@UoM?nzYQ_uSmP zn{PopUl6ly>161QwRTEv@SVW2!Ug!y-m}3hCc99HD6-nW*1`tgvww>THc*?)yJg*{ zhvhxZb+2CRQqxws=FX)NZN_SnXh*9TjcKRGU}nuR znKgY^s@nwHgbH*EnLHA_YxlC#4$e>#hR-{NB8w33ZBWgv8!Gforcmuf?2m?D#L-q`d&_JI*?V1DZz|3nbP+Y{{WklMh8EF9|QW5h#5uaT!r~gs@KY|Qx0~DE8b~lT9rkwynVk;g20LwyJ=fI zMp>f6uF+4KX7&6%b)!yyQ5_`}91OwgbRQ-I7lR5+yj-1Us!a4U?g?~`6Hle^uYT(2 zq%h&tMrlphf!ksMx1fbm8W%};V#2YSi*v;Dt9N*N{U{fuB?sFG<6$YuXtVWJ6*Z@a zWa2eXO)P$@Q+8ytq37;jvDF|E_aLpSSrg<7v@ITk$6I zlX=yCEhr6#z;@zUeSK*J=K`0~{kjJtgZ&fLx>j#`_zO)m?<`9UjYxdcLME$eLbsa9 zb?*DjAnes)*?({lNbZTzJog(7SLfUMR1_ZHg8<4puC@`Oo3pPsEXU3a^CC}&Co(p!-`RQe6xjYU zq42tO=NLUh+$vdlTc|Pm$*fa2G4L_T(cH1pE_PegpGo`NI>gepjlYIj*N%jsWA8WE zh=AY^>h2ean)WKs1pWT;!^>=1M{^q^6c9<{2M-_yBH`%V{c&pcUb9J~f#cBAOGWYM z#)UKzf*|k)kQcARW(=E^0Z2(P8c`e0%LYXAslT0=+AHQ*RF7QjQw;|V*CGf(kS`WI)!l|AQJtgb9Hi&_ zG^6hp^C3Us^y@4WO_)Vb-mNsW?5{IKGs989PZm1|hrS$SmweM`5RLvN&>cVYaKz9t z4oSPjy@Pb;$v5&?k*vr&NUD96C*eap!y1~VVHJ8nSYB1r-Xsw)3x8C#+|G&zhvP}- zYOqm;b()9CvLu4?`Dr!EG$d1iNH3i4yIU8>SH5Zrq2ICKwS|_48s#dwKvN%UP6n0F zF89i0D(+atUcRa_$#b&#E&I3j{a1KGh)DeLM@o>bAHy(NkU1(26&U)P>4BdL-SJ$v z-T4W`N^H*aT~&Je^%sbiJ|7jkSVctLq>9=TUk?tQoL=N2mY#j|{j?w*|1D#uD6^ZvitHp( z!$IH(eRU}nEs3_OI-3q0uH^VK%n{9){OaXcLb7ffcW8IA;xbnvQCrAgL()plvzK)( z#w!BMdmd1(wzWTC;Tl>|G2ysEqj;8EYQ|^xao&ctrjk1ZulLw#`EXrAQ9#;a5iOrK zf%^TVjaMA`f;7w2$bUw-jU5Ev7Q*JMTaoO_zVtGYQ@Zi6-=wi)CZ_IhtVkD>Il)z4Uvc6P*GX zVeUBnM+w0F0B*TyX&2knubeivZ_B@YadfmTEpaL;ElvKJXi!SswzXJlH42__)1s}; zu)n%pfZs*!M)fIB8{xgkR@J2;HeqPpKar~Um;JtyzP;3^?9DHi(Ny>yYirOs9&{}% zXg0Ad{4QV`(US9q^Xa>827=l)9sR__dj?OxroIZiEbp~X+>rTMkf-=Umg_JmY(e1h z@gw!M*}E-|qp2bibHfA+`b;@47CfUQKQ_l$hD(v%`3HDFDTpkYn_Q)ryTpEDgPW^F z{|kS;$)CaTPa2e5fdZSY^W8(RdXtNZp&9@+y+a)KFW$%f9&OB1Y>hk}if zk1YoUOtGhprpXioquTb7tLe|f6Yx6}x1cGTt*s9GJ-dwo@xJ1PIOJkWyttL9{1na> z&2j1*xjB=<{J|;DVmmH+F_INXJG`ZqDd$Agx{{Z|p-Ndr&_D6Vdf)H!utDGj`_C1s zr9U=KBes?xxs=CGTonmBSI94cMsy3#!WFh8$suOXQw4wml6gZvmR)K7thz>yb=ti;-29b_cT3yY<(k z!S4gj7QGq_e0j3G+w}ax6;JI2oxEptBn2~ilN!haZW0k={Ky07xum~7 zll2E=0;9WG38deE-0=s=HP~?%gV!gAufU=bcZ~Sb7Hc~f;IV8H^}BG2^ET1K<)8Fa zcz-1-${?mQTLzRP_!$*!3Ahvd{A!kIoT%JGw)p^>6<*H* zm7;@LkueiTsN&rvnusYX`rd&g`zrqh48JM?Yx>S@qI|9u{BFJi-*!^X*f=ikIo{~X zr%qLJP<@<^tr8vs44jz2F8aj))^?I$(Jv}{@(RFFG zquR&Y9O~B?J3{q*%>$V6#FACkN4#gx3!;7eP3a;^24jf)UR;e_ZhB59(~^7n$7(`P zEccsu1;E`t{_@>gb~~+0-%ldz)+wu8_SrH#bPFSvex)Q-IsY;+w_Td4YMWT3f|qhVF|*~`QC)@gENBL9S< zMzM~uv9W9Zx7g`nv}0L`lY_0jlbs`SD$4cXpu~QYGA=GXJsmP)*k219SH1OQcsQjL zcKZrnyIeJc@#4j}(Q2V*BS_J4X1zbl@lBMefK{>eU|4oq!dm4s`lFoqZXf4sKLwvg zhHL){Ayng<(e1%b?jYRqbiby?V~qtfQQb?RrxMM;m)bX^JYq5*KY~=e>03RNf~wY~ zk9s&ipHG|~hOe%g3efQ%v<3I{D6iciAp-^0_^*6HEn_55uR{oK@~ePg*m$`F<|6b1 zlw}k7E5c*=Yl4AuHGN?04gzp{UYgfV0!pXgjbd1nUInLy`YytY;|6)5>l zn)S}^_R++cyXY@96K;Mc@^*FTAR{RfzSo9DohCcwEV|SRi;m6BJ(^99iZ+oFlk^wB zJAu5KTU?asUfjyuUaw~C|AB!df3~IC0B%T?!*V3j8d8pnR~ifJ9%k@U^EZiD?icDR z-={JtjB*y6b7_DQ5lv1_lJF7C&(GtgP1@+kOMe4l!N-3!BM*3rEb5P{1Eb!lq1YZui=9O_4GHr1|tP-a3v6eRl z6SvN|2b=one^*^i^X;h(RjyW(5i=`-;5>W9`15tGzvO79dp1=9x1fZSZo=W$ z^z)x0!(CLqzl)H|yM`p@6Y&uxGZ(A1_EC6O+62dDc-Xx4Z;tqCe!CifO7tzJKkkgl0JPo!gCF5W;4uBlcM+Hd+;r6!O@j zS#|qPqRZ~%*!r^xB#o}Rf=hNUI$VZHKVL__!vWYtR(VRVAidS9q@CQJ;^S=~AI3{{ zA1eIr?N#%BaVY+hD-^4+S*=+P0k7S#{5G7&O3%c3$ja3@3!|T&io5A}4R!s;8KM7i z->E+jhCd3t7*h0TOQX$F2#5Qa{zFUI4K1}9){ke^Ph4CK?`n6vj8k^hbk9kza?u{^ zq5|KrQrGucS4Zwl-TgSaOlJ-T+CY*DM@poXttK10iIM%it#>7!_l|C(^?JO~=P!HU zBS3pbU|e9}hvxu>?f?)Q5uTWD+bu=Z5-2}_+hR=Z1uYv-|VG_U^{Diwd4Mhn{2kknDlDgco1u+fgS3|3m#>mLB{w z5C%g`De@d%mi<&DbLC&K{ z$Bj}s82Brx3OU=CcIqHS+mA}`3+TW}Z44~q9ad_$gA_(l*+OUy}a^k?5xsXzgq}{ zFe+g%zD*+6HM-ZA#@6nF+?NY~7Uv2#dkvxH`;HkOx_MsoE4K(A+iN0TbXsHzLeepi zx391@A?Xu>5mo-{Qp+vW{e7y?zMCg&d*m@8?jI&P=c@FS4`l`cG6Eg}-R&0=(Ju)% QaUB39IW^fbX|urp0ygapLI3~& literal 0 HcmV?d00001 diff --git a/docs/images/treeandlistapp.png b/docs/images/treeandlistapp.png new file mode 100644 index 0000000000000000000000000000000000000000..0867b1473f8068d071115c77863886e5e1600cc3 GIT binary patch literal 19079 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 I>zopr0ER;cLI3~& literal 0 HcmV?d00001 diff --git a/docs/images/treeandlistappwithdesclist.png b/docs/images/treeandlistappwithdesclist.png new file mode 100644 index 0000000000000000000000000000000000000000..c5b790bbd5e41a1c5be7610099c45f62a4c4eaa5 GIT binary patch literal 18041 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 PW*9tO{an^LB{Ts5Zsimilar optimisation techniques, this violates the object-oriented principle of modularity, but it is worth it for the benefit it brings. The effect can be seen in the akonadiconsole tool by not filtering out the items from the tree. + +In the screenshots below I removed the filtering out of Items in the tree so that the fetching/purging can be seen. In real applications, the Items in the tree on the left would not be visible. + +@image html dox/bufferedcaching1.png "When a Collection is clicked, its Items are put into the model. The rest of the Collections have no items." + +@image html dox/bufferedcaching2.png "The Inbox Collection is selected, so its items are fetched. Personal Contacts is no longer selected, so it is put into a queue to be purged." + +@image html dox/bufferedcaching3.png "Select another Collection and its items are fetched too." + +@image html dox/bufferedcaching4.png "Another Collection is selected, pushing the Personal contacts out of the queue and purging them" + +@image html dox/bufferedcaching6.png "If the Collection is selected again, its Items are refetched" + +For this example, I used a queue length of just two Collections, so that if a Collection was deselected two clicks ago, it will be purged. In real applications, a longer queue length will be used, but it’s harder to illustrate in screenshots. Another unrealistic part of this demo is that this feature will like be used in applications like KMail where Collections can contain tens of thousands of Items and fetching them is an expensive operation. + +This feature should be totally invisible to users and even developers using Akonadi, but it should offset the main disadvantage of using a cache of Items in the EntityTreeModel. + +*/ + diff --git a/docs/kontact.svg b/docs/kontact.svg new file mode 100644 index 0000000..74457a3 --- /dev/null +++ b/docs/kontact.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/server.md b/docs/server.md new file mode 100644 index 0000000..3bf8fe1 --- /dev/null +++ b/docs/server.md @@ -0,0 +1,244 @@ +# Akonadi Server # {#server} + +[TOC] + +This is the API documentation for the Akonadi server. If you are using Akonadi +from within KDE, you almost certainly want the [client library documentation][client_libs_documentation]. +This API reference is more useful to people implementing client libraries or +working on the Akonadi server itself. + +For additional information, see the Akonadi website. + +## Architecture ## + + + +The Akonadi framework uses a client/server architecture. The Akonadi server has the following primary tasks: +* Abstract access to data from arbitrary sources, using toolkit-agnostic protocols and data formats +* Provide a data cache shared among several clients +* Provide change notifications and conflict detection +* Support offline change recording and change replay for remote data + +## Design Principles ## + +The Akonadi architecture is based on the following four design principles: + +* _Functionality is spread over different processes._ + This separation has the big advantage that if one process crashes because of + a programming error it doesn't affect the other components. That results in + robustness of the whole system. A disadvantage might be that there is an additional + overhead due to inter-process communication. +* _Communication protocol is split into data and control channel._ + When doing communication between processes you have to differentiate between the type of data + that is being transferred. For a large amount of data a high-performance + protocol should be used and for control data a low-latency protocol. + Matching both requirements in one protocol is mostly impossible and hard to + achieve with currently available software. +* _Separate logic from storage._ + By separating the logic from the storage, the storage can be used to store data + of any type. In this case, the storage is a kind of service, which is available for + other components of the system. The logic is located in separated components and so + 3rd-party developers can extend the system by providing their own components. +* _Keep communication asynchronous._ + To allow a non-blocking GUI, all the communication with the back-end and within the + back-end itself must be asynchronous. You can easily provide a synchronous convenience + for the application developer; the back-end, however, must communicate asynchronously. + +## Components ## +The Akonadi server itself consists of a number of components: +* The Akonadi control process (`akonadi_control`). It is responsible for managing all other server components and Akonadi agents. +* The Akonadi server process (`akonadiserver`). The actual data access and caching server. +* The Akonadi agent server (`akonadi_agent_server`). Allows running of multiple Akonadi agents in one process. +* The Akonadi agent launcher (`akonadi_agent_launcher`). A helper process for running Akonadi agents. +* The Akonadi control tool (`akonadictl`). A tool to start/stop/restart the Akonadi server system and query its status. + This is the only program of these listed here you should ever run manually. +* The Akonadi protocol library (`libakonadiprotocolinternals`), Contains protocol definitions and protocol parsing methods + useful for client implementations. + +### The Akonadi server process ### + +The Akonadi server process (`akonadiserver`) has the following tasks: +* Provide a transaction-safe data store. +* Provide operations to add/modify/delete items and collections in the local store, implementing the server side of the ASAP protocol. +* Cache management of cached remote contents. +* Manage virtual collections representing search results. +* Provide change notifications for all known Akonadi objects over D-Bus. + +### The Akonadi server control process ### + +The Akondi control process (\c akonadi_control) has the following tasks: +* Manage and monitor the other server processes. +* Lifecycle management of agent instances using the various supported agent launch methods. +* Monitor agent instances and provide crash recovery. +* Provide D-Bus API to manage agents. +* Provide change notifications on agent types and agent instances. + +## Objects and Data Types ## + +The Akonadi server operates on two basic object types, called items and collections. They are comparable to files and directories +and are described in more detail in this section. + +## Akonadi Items ## + +An item is a generic container for whatever you want to store in Akonadi (eg. mails, +events, contacts, etc.). An item consists of some generic information (such as identifier, +mimetype, change date, flags, etc.) and a set of data fields, the item parts. Items +are independent of the type of stored data, the semantics of the actual content is only +known on the client side. + +## Items Parts ## + +Akonadi items can have one or more parts, e.g. an email message consists of the +envelope, the body and possible one or more attachments. Item parts are identified +by an identifier string. There are a few special pre-defined part identifiers (ALL, +ENVELOPE, etc.), but in general the part identifiers are defined by the type specific +extensions (ie. resource, serializer plugin, type specific client library). + +## Item Tags ## + +Tags are self-contained entities stored in separate database table. A tag is a +relation between multiple items. Tags can have different types (PLAIN, ...) and applications +can define their own type to describe application-specific relations. Tags can also have +attributes to store additional metadata about the relation the tag describes. + +## Payload Data Serialization ## + +Item payload data is typically serialized in a standard format to ensure interoperability between different +client library implementations. However, the %Akonadi server does not enforce any format, +payload data is handled as an opaque binary blob. + +## Collections ## + +Collections are sets of items. Every item is stored in exactly one +collection, this is sometimes also referred to as the "physical" storage location of the item. +An item might also be visible in several other collections - so called "virtual collections" - +which are defined as the result set of a search query. + +Collections are organized hierarchically, i.e. a collection can have child +collections, thus defining a collection tree. + +Collections are uniquely identified by their identifier in +contrast to their path, which is more robust with regard to renaming and moving. + +## Collection Properties ## + +Every collection has a set of supported content types. +These are the mimetypes of items the collection can contain. +Example: A collection of a folder-less iCal file resource would only support +"text/calendar" items, a folder on an IMAP server "message/rfc822" but also +"inode/directory" if it can contain sub-folders. + +There is a cache policy associated with every collection which defines how much +of its content should be kept in the local cache and for how long. + +Additionally, collections can contain an arbitrary set of attributes to represent +various other collection properties such as ACLs, quotas or backend-specific data +used for incremental synchronization. Evaluation of such attributes is the responsibility +of client implementations, the %Akonadi server does not interpret properties +other than content types and cache policies. + +## Collection Tree ## + +There is a single collection tree in Akonadi, consisting of several parts: + +* A root node, id 0 +* One or more top-level collections for each resource. Think of these as mount-points + for the resource. The resources must put their items and sub-collections into their + corresponding top-level collection. +* Resource-dependent sub-collections below the resource top-level collections. + If the resource represents data that is organized in folders (e.g. an IMAP + resource), it can create additional collections below its top-level + collection. These have to be synched with the corresponding backend by the + resource. + Resources which represent folder-less data (e.g. an iCal file) don't need + any sub-collections and put their items directly into the top-level collection. +* A top-level collection containing virtual collections. + +Example: + + +-+ resource-folder1 + | +- sub-folder1 + | +- sub-folder2 + | ... + +-+ resource-folder2 + | ... + | + +-+ Searches + +- search-folder1 + +- search-folder2 + ... + + +## Object Identification ## + +### Unique Identifier ### + +Every object stored in %Akonadi (collections and items) has a unique +identifier in the form of an integer value. This identifier cannot be changed in +any way and will stay the same, regardless of any modifications to the referred +object. A unique identifier will never be used twice and is globally unique, +therefore it is possible to retrieve an item without knowing the collection it belongs to. + +### Remote Identifier ### + +Every object can also have an optional so-called remote identifier. This is an +identifier used by the corresponding resource to identify the object on its +backend (e.g., a groupware server). + +The remote identifier can be changed by the owning resource agent only. + +Special case applies for Tags, where each tag can have multiple remote IDs. This fact is +however opaque to resources as each resource is shown only the remote ID that it had +provided when inserting the tag into Akonadi. + +### Global Identifier ### + +Every item can has also so called GID, an identifier specific to the content (payload) +of the item. The GID is extracted from the payload by client serializer when storing the +item in Akonadi. For example, contacts have vCard "UID" field as their GID, emails can +use value of "Message-Id" header. + +## Communication Protocols ### + +For communication within the Akonadi server infrastructure and for communication with Akonadi clients, two communication technologies are used: +* D-Bus Used for management tasks and change notifications. +* ASAP (Akonadi Server Access Protocol), used for high-throughput data transfer. ASAP is based on the well-known IMAP protocol (RFC 3501) which has been proven it's ability to handle large quantities of data in practice already. + +## Interacting with Akonadi ## + +There are various possibilities to interact with Akonadi. + +### Akonadi Client Libraries ### + +Accessing the Akonadi server using the ASAP and D-Bus interfaces directly is cumbersome. +Therefore you'd usually use a client library implementing the low-level protocol handling +and providing convenient high-level APIs for Akonadi operations. + +### Akonadi Agents ### + +Akonadi agents are processes which are controlled by the Akonadi server itself. Agents typically +operate autonomously (ie. without much user interaction) on the objects handled by Akonadi, mostly +by reacting to change notifications sent by the Akonadi server. + +Agents can implement specialized interfaces to provide additional functionality. +The most important ones are the so-called resource agents. + +Resource agents are connectors that provide access to data from an external source, and replay local changes +back to their corresponding backend. + +## Implementation Details ## + +### Data and Metadata Storage ### + +The Akonadi server uses two mechanisms for data storage: +* A SQL databases for metadata and small payload data +* Plain files for large payload data + +More details on the SQL database layout can be found here: \ref akonadi_server_database. + +The following SQL databases are supported by the Akonadi server: +* MySQL using the default QtSQL driver shipped with Qt +* Sqlite using the improved QtSQL driver shipped with the Akonadi server +* PostgreSQL using the default QtSQL driver shipped with Qt + +For details on how to configure the various backends, see Akonadi::DataStore. diff --git a/docs/tags.md b/docs/tags.md new file mode 100644 index 0000000..4f14a23 --- /dev/null +++ b/docs/tags.md @@ -0,0 +1,163 @@ +# Tags # {#tags} + +[TOC] + +Akonadi tags are an abstract concept designed to allow attaching the same shared +data to multiple Akonadi Items, rather than just grouping of arbitrary Akonadi Items +(like the more conventional meaning of "tags"). They can however be used, and most +commonly are used, as simple labels. + +[ Note: this document uses the word "label" to refer to tags in the more common sense + of the word - as a named labels like "Birthday", "Work", etc. that are used by users + in applications to group multiple emails, files etc. based on their shared characteristic. + The word "tag" in this document always refers to the concept of Akonadi Tags. ] + + +Akonadi Tags have an ID, type, GID, RemoteID and a parent. + +* ID - an immutable unique identifier within Akonadi instance, same as Item and Collection IDs +* Type - an immutable string describing the type of a Tag. Applications can define custom tag + types and can fetch Tags of only a certain type. +* GID - an immutable string-based identifier. GID can be used to lookup a Tag in Akonadi. If + application needs to store a reference to a Tag in a configuration file or a cache it should + use GID rather than ID. GID may or may not be a human-readable string. Generally for Tags + exposed to UI one should use Akonadi::Tag::setName() to set a human-readable name of the Tag + (Akonadi::Tag::setName() is equivalent to adding Akonadi::TagAttribute and setting its displayName). +* RemoteID - an identifier used by Akonadi Resources to identify the Tag in the remote storage. +* Parent - Akonadi allows for hierarchical Tags. The semantics of the hierarchy are only meaningful + to the application or the user, Akonadi does not make any assumptions about the meaning of the + hierarchy. +* Attributes - same as Item or Collection attributes + +# Tag type # {#tag_types} + +Akonadi Tags have types to differentiate between different usecases of Tags. +Applications can define their own types of Tags - for example an email client +could create a Tag of type SENDER, tagging each email with Rags representing +the email sender, allowing to quickly perform a reverse-lookup for all emails +from a certain sender. + +In Akonadi, two Tag types are present by defaut: PLAIN and GENERIC. + +There's no limitation as to which attributes can be used with which type. Generally +PLAIN Tags are not used much, and the GENERIC Tags are used as labels. + + +# Tag GID # {#tag_gid} + +For the needs of uniquely identifying Tags, each Tag has a GID. The GID is immutable +and should be used by applications if they need to store a reference to a Tag in +a configuration file or a cache. + +GID is also used in Tag merging: when a Resource tries to create a new Tag with a +GID that already exists in Akonadi, the Tags are assumed to be the same and the +new Tag is merged into the existing Tag (see Tag Remote ID below for details and +example as how Tags are handled from Resource point of view). + + + +# Tag RemoteID # {#tag_rid} + +Much like with Items and Collections remote IDs, Tag RIDs are used by Akonadi Agents +and Resources to be able to map the Akonadi Tag to their representation in the +backend storage (IMAP server, CalDAV server, etc.) + +There is one major difference between RIDs for Items or Collections and for Tags. +Unlike Items and Collections which all have a strictly defined owner Resource, +Tags don't have an owner. This is because Tags can group entities belonging to +different Resources. For this reason, each Tag has multiple remote IDs, where +each Remote ID belongs to an individual Resource. The isolation of the Remote IDs +is fully handled by the server and is opaque to the Resources. + +For this reason, Tag Remote IDs are not exposed to client applications, instead +client applications should use the GID, with is immutable and shared between +both clients and Resources. + + +As an example, let's have a Tag of GENERIC type with GID "birthday": + +An IMAP Resource may fetch an Item with a flag "$BIRTHDAY". The resource will then +create a new GENERIC Tag with GID "birthday" and remoteID "$BIRTHDAY". The +Akonadi Server, instead of creating another Tag with GID "birthday" instead +adds the "$BIRTHDAY" remoteID with relation to the IMAP resource to the existing +"birthday" Tag. + +A DAV Resource may fetch an Item with a label "dav:birthday". The resource will +then create a new GENERIC Tag with GID "birthday" and remoteID "dav:birthday". +The Akonadi Server will add the "dav:birthday" and the relation to the DAV resource +to the existing "birthday" Tagg as well. + +When user decides to add the "birthday" tag to two events - one owned by the DAV +resource and one owned by the IMAP resource the Akonadi server will send a change +notification to the DAV Resource with the "dav:birthday" RID and with the "$BIRTHDAY" +RID to the IMAP Resource. + + +# Client API # {#tag_client_api} + +Existing Tags can be retrieved from Akonadi using Akonadi::TagFetchJob. + +To create a new GENERIC Tag with TagAttribute with display name set, one should use +Akonadi::Tag::genericTag(const QString &name) static method. The tag will have a +randomly generated GID set. This is equivalent to +calling + +~~~~~~~~~~~~~{.cpp} +Tag tag; +tag.setType("GENERIC"); +tag.setGid(QUuid::createUuid().toByteArray()); +tag.setName(name); +~~~~~~~~~~~~~ + + +Such Tag can now be uploaded to Akonadi using Akonadi::TagCreateJob. If a Tag with +the same Type and GID already exist in Akonadi, all other data in the existing +Tag will be overwritten by data in the new tag. + +Any changes to a Tag can be stored in Akonaddi using Akonadi::TagModifyJob. Resources +can use Akonadi::TagModifyJob to set or unset a RemoteID for an existing Tag. If the +Tag has no Remote IDs left after this, the Tag will automatically be removed from +Akonadi. + +A tag can be removed from Akonadi using Akonadi::TagDeleteJob. Removing a Tag +will also untag it from all Items and will notify all Resources about it so +that the entities are untagged in their backend storage as well. + +To retrieve all Items with a certain Tag, Akonadi::ItemFetchJob has an overloaded +constructor that takes an Akonadi::Tag as an argument. + +Tags for each Item are available in Akonadi::Item::tags(). To tag or untag an Item +modify the list of Tags in the Item and store the change in Akonadi using +Akonadi::ItemModifyJob. + + +## Fetch Scope ## {#tag_fetch_scope} + +By default Item Tags are not fetched when simply running Akonadi::ItemFetchJob. To +enable fetching Tags, you have to set Akonadi::ItemFetchScope::fetchTags to true. +The retrieved Tags will only have their ID set. To retrieve more you need to modify +the Akonadi::TagFetchScope (from Akonadi::ItemFetchScope::fetchScope()) and unset +Akonadi::TagFetchScope::fetchIdOnly(). Now ItemFetchJob will return Items with Tags +with ID, GID. If the Akonadi::TagFetchScope::attributes() is empty then all attributes +will be received. Otherwise you can restrict the fetch scope to only fetch certain +attributes. + + +## Implementation in Resources ## {#tag_resource_impl} + +If the storage backend supports some form of labels it should implement support for Tags +so that tags from the backend are synchronized into Akonadi and vice versa. + +To be notified abount changes in Tags, Resource implementation must inherit from +Akonadi::AgentBase::ObserverV4 (or later). + +Akonadi::AgentBase::ObserverV4::tagAdded(), tagChanged() and tagRemoved() may be +optionally reimplemented by Resource implementation if it wants to synchronize +all local Tags with the remote storage so they can be presented to the user, even +if no Items belonging to the Resource are tagged with it. + +The most important method to reimplement by Resource implementations is +Akonadi::AgentBase::ObserverV4::itemsTagsChanged(), which gives a list of changed +Items belonging to the Resource, a set of Tags added to all those Items and a set +of Tags removed from those Items. + diff --git a/icons/128-apps-akonadi.png b/icons/128-apps-akonadi.png new file mode 100644 index 0000000000000000000000000000000000000000..662b9e1cb06e077b37aa01959a114a60514f54f2 GIT binary patch literal 22934 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 yFn(ANUd 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 RgO~sS002ovPDHLkV1g}>^h*E$ literal 0 HcmV?d00001 diff --git a/icons/16-apps-akonadi.png b/icons/16-apps-akonadi.png new file mode 100644 index 0000000000000000000000000000000000000000..045b0cf61388779f6e6c4fabf3dc9350d2525655 GIT binary patch literal 939 zcmV;c162HpP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L02{9W02{9XUK)`c00007bV*G`2iOG> z2^|#pn+S#g00S#YL_t(I%cYY`XdHC_#(y*O|L@Gs%%qrC zy!pX*`QZ1#clh9cO8Iwi_R`0hnfaL)jI4gK>qoPP2W_Rk?cJ>J)USlCy^rp%y>axP z1y6jqby~#fwb}8wF!7*k_cfKSe}uOiP`le;Ypu%8osD1p=FZcbH{ZGUH>UwCytQnO z=Z?NE4~fZ#9vjP)hAjph12v~3q=r;gBtWbTMxp|0)DU+z>ooe1vm|Mh(DRRoy@;q2)9Xg`(+CuW z*!MG0JaSg(jkE z01z^HJsvEc@3m?JUtIkLCDqV0Nt&jlNf+JLD2zH}kB#D_&#J@aLu29?stE}JN#di(6n!9vbNV>hM=De|z3zk&zI%RN#!4SU0a)6qYRyK= z{p_pt#p|n$%VCf#s!V&g8I8gkpSjcY+0927Gs@6F- z=d4W)N3Sfe>^-@DFSyY0lJZffsTfUhFg9U)Ia?_#4;I~RUCMo_X*E@7;Xh}(c6W=u z7iulf%j_N0&Gq%F+iZ36ih}AmW>_veez8~x3$`8RZ7VWOt7phu`Ul`=SzQ4ANPqwU N002ovPDHLkV1kYuzGnac literal 0 HcmV?d00001 diff --git a/icons/22-apps-akonadi.png b/icons/22-apps-akonadi.png new file mode 100644 index 0000000000000000000000000000000000000000..f8a06a33821ab07bff43ecabe6a8a95c51edaa92 GIT binary patch literal 1487 zcmV;=1u*)FP)Px#32;bRa{vGvuK)lWuK`{fksJU300(qQO+^RT1rZ4xIMY~FBme*a24YJ`L;(K) z{{a7>y{D4^00mD;L_t(|+O?2RY#h}c$G>mh%)I%(YwvE19TPjTfhf>akS2lz5sFmx z!U2RtIdG`j0}7XL3KCR>s;bngRRrRK5I592h^lH;34-7NGEyNS;h(|EI<{kH*RwOT z^Y_iX*Jtcj^g_AxGx|!Sk>>m7hyND@-@%U_dIik)Zu48geGp_WPfCop)BboEy~nfc z%GVdqeShPHr!G#4TFl=bY;_JC>ix!TIrk8vp%_;f4|9YYV+>YD*k0dW%HqMZD<7Zx z$Nw}w{o=||$8z7c%HibkI|JC04xke>T%c6OnKDk4akv^|^XdqjE1NRjUOQjr;kotY z^8(+3Vvl3bE&0NsGYiG8L9TLVf7Z2AmnBxKb<7Bxgxqs}=**@bMP zvr^V?0eBO8Xiyw~{#|#b%g%JWEfi%5p;HjmYzMhFi)jSZq%c%-<{GdA6JE=L-*(Yz z2gLKO-_I;OMgGfpY9{!}q^Y$g0tZA2Bv}P6ByxfOsFJ%W8c_jMgJDoG6=FFi9M6XD zTStuUO%ERW_13QO{Le4ItnpYq${{n3Tjsl1>u-Zs0+E^?Hc*l%cTTXQM()l~0h&g> zxx1m3Shfk)oqod~t-LVz#D{9ID2^AU_Ol`?k%u`B?Qg?MGHk3zsB(@huTiSNCgrHr zds8TtH3V-Uc0wT33aJ7pWEzuf7@hT+=#;K|HVa1uLPNlU0Hp+{Z=b>&OY30u6ed~> zkP53!;FUM#aj$~XwqYi4GqekZ{?cMoUY)k~d6losx z|M?}NQG-#$0T7s?!53Esuq1(04R_9!sdecvMH(hZ;}CHaVtW`vPy%;;0gcn!l8*B$ z0>z0xe)Ixg8Jg%#-5us!%r`RG2osD4r5`lhb-f`){n6K#Q^ajMVTg$ z$U#{H!!uwy7M$4*oVf*DU#)5#;-x=-+9w2iocQf~zss)`&+z^-@{Kj1(qO9ghRGl$ zP;(AmWuUZ%?Hh0=T)4dety^c|E!>RcMvcL>^5y3qpL*J2tkkiG48!NCYv0}Mn|pvY z0(^S|`FIOiBmfXd^?XyqGEhqj*vk9|2VozXN3xz`bUnHB_-XgmGB4XYr7?x#uOAQb z$o{=7WK5{WbtvxJZ{wYQ zQk!<2c--Z6-K2{L5ivuqf3=*wJsgbgD9XA^G@@CS5w_d*mj~wBpY7Y{Z3Uhi1-_RA zt~>S|JF^%osIJ!p-{Mb8OE}8fq*U$FBBSFpGeR|IG)h@fas$AQu?T!td!Ef5%jUMt pYTIHpGp)ulT`{7Sd}whN{{nuzq^pv@C$j(m002ovPDHLkV1nRezRv&v literal 0 HcmV?d00001 diff --git a/icons/256-apps-akonadi.png b/icons/256-apps-akonadi.png new file mode 100644 index 0000000000000000000000000000000000000000..4aa5fac7fa0e4fe4e10e9e8fa368a24d3a7e2751 GIT binary patch literal 67747 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 z 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

YWR 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

`j$|Gkfw~00(EvM^U~D!&VlO}& z)pFP;p^3?;g1mw*sd&a+3(HZ^rtHo<}OJ?N56ed76-aSMr_xaJBx_~5hnTdz8Tjf-6BKL+3@ z{@pY9!p~l*gm2n&3vT_B|3QEJJx7--pS=A(JbTU6c=ut!R~~+j@}UF z^nQd9Jpf3CK8d+q64z1|!yJKZ4nbAZ2%HJjRC?{I)Ld$W2p7YcfKj*irAjUf3fBU% zF#wS)iYDQ0E+gf}09@K@Nd5qSP{dH9y21d+|HdS|&ihRzTZ>A$WrGV_3-AMBTeq6~ zWqjR$PY(WZZ5i5QE}YHLSArRgfFIvSTEq62lw8eUPzmtch?WWJDz>NGhNKN900Fi4 zRVIY)%Ylg{F=b z72r$?GPpyCbT-Lat0Kj1M5^PN2~c{~tQMu&NYJP90bF+SjNbU>Yw){|^|<2HgZTcp zy$OitU%lAlwtxE-#V_A%?}TsiGx@RqL?8XckK*qjH*Mga(|F@8SL5oFFW{c-9lHGR zIZ3rPhl;#Q)lEG(8uSfi;3Sw2A-N)jP9hv0Z-1ob!{YAQuXX5blvCFTMA zS~bgJR54}{*@bZFA4NzpX9W!S;ts$7eyIN1B>s{C_?lsM75mGD0DHV?077lOy$gay zH``^g4Eeli15A;K3#k%k@063+2Y9puG_~h0ls@Ke9|GxX87H*Mga`ya*s zIlYS?eaDSB_qng)iLFgKm=k!r5M7k4Ty~BLWf6};4N3!5N~jPdl72ByM1xd{a#JrC zcl+N!AyNrSRK)=u-GJQlB9;e0h(-h&A;D6F1U2rzRbxD5DBLdwD3QAKw*n(6NaNg0 z0THf6ELmt6grx>k2;kQ;(g-I8__eCNJMe8D)Il#{??b=};dYgpI+Fqg4F|r$4bPYW zSY-to)2IQ~W(ouY04Ov8ry((yl!+Sn31XBzvyM}9&Kc-48%>ee6H;FS{Nd#%Q}LNy z4R2n5zx_73LoR^Uz^j0E%u0#eb|&(DHtdbxIVe{(1VCwjla0m<8nbBU8xv4QoQZG4 z;Rm377k3FlHL}XO4n$t`%hQA;NeAgQ^ThHj`T}&6we)jz8yU_hW{?s1b*`t@4`cq+ zv-rN(U99&W+`=9I^FI9e>yP62wawQ5m6s|${A-Wm+;6@?vGNeUg`Tya{xtpihu(qP zE+StT=J@1ip29Q7Ht>!&9K{P?dVrq2=wfW;gnIr0^=S>1j1AC^%rPqy)C?+nJuxTL zGAGH2P%Ff{3zNzIQayfEC5nx--7f4V!bsa1grs`tQNp)S%3wiej=bcd4qWDQ5+ojk zh*Vfg8@LS;`jWVNMnU&yu^-Y>1)OwTj!kNVw zL2}Au)MDtIBE*+j475olieMUW83uCZc3~$F(%X$_O-tLK;lOQ9H#X+63hZX%NAXo- z7Qpfx;1zfT_3tw)$+#Tx{Rpa>>Tmz+_@!4=$<%$V72Ra2GE}523UrVvnkJ-^lwhS3 z6-cvAb;6MC0qa@xg|!11PCSRVU%8do2*pT>yUmzdwyHzW0X} z_!7R}L->|^ZpN{XeUSd>ecy+-0!v2lUq16Np58csA9%}Ec;W5`=(&rI;h+|5PC9yN zN5L~e+8bzB2`ClbT3;cWNa}zAiINyVNvHx9X5+)mF+wes6l&my1Ig8X4J|+&L}Gi( zD1(hsEh^}_2Br7RL?r45F-M~s@Re&(OIH>hse1mvAme(fwK))@sY&D*)TDkt0&8`N zZw_H;iAkymvn1U1adWb*QB(g_m1w07XjKgWp=kgO^s6R;5c@nb=$h@!VG1MzdoY3+ z$*btnxR+US*z>7lVfAkkM;&~&Zye}Y&@KyMFlSh%q$#sYQ&8OM>XMZhVyn%*Z@~5z zTkyGo)+nR-4x(jZbJp!=2=L!fjo$!=NucrXcyJ|tD>?7=h;GtErs9J!(gY>%@H4N3 zX(Ef!c9fK;fV(ve^K-hk5O~|?F5sY4)OFUEYsTcv9B;pNUGLpIg1c6MA5Ol1eEUg! z;^Y5I@$dg0{;Y@at@>Q?gShp>x6y}w?47s?Xd1yiC-BS$FY*Yk!sero-~pP@#^yQ> z^q`%74{3S;!|pju4$NqGZx2&t><$CX)~2+xv)J#L2nkXv)lvv7&=UF~gyJGft4bsW zh2sxZj5?F3LV#-yg8j^31gB{STA9>Sb89JVLn=unuSl$+5kwbaV+5ieRanqkj3y~c z0lgu16m!LgLq800fFAoDA^(DyEkGySyq)RKe`qP4x#NuRbWS zbg0D{=17qZsoffGLKB-b_qD_tpcezUgEs2oBk!Hb)$}~Bx%y^j-Qy1exnyr@IVe2) zEt$pv82bvQr9|F$t1bZd8^~g$06##);mubEKU^ya2O=~yqhyqPKmN=5e4J)w5Im?r zFdJWcO*Df*!0ev?ZV<=}&^-aqSnDtarBTiAP>vqdXJ2>`m%n@g*S-Fz-klEOiA8bU zn?~Tj?RBsx?EHUn`}6q7Z+%(uU*CgxzC9U0OCkI<-1_mKqYvGB6paz|3)A2pJi2i) zZhqsnIQY~9cOaQb2 ztxW+MEXz@wb1Y$`!r}M|FpC4)*V;a7O5uRZ?0dvY$)u$ifFO#Lv?Er8w5CJ~1(=t% zHi!}4baqC706YP(b^kX|f##57IG|-25(EIYc{DGha!(FZWO=asGC(~~k- z)tjb3Q3WZnR!ZndA%m#Qpwlu#O@b+P*q!g{`r(7vJ$D`lX9>GHsPbW)IsF8#Bi6%j z`3~Iiz*(F>|0LeEwS}u+(`@`N0{D-&pIiX{-WA|~`!j&{{Lp)G>pMR{ANq%;5j1Dv z7w*HIdyGquU5%SwC3xYXC+X?w7G^YHtDDd{DF98-V;i|oGyq`jA%HQ1Bp|P+uvTQC zL{#&{i_x(1YoJfw&)exLW2x6^n|bkT|Q7y7SKLvkK@`^Q>xDI?Q>%teau`!nPi|=rGQ0U%-KsQLuwLIix!~ zg2NY{#|^K0m41~EGwZ{J3juo1@QkF;`xio09wnj z590E7e?R@kFWiKmIMkL;Kldy?dGAv=bKn4OzWI%qJoOOHo?WAtHV3SwgiRcvbBhpT zx4wyO-N8(zG%o`*6E+msE`>;h*b}>9;4dKzWdw1Bq5;dLJpc;_I#!w{AgBhuxRyLE zXi<_%5Da9uQ&rF)!l(=pk6jJ5fFv>#ix>_|^>CzZ!A%FtU=U9xD^kPN71fbM<2j%T zEfFd%yrb(u2!4&`2@*EKeq|s`0{@5@fJPw-Tfi}alx!UiY9??v%Q&nM@3GZ#X*#S?^ETALxGXF$bjTIPo%vxm+vu;g`4v+-QhKvEd zP7iOQoaxbV0(XOfetY*E)?V7fb(g4~e)Vf{*B76I=nD(`eht^VecxjFXMlhCsi*Pe zKmAq3BOk^8&qMe&er7-YJ9OJ$|7-ZUAKJn-XwO6Ue+{2KeF4024Bv76QA{5F3SJnt z==8ecK(59NcFu2O+Rbnw6-eEqR5~d2piwR3fpjpyatW-cjHSSuG2(O7a zGpO`8k1sQwiRkAs5tMNu;`2Q5b_(%UmCpHat@u1~Pr5>asjq0`L$8 zGiVs#wIixjR3XdIpBhYrH2fC;b8D93?tuy*y9OLjTqqG8u#<9}g-giL6kJ1KVu)%u z<^Uo%*omCRIYi&@l&pN!%OyA=bxEoEcNouy^hUINNr`|8MP_AjK-#Bn?}L&9(-Ukm zkOobXp;?s*fjc|zx$-gqlp;~b83f!M)SMC`%!w*MPUMV0NGV5?jRe3V#uc+!r$Yt0 z%qW9v5;-MJ!q`m%x^5GxoYy{2u!aLDdoN>sma*3lsGA4%?C#z|(i4u)_Eg?*E$%*f z7AGHEfPQNYZ(T@xnoNOs9tZHjJ0HSt-}fZ&{`V;UoF2kogy)Api0}NaTj-~L=C9(m zLugN#I~`N27dv)TLd|6l9B&f+|iS!Et) zGAs_zW{PRcx-MueNbySXO!SEPAlVWPE(HS_5zE_-!E4042o}WLF+`|{E0kP>kLDS5TXI3DWjmjU z5zG-stW+d`>-JZVfF2G0s);JN=+Su$qIVtjEkSVr3=tuw5Y-*Q>$a^{6~x?K6;Y6a>&79>#SW{fx>Ew$R3|r(GU{m~I5fKW_EWt=NUmmh z2yEJ=m>djH6n6~?tavpgCG|Uiz_v>f02lpe$cQS4t1E1-WvQe9YOPnP1XZU%X);rt z6G~w%+2eeNaIm7!nd8SM45FT`vcGgrm79F(#MG~U^3z;+;}!?Y1y6PG_?Zc_y%TWA zZQ#$|zRowleS)|CL4E;Z>ctCO2KOps{=94DFI4Z(|D9Df@#GwC{@NHXmFu!B+7QFvtZT;nyJ-^ zij2iJLKFov1m8vpaLl#_q7pI7Vj_r{7=o(_89X{tRH`~&N3jycq{mCZDdHHOlbB>g zqF7~P7NP+>;yH^)2Lujh)_6r!HX`xr_%_>c^q+whm#z2?+inC9DqCZ~v5LDO;v7!j zK?Dtvjer6!fYhy+R0G@oyds|1F7h~LxRUTt&>+YdoM=cCWQ;Chgp`m`oLCTA7eBm~ zaS4uSbSxtwp|~4Llx=1)D>O~xr8q&i29(D_(**Y_kerZ%dver;&3i3q+d5teA>aiN zoK(@;dZiMg6KYG;LSM6Y$BtoZr<652lgL2lgBFIB6NUw+E57^0NiOPfE?oF7olkkp z7I!NBBln2@uWrI$yz)A)|KWF>Z+#t(xNiXeoqdAIAN`K}@~LzD-fw<^UxOpc$SW^@ zkMH-wm9_BX6KC0L4`|-~fZN*2HGw&mCQi3|t^F-x?C5L4b&Z>dTEyFe4FM@dhy%4I zTwH>~UDG_+fTFoT%Tnx)nN z4MHApYw=W3uTqA?@*OJFiq|MALQNI13Lc>=E4L0z{5Y$xt%@7n-_2M!%LTV zz@48tG2!vY&p>ML0oVp7`4#=kukrfduAFcD0vvJQ0RHDZ2G9MueEBP9`NFR}!PjOiSsYY5^5Ljuk~rIChl93-X4x@2i%Am@r| zAbS=iAXY#WCyJFGR0$-hTA@@N$EATyQLWp7VXhEocytjlyjCy+uZTI&!r?P64tDqH zKOsg0!7Cf8J5*3LkbxK@RTL4Ss(7e@tz<_R5zv5!gw={z!4X1}921lZBGgTCQPh2N zpSGQsaimQN1E|%tY4JMHq|V1-AWj-66(PDtrkb`oVf6?nDv+SsN0dh`CIr#Fc?oqE?zPWuQrLTc!QN?cf9x?X-st(^ z#SiF~#%FBF(?5IWuIW9%Mc_~0xxq^>zQapz6z7GzM1S7^{#QBwyPSOXOY$3!{~XVL zagSf+ZZ5z5177+5jm_ranyn%1)@#_i*BLW%(9Y?{6=@PkVIopjc+LosVjW@X^vi{q z1a_FPlr=>MQkgN7l>`sOR#`1a30=U)LJSRA1}sOp17af~MOJH*E(&9Ge8`egOBs5I zio}MzT#3X$Y8%RGEmdSAQm^1%2?~f{W>VbI&>&VZDzQ4Qj=Lf;qAp?xC|E80XE>k; z5)?QhXmB^f>xhdGR8RsUPz(%6F#+am?#qO3f{5Znp?IO2&)mmMDVfwvT)Z+^rR@?i zLvK#g1`l2t38bRr*1gFE-*iTbw(8@$XSOpX-_H1iO|0<7NYW6z_+o_Vj$;F;O-XUZ zvN|C}pSDx17@G;cD%BF+s!Ly3R;XIo84C~D;PvF3{OG5-bm=PZUtcjPw7)&4#GHl{9BT*BB z1+j6UO%0xmq1Yz>x1j?856ZY$5;|o|gcwl`l+{WsXCwyT2uTf!qx=XAa;<=U6b~j{ zOOW6NFo$dzYDLtUG%cY<2ArBRF(z+PCTl~@BdJD2gdEj_jfpx^=P)Mmm zbVGMmhqBkb&8ZVo+1MaPjOC5N3J72jG%?4+a;FDN{h!3 z==*`Hft_5~Sx06+iTjx|T)V8i`N1W8x#E$@gwwX*iO-&4`mqXs3;28B@BZN>-hTHQ zFa7I#&I|YC`TGX&|BeUF!>@l;e(~u~@=Kq8if2E+dygUHgMHq3^LxB|W6os}Zp>Dk zeegj(@z5i*t1EEjvP9cqHJ|fAzD2v9Fia}Dq1%Xm-LGikNaTeR-9qO%&ccZk{VrFPNl5WA2?er}~1_HE2-`rFB8HvQ9!8 zi|1LxtZyhNVce&*0bPu=pEyogHJIP>tdY`JtgFoPoZVcQ>NQ`TJV}1=42$b)E?&CC zVhWF#b9(7K`pASw&ppP+c?bB;EqL);Z}IlqAMovO{-g6Rf56AMZva1joP*zfUVi@J zb3FdJ(|qo!^E`DH&T}{W`}D?_ueJ5LI;He@ z>%fdKIO8xNjY)`t*F>n!h6aKU5F;>WwOR=O1s(U&b1yMQVrpGQfRUIo+pG+XkeUY6 zFv*@XM4b>~WMqMaN-I;;m32RGY_bD+ML&+{?lGuKd{FmUN2^e8cQo?@=$))iG{d@M zbWLSB1ll}$X#=*(46}~ODiG{8d$tSds@sVZ)KjOh_mxZiRr>vXy1wJly^gNm=hUe^ z&YnHX$GHf+3cU7Dukz2|eUH~ZEd2Xlzv%qn1%BN450{PJr_Vw|RsaA107*qoM6N<$ Ef?kNfg8%>k literal 0 HcmV?d00001 diff --git a/icons/32-apps-akonadi.png b/icons/32-apps-akonadi.png new file mode 100644 index 0000000000000000000000000000000000000000..193d07a9aa0841c46c8810421993ad264266a0f5 GIT binary patch literal 2530 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 IM6N<$f{(Kn{Qv*} literal 0 HcmV?d00001 diff --git a/icons/64-apps-akonadi.png b/icons/64-apps-akonadi.png new file mode 100644 index 0000000000000000000000000000000000000000..e60453d22d7624887f99484e32ea45e876b81f73 GIT binary patch literal 7355 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

BIT#3e5DVZwGUg0 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 F_&;zi3Pu0` literal 0 HcmV?d00001 diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..662b9e1cb06e077b37aa01959a114a60514f54f2 GIT binary patch literal 22934 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 yFn(ANUd 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 RgO~sS002ovPDHLkV1g}>^h*E$ literal 0 HcmV?d00001 diff --git a/metainfo.yaml b/metainfo.yaml new file mode 100644 index 0000000..1127ccd --- /dev/null +++ b/metainfo.yaml @@ -0,0 +1,45 @@ +name: akonadi +description: PIM Storage Framework +maintainer: dvratil +group: kdepim +type: solution +platforms: + - name: Linux +public_lib: true +libraries: + - qmake: AkonadiCore + cmake: KF5::AkonadiCore +cmakename: KF5AkonadiCore +# TODO: kapidox currently assumes: one product == one metainfo.yaml == one cmake config file +# listing KF5AkonadiAgentBase, KF5AkonadiAgentBase, KF5AkonadiWidgets, KF5AkonadiXml as separate +# packages in CMake's worldview needs changing kapidox first -> https://bugs.kde.org/show_bug.cgi?id=410452 +irc: akonadi + +group_info: + fancyname: KDE PIM + maintainer: + - mlaurent + - dvratil + irc: kontact + mailinglist: kde-pim + platforms: + - Linux + description: KDE PIM provides set of libraries and application to access and + manage personal information like emails, contacts, events, etc. + logo: docs/kontact.svg + long_description: + - KDE PIM provides a set of libraries and applications to access and + manage personal information like emails, contacts, events, etc. + - KDE PIM provides a set of libraries to parse and interact with various + standardized PIM data formats, like RFC822 (KMime), ICAL (KCalendarCore) + or VCARD (KContacts). + - The backbone of the entire suite is Akonadi, the PIM storage framework. + Akonadi provides unified API to access and manage any kind of PIM data + regardless of the actual storage backend. + - There are quite a few other libraries that provide PIM-specific widgets + and utilities and can be useful for application developers who need to + work with PIM data in their projects. + - Please note that unless stated otherwise, none of the libraries have + stable API or ABI as of now. We are trying to keep the changes small and + we always announce big changes ahead on the mailing lists, but as the + project evolves we might need to adjust some API here and there. diff --git a/metainfo.yaml.license b/metainfo.yaml.license new file mode 100644 index 0000000..f24ebfe --- /dev/null +++ b/metainfo.yaml.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: none +SPDX-License-Identifier: CC0-1.0 diff --git a/po/ar/akonadi_knut_resource.po b/po/ar/akonadi_knut_resource.po new file mode 100644 index 0000000..d8a5fb4 --- /dev/null +++ b/po/ar/akonadi_knut_resource.po @@ -0,0 +1,93 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Seif Abaza , 2009. +# Zayed Al-Saidi , 2009. +# Safa Alfulaij , 2015, 2018. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2018-01-23 14:30+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 2.0\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "لم يُحدّد ملفّ بيانات." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "حُمّل الملفّ '%1' بنجاح." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "حدد ملف البيانات" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "ملفّ بيانات أكونادي Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "لم يُعثر على عنصر لِـ remoteid ‏%1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "التّجميعة الأبّ لم يُعثر عليها في شجرة DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "تعذّرت كتابة التّجميعة." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "التّجميعة المعدّلة لم يُعثر عليها في شجرة DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "التّجميعة المحذوفة لم يُعثر عليها في شجرة DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "التّجميعة الأبّ '%1' لم يُعثر عليها في شجرة DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "تعذّرت كتابة العنصر." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "العنصر المعدّل لم يُعثر عليه في شجرة DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "العنصر المحذوف لم يُعثر عليه في شجرة DOM." + +#~ msgid "Path to the Knut data file." +#~ msgstr "مسار ملف بيانات Knut" + +#~ msgid "Do not change the actual backend data." +#~ msgstr "لا تغير بيانات الخلفية الفعلية." diff --git a/po/ar/libakonadi5.po b/po/ar/libakonadi5.po new file mode 100644 index 0000000..4096c54 --- /dev/null +++ b/po/ar/libakonadi5.po @@ -0,0 +1,2798 @@ +# translation of libakonadi.po to +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Youssef Chahibi , 2007. +# Safa Alfulaij , 2018. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2018-08-11 12:55+0300\n" +"Last-Translator: Safa Alfulaij \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" +"X-Generator: Lokalize 2.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "صفا الفليج" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "safa1996alfulaij@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "تعذّر تسجيل الكائن في D-Bus: ‏%1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "مورد %1 نوعه %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "معرّف الوسيط" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "وسيط «أكونادي»" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "جاهز" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "غير متّصل" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "يُزامن…" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "خطأ." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "لم يُضبط" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "معرّف المورد" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "مورد «أكونادي»" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "جُلب عنصر غير صالح" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "خطأ أثناء إنشاء العنصر: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "خطأ أثناء تحديث التجميعة: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "فشل تحديث التجميعة المحلية: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "فشل تحديث العناصر المحلية: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "لا يمكن جلب العناصر بوضع عدم الاتّصال." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "يُزامن المجلد ”%1“" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "فشل جلب التجميعة للمزامنة." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "فشل جلب التجميعة لمزامنة الصفات." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "لم يعد العنصر المطلوب موجودًا" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "أُلغيت المهمّة." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "لا تجميعة كهذه." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "عُثر على تجميعات يتيمة لم تُحلّ" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "لم أجد العنصر الآخر للتعامل مع التضارب" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "تعذّر الوصول إلى واجهة D-Bus للوسيط المنشأ." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "انتهت مهلة إنشاء سيرورة الوسيط." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "تعذّر جلب نوع الوسيط ”%1“." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "تعذّر إنشاء سيرورة الوسيط." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "سيرورة التجميعة غير صالحة." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "معرّف التجميعة غير صالح." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "تعذّر جلب واجهة D-Bus للمورد ”%1“" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "انتهت مهلة مزامنة صفات التجميعة." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "التجميعة غير صالحة لنسخها" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "تجميعة الهدف غير صالحة" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "أمّ التجميعة غير صالحة" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "فشل تحليل التجميعة من الردّ" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "التجميعة غير صالحة" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "قُدّمت تجميعة غير صالحة." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "لم تُحدّد أي عناصر للنقل" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "لم يُضبط مقصد صالح" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "التجميعة غير صالحة." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "التجميعة الأم غير صالحة" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "تعذّر الاتصال بخدمة «أكونادي»." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"إصدارة ميفاق خادوم «أكونادي» غير متوافقة. تحقّق من تثبيت إحدى الإصدارات " +"المتوافقة." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "ألغى المستخدم العملية." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "خطأ مجهول." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "ردّ غير متوقّع" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "فشل إنشاء العلاقة." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "انتهت مهلة مزامنة المورد." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "تعذّر جلب التجميعة الجذر للمورد %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "لم يُقدّم معرّف المورد." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "معرّف المورد ”%1“ غير صالح" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "فشل ضبط المورد المبدئي عبر D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "فشل جلب تجميعة الموارد." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "انتهت المهلة أثناء محاولة أخذ القفل." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "فشل إنشاء الوسم." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "فشل نقل التجميعة إلى المهملات، أجهضتُ عملية الرمي" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "مُرّرت عناصر غير صالحة" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "مُرّرت تجميعة غير صالحة" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "لا تجميعة صالحة أو أن قائمة العناصر فارغة" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "تعذّر العثور على التجميعة لاسترجاعها ومورد الاسترجاع غير متوفّر" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "الاسم" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "يحمّل…" + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@window:title" +msgid "Error" +msgstr "خطأ" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"تحتوي التجميعة ”%1“ الهدف\n" +"تجميعة بالاسم ”%2“ فعلًا." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "الاسم" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "تعذّر نسخ العنصر:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "تعذّر نسخ التجميعة:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "تعذّر نقل العنصر:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "تعذّر نقل التجميعة:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "تعذّر ربط الكيان:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@title:window" +msgid "Error" +msgstr "خطأ" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "المجلدات المفضّلة" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "إجمالي الرسائل" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "الرسائل غير المقروءة" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "الحصّة" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "حجم التخزين" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "حجم تخزين المجلد الفرعي" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "غير المقروء" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "المجموع" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "الحجم" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "الوسم" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "تعذّر جلب العنصر للفهرس" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "لم يعد الفهرس متوفّرًا" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "جزء الحمولة ”%1“ ليس متوفّرًا لهذا الفهرس" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "لا جلسات متوفّرة لهذا الفهرس" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "لا عناصر متوفّرة لهذا الفهرس" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "ملحقة غير مسمّاة" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "لا يتوفّر وصف" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"تختلف إصدارة ميفاق خادوم «أكونادي» من إصدارة الميفاق التي يستخدمها هذا " +"التطبيق.\n" +"إن كنت قد حدّثت نظامك حديثًا فرجاءً اخرج منه وثمّ عُد وتحقّق من أن كل التطبيقات " +"تستخدم إصدارة الميفاق الصحيحة." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"لا يتوفّر أي وسيط «أكونادي». رجاءً تحقّق من تثبيت «إدارة كدي للمعلومات الشخصية» " +"لديك." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"إصدارة الميفاق لا تتطابق. إصدارة الخادوم أقدم (%1) من إصدارتنا (%2). إن كنت " +"قد حدّثت نظامك حديثًا فرجاءً أعِد تشغيل خادوم «أكونادي»." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"إصدارة الميفاق لا تتطابق. إصدارة الخادوم أحدث (%1) من إصدارتنا (%2). إن كنت " +"قد حدّثت نظامك حديثًا فرجاءً أعِد كل تطبيقات «إدارة كدي للمعلومات الشخصية»." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "الاختبار الآلي لِ‍«أكونادي»" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "يتحقّق ويُبلغ عن حالة خادوم «أكونادي»" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "‏© ٢٠٠٨ Volker Krause ‏" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&سيرورة وسيط جديدة…" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "ا&حذف سيرورة الوسيط" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "ا&ضبط سيرورة الوسيط" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "سيرورة وسيط جديدة" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "تعذّر إنشاء سيرورة الوسيط: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "فشل إنشاء سيرورة الوسيط" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "أأحذف سيرورة الوسيط؟" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "أمتأكّد من حذف سيرورة الوسيط المنتقاة؟" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "عاجز إلى جلب عنصر بوصة إعادة نمط." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "(أقلّ من دقيقة)" +msgstr[1] "(دقيقة واحدة)" +msgstr[2] "(دقيقتان)" +msgstr[3] "دقائق" +msgstr[4] "دقيقةً" +msgstr[5] "دقيقة" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "الجلب" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "استخدم الخيارات من المجلد أو الحساب الأب" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "زامِن عند اختيار هذا المجلد" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "زامن آليًا بعد:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "أبدًا" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr " من الدقائق" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "الأجزاء المخبّئة محليًا" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "خيارات الجلب" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "اجلب الر&سائل الكاملة دائمًا" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "اجلب &متون الرسائل عند الطلب" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "أبقِ متون الرسائل محليًا لمدة:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "للأبد" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "ابحث عن" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "استخدم المجلد مبدئيًا" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "مجلد فرعي ج&ديد…" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "أنشِئ مجلدًا فرعيًا جديدًا تحت المجلد المنتقى حاليًا" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "مجلد جديد" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "الاسم" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "تعذّر إنشاء المجلد: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "فشل إنشاء المجلد" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "عام" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "لا كائنات" +msgstr[1] "كائن واحد" +msgstr[2] "كائنان" +msgstr[3] "%1 كائنات" +msgstr[4] "%1 كائنًا" +msgstr[5] "%1 كائن" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "الا&سم:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "ا&ستخدم أيقونة مخصّصة:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "folder" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "الإحصائيّات" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "المحتوى:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "لا كائنات" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "الحجم:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "صفر بايت" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "تذكّر أن الفهرسة تأخذ بضعة دقائق." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "الصيانة" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "خطأ أثناء جلب عدد العناصر المفهرسة" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "لا عناصر مفهرسة في هذا المجلد" +msgstr[1] "عنصر واحد مفهرس في هذا المجلد" +msgstr[2] "عنصران مفهرسان في هذا المجلد" +msgstr[3] "%1 عناصر مفهرسة في هذا المجلد" +msgstr[4] "%1 عنصرًا مفهرسًا في هذا المجلد" +msgstr[5] "%1 عنصر مفهرس في هذا المجلد" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "يحسب الملفات المفهرسة…" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "الملفات" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "نوع المجلد:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "مجهول" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "العناصر" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "مجموع العناصر:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "العناصر غير مقروءة:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "الفهرسة" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "فعّل فهرسة النص كاملًا" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "يجلب عدد العناصر المفهرسة…" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "أعِد فهرسة المجلد" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "لا مجلدات" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "افتح حواريّ التجميعة" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "اختر تجميعة" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "ا&نقل هنا" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "ان&سخ هنا" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "ألغِ" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "وقت التعديل" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "الرايات" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "الصفة: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "حلّ التضاربات" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "استخدم نسختي" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "استخدم نسختهم" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "أبقِ النسختين معًا" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"تتضارب تغييراتك مع تغييرات أجراها آخر في هذه الأثناء.
إن لم ترد " +"حذف إحدى النسخ وإهمالها، فعليك دمج التغييرات هذه يدويًا.
انقر ”افتح محرّر النصوص“ لإبقاء نسخة من النصوص، وبعدها اختر " +"أي الإصدارتين هي الأصح، وأعِد فتحها وعدّلها ثانيةً بإضافة النواقص." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "البيانات" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "يبدأ خادوم «أكونادي»…" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "يُوقف خادوم «أكونادي»…" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "ان&قل هنا" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "ان&سخ هنا" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "ارب&ط هنا" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "أ&لغِ" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"تعذّر الاتصال بخدمة إدارة المعلومات الشخصية.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "تبدأ خدمة إدارة المعلومات الشخصية…" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "تُطفأ خدمة إدارة المعلومات الشخصية…" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "تُجري خدمة إدارة المعلومات الشخصية ترقية لقاعدة البيانات." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"تُجري خدمة إدارة المعلومات الشخصية ترقية لقاعدة البيانات.\n" +"يجري هذا بعد تحديث البرمجيات وهو ضروري لتحسين الأداء.\n" +"سيأخذ هذا بضعة دقائق حسب مقدار المعلومات الشخصية." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"خدمة «أكونادي» لإدارة المعلومات الشخصية لا تعمل. لا يمكن استخدام هذا التطبيق " +"دونها." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "ابدأ" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"إطار عمل «أكونادي» لإدارة المعلومات الشخصية لا يعمل.\n" +"انقر ”التفاصيل…“ لتجد معلومات مفصّلة عن هذه المشكلة." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "إطار عمل «أكونادي» لإدارة المعلومات الشخصية لا يعمل." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "التفاصيل…" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "أتريد إزالة الحساب ”%1“؟" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "أأزيل الحساب؟" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "حسابات الورود (أضِف واحدًا على الأقل):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "أ&ضِف…" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&عدّل…" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "أ&زِل" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "أعِد التشغيل" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "أحدث مجلد" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "تغيير اسم المفضّلة" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "الاسم:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "اختبار خادوم «أكونادي» الآلي" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "احفظ التقرير…" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "انسخ التقرير إلى الحافظة" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "يطلب ضبط خادوم «أكونادي» لديك مشغّل QtSQL ‏”%1“ وقد عُثر عليه في النظام." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"يطلب ضبط خادوم «أكونادي» لديك مشغّل QtSQL ‏”%1“.\n" +"المشغّلات الآتية مثبّتة: %2.\n" +"تحقّق من أن المشغّل المطلوب مثبّت." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "عُثر على مشغّل قاعدة البيانات." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "لم يُعثر على مشغّل قاعدة البيانات." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "لم يُختبر تنفيذي خادوم MySQL." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "لا يطلب الضبط الحالي خادوم MySQL داخلي." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"لقد ضبطت «أكونادي» ليستخدم خادوم MySQL ‏”%1“.\n" +"تحقّق من أن خادوم MySQL مثبّت، واضبط المسار الصحيح وتحقّق أيضًا من امتلاكك " +"تصاريح القراءة والتنفيذ المطلوبة لتنفيذي الخادوم. يفترض أن يكون اسم تنفيذي " +"الخادوم ”mysqld“. مكان التنفيذي يختلف حسب التوزيعة لديك." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "لم يُعثر على خادوم MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "لا يمكن قراءة خادوم MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "لا يمكن تنفيذ خادوم MySQL." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "عُثر على MySQL باسم غير متوقّع." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "عُثر على خادوم MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "عُثر على خادوم MySQL: ‏%1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "يمكن تنفيذ خادوم MySQL." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "فشل تنفيذ خادوم MySQL ‏”%1“ برسالة الخطأ الآتية: ”%2“" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "فشل تنفيذ خادوم MySQL." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "لم يُختبر سجل أخطاء خادوم MySQL." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "لم يُعثر على سجل أخطاء MySQL حالي." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"لم يُبلغ خادوم MySQL عن أية أخطاء وهو يبدأ التشغيل. يمكن العثور على السجل في " +"”%1“." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "لا يمكن قراءة سجل أخطاء MySQL." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "عُثر على ملف سجل أخطاء خادوم MySQL إلا أنه لا يمكن قراءته: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "يحتوي سجل خادوم MySQL على أخطاء." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "يحتوي ملف سجل أخطاء خادوم MySQL ‏”%1“ على أخطاء." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "يحتوي سجل خادوم MySQL على تحذيرات." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "يحتوي ملف سجل أخطاء خادوم MySQL ‏”%1“ على تحذيرات." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "لا يحتوي ملف سجل خادوم MySQL على أي أخطاء." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "لا يحتوي ملف سجل خادوم MySQL ‏”%1“ على أي أخطاء أو تحذيرات." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "لم يُختبر ضبط خادوم MySQL." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "عُثر على ضبط خادوم MySQL المبدئي." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "عُثر على ضبط خادوم MySQL المبدئي وقراءته ممكنة من %1" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "لم يُعثر على ضبط خادوم MySQL المبدئي." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"إما أنه لم يُعثر على ضبط خادوم MySQL المبدئي أو أن قراءته غير ممكنة. تحقّق من " +"أن تثبيت «أكونادي» لديك مكتمل وأن لديك تصاريح الوصول." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "ضبط خادوم MySQL المخصّص غير متوفّر." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "لم يُعثر ضبط خادوم MySQL المخصّص، إلا أنه اختياري." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "عُثر على ضبط خادوم MySQL المخصّص." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "عُثر على ضبط خادوم MySQL المخصّص وقراءته ممكنة من %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "لا يمكن قراءة ضبط خادوم MySQL المخصّص." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"عُثر على ضبط خادوم MySQL المخصّص في %1 إلا أن قراءته غير ممكنة. تحقّق من تصاريح " +"الوصول لديك." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "إما أنه لم يُعثر على ضبط خادوم MySQL المخصّص أو أن قراءته غير ممكنة." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "إما أنه لم يُعثر على ضبط خادوم MySQL أو أن قراءته غير ممكنة." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "يمكن استخدام ضبط خادوم MySQL." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "عُثر على ضبط خادوم MySQL في %1 وقراءته ممكنة." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "تعذّر الاتصال بخادوم PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "عُثر على خادوم PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "عُثر على خادوم PostgreSQL والاتصال يعمل." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "لم يُعثر على akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"يجب أن يكون الوصول إلى البرنامج ”akonadictl“ ممكنًا في ‎$PATH. تحقّق من تثبيتك " +"لخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "عُثر على akonadictl ويمكن استخدامه" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"عُثر على البرنامج ”%1“ للتحكم بخادوم «أكونادي» وقد نُفّذ بنجاح.\n" +"النتيجة:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "عُثر على akonadictl إلا أنه لا يمكن استخدامه" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"عُثر على البرنامج ”%1“ للتحكم بخادوم «أكونادي» إلا أنه لم يُنفّذ بنجاح.\n" +"النتيجة:\n" +"%2\n" +"تحقّق من أن خادوم «أكونادي» مثبّت كما ينبغي." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "عمليّة التحكّم بخادوم «أكونادي» مسجّلة في D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "عمليّة التحكّم بخادوم «أكونادي» مسجّلة في D-Bus ما يعني أن الخادوم يعمل." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "لم تُسجّل عمليّة التحكّم بخادوم «أكونادي» في D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"لم تُسجّل عمليّة التحكّم بخادوم «أكونادي» في D-Bus ما يعني غالبًا بأنه الخادوم لم " +"يبدأ أو أنه واجه خطأ فادحًا وهو يبدأ التشغيل." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "عمليّة خادوم «أكونادي» مسجّلة في D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "عمليّة خادوم «أكونادي» مسجّلة في D-Bus ما يعني أن الخادوم يعمل." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "لم تُسجّل عمليّة خادوم «أكونادي» في D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"لم تُسجّل عمليّة خادوم «أكونادي» في D-Bus ما يعني غالبًا بأنه الخادوم لم يبدأ أو " +"أنه واجه خطأ فادحًا وهو يبدأ التشغيل." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "فحص إصدارة الخادوم غير ممكن." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"دون وجود اتصال بالخادوم فالتحقّق من إن كانت إصدارة الميفاق تطابق المتطلبات " +"ليس ممكنًا." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "إصدارة ميفاق الخادوم قديمة للغاية." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"إصدارة ميفاق الخادوم هي %1، بينما الإصدارة %2 هي التي يطلبها العميل. إن كنت " +"قد حدّثت تطبيقات «إدارة كدي للمعلومات الشخصية» حديثًا فرجاءً أعِد تشغيل " +"«أكونادي» والتطبيقات سابقة الذكر." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "إصدارة ميفاق الخادوم حديثة للغاية." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "إصدارة ميفاق الخادوم تتطابق." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "إصدارة الميفاق الحالية هي %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "عُثر على وسطاء موارد." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "عُثر على وسيط موارد واحد على الأقل." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "لم يُعثر على أي وسيط موارد." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"لم يُعثر على أي وسيط موارد، ولا يمكن استخدام «أكونادي» دون وجود واحد. يعني " +"هذا غالبًا بأن لا وسطاء موارد مثبّتة أو أنه توجد مشكلة بالتثبيت. بحثتُ في " +"المسارات الآتية: ”%1“. ضُبط متغيّر البيئة XDG_DATA_DIRS على ”%2“، تحقّق من أن " +"القيمة تشمل كل المسارات التي يمكن أن تكون فيها وسطاء ”أكونادي» مثبتّة." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "لم يُعثر على سجل أخطاء حالي لخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "لم يُبلغ خادوم «أكونادي» عن أية أخطاء وهو يبدأ التشغيل." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "عُثر على سجل أخطاء حالي لخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"أبلغ خادوم «أكونادي» عن أخطاء وهو يبدأ التشغيل. يمكن العثور على السجل في %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "لم يُعثر على سجل أخطاء سابق لخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "لم يُبلغ خادوم «أكونادي» عن أية أخطاء في آخر مرة بدأ فيها التشغيل." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "عُثر على سجل أخطاء سابق لخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"أبلغ خادوم «أكونادي» عن أخطاء حدثت في آخر مرة بدأ فيها التشغيل. يمكن العثور " +"على السجل في %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "لم يُعثر على سجل أخطاء حالي لعمليّة التحكّم بخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"لم تُبلغ عمليّة التحكّم بخادوم «أكونادي» عن أية أخطاء وهو يبدأ التشغيل الآن." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "عُثر على سجل أخطاء حالي لعمليّة التحكّم بخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"أبلغ خادوم «أكونادي» عن أخطاء حدثت في آخر مرة بدأ فيها التشغيل.. يمكن العثور " +"على السجل في %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "لم يُعثر على سجل أخطاء سابق لعمليّة التحكّم بخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"لم تُبلغ عمليّة التحكّم بخادوم «أكونادي» عن أية أخطاء في آخر مرة بدأ فيها " +"التشغيل." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "عُثر على سجل أخطاء سابق لعمليّة التحكّم بخادوم «أكونادي»." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"أبلغ خادوم «أكونادي» عن أخطاء حدثت وهو يبدأ التشغيل الآن. يمكن العثور على " +"السجل في %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "بدأ «أكونادي» بحساب الجذر" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"يعرّضك تشغيل التطبيقات التي تتعامل مع الشابكة بحساب الجذر/المدير- يعرّضك إلى " +"مخاطر أمنية كثيرة. MySQL الذي يستخدمه تثبيت «أكونادي» هذا لا يسمح بأن يُشغّل " +"بحساب الجذر، ذلك لحمايتك من هذه المخاطر." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "«أكونادي» لا يعمل بحساب الجذر" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"«أكونادي» لا يعمل بحساب الجذر/المدير، وهذا هوا الإعداد المستحسن لنظام آمن." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "احفظ تقرير الاختبار" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "خطأ" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "تعذّر فتح الملف ”%1“" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"حدث خطأ أثناء بدء تشغيل خادوم «أكونادي». على الاختبارات الذاتية الآتية " +"مساعدتك في تعقّب المشكلة وحلّها. عندما تطلب الدعم أو تُبلغ عن العلل، رجاءً اشمل " +"هذا التقرير مع الطلب/العلة." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "التفاصيل" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

لتجد فوائد أكثر عن مواجهة الأعطال، رجاءً راجع userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "م&جلد جديد…" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "جديد" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "ا&حذف المجلد" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "احذف" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "زامِن المجلد" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "زامِن" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&خصائص المجلد" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "الخصائص" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "أل&صِق" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "ألصِق" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "أدِر الا&شتراكات المحلية…" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "أدِر الاشتراكات المحلية" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "أضِف إلى المجلدات المفضّلة" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "أضِف إلى المفضّلة" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "أزِل من المجلدات المفضّلة" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "أزِل من المفضّلة" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "غيّر اسم المفضّلة…" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "غيّر الاسم" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "انسخ المجلد إلى…" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "انسخ إلى" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "انسخ العنصر إلى…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "انقل العنصر إلى…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "انقل إلى" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "انقل المجلد إلى…" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&قصّ العنصر" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "قصّ" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&قصّ المجلد" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "أنشِئ موردًا" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "احذف المورد" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "خصائص المور&د" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "زامِن المورد" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "اعمل بلا اتّصال" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "زا&مِن المجلدات تكراريًا" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "زامِن تكراريًا" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "ا&نقل المجلد إلى المهملات" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "انقل المجلد إلى المهملات" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "ا&نقل العنصر إلى المهملات" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "انقل العنصر إلى المهملات" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "ا&ستعد المجلد من المهملات" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "استعد المجلد من المهملات" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "ا&ستعد العنصر من المهملات" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "استعد العنصر من المهملات" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "ا&ستعد التجميعة من المهملات" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "استعد التجميعة من المهملات" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "زا&مِن المجلدات المفضّلة" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "زامِن المجلدات المفضّلة" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "زامِن شجرة المجلدات" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "ا&نسخ المجلد" +msgstr[1] "ا&نسخ المجلد" +msgstr[2] "ا&نسخ المجلدين" +msgstr[3] "ا&نسخ %1 مجلدات" +msgstr[4] "ا&نسخ %1 مجلدًا" +msgstr[5] "ا&نسخ %1 مجلد" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "ا&نسخ العنصر" +msgstr[1] "ا&نسخ العنصر" +msgstr[2] "ا&نسخ العنصرين" +msgstr[3] "ا&نسخ %1 عناصر" +msgstr[4] "ا&نسخ %1 عنصرًا" +msgstr[5] "ا&نسخ %1 عنصر" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&قصّ العنصر" +msgstr[1] "&قصّ العنصر" +msgstr[2] "&قصّ العنصرين" +msgstr[3] "&قصّ %1 عناصر" +msgstr[4] "&قصّ %1 عنصرًا" +msgstr[5] "&قصّ %1 عنصر" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&قصّ المجلد" +msgstr[1] "&قصّ المجلد" +msgstr[2] "&قصّ المجلدين" +msgstr[3] "&قصّ %1 مجلدات" +msgstr[4] "&قصّ %1 مجلدًا" +msgstr[5] "&قصّ %1 مجلد" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "ا&حذف العنصر" +msgstr[1] "ا&حذف العنصر" +msgstr[2] "ا&حذف العنصرين" +msgstr[3] "ا&حذف %1 عناصر" +msgstr[4] "ا&حذف %1 عنصرًا" +msgstr[5] "ا&حذف %1 عنصر" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "ا&حذف المجلد" +msgstr[1] "ا&حذف المجلد" +msgstr[2] "ا&حذف المجلدين" +msgstr[3] "ا&حذف %1 مجلدات" +msgstr[4] "ا&حذف %1 مجلدًا" +msgstr[5] "ا&حذف %1 مجلد" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "زامِن المجلد" +msgstr[1] "زامِن المجلد" +msgstr[2] "زامِن المجلدين" +msgstr[3] "زامِن %1 مجلدات" +msgstr[4] "زامِن %1 مجلدًا" +msgstr[5] "زامِن %1 مجلد" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "ا&حذف المورد" +msgstr[1] "ا&حذف المورد" +msgstr[2] "ا&حذف الموردين" +msgstr[3] "ا&حذف %1 موارد" +msgstr[4] "ا&حذف %1 موردًا" +msgstr[5] "ا&حذف %1 مورد" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "زا&مِن المورد" +msgstr[1] "زا&مِن المورد" +msgstr[2] "زا&مِن الموردين" +msgstr[3] "زا&مِن %1 موارد" +msgstr[4] "زا&مِن %1 موردًا" +msgstr[5] "زا&مِن %1 مورد" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "انسخ المجلد" +msgstr[1] "انسخ المجلد" +msgstr[2] "انسخ المجلدين" +msgstr[3] "انسخ %1 مجلدات" +msgstr[4] "انسخ %1 مجلدًا" +msgstr[5] "انسخ %1 مجلد" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "انسخ العنصر" +msgstr[1] "انسخ العنصر" +msgstr[2] "انسخ العنصرين" +msgstr[3] "انسخ %1 عناصر" +msgstr[4] "انسخ %1 عنصرًا" +msgstr[5] "انسخ %1 عنصر" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "قصّ العنصر" +msgstr[1] "قصّ العنصر" +msgstr[2] "قصّ العنصرين" +msgstr[3] "قصّ %1 عناصر" +msgstr[4] "قصّ %1 عنصرًا" +msgstr[5] "قصّ %1 عنصر" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "قصّ المجلد" +msgstr[1] "قصّ المجلد" +msgstr[2] "قصّ المجلدين" +msgstr[3] "قصّ %1 مجلدات" +msgstr[4] "قصّ %1 مجلدًا" +msgstr[5] "قصّ %1 مجلد" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "احذف العنصر" +msgstr[1] "احذف العنصر" +msgstr[2] "احذف العنصرين" +msgstr[3] "احذف %1 عناصر" +msgstr[4] "احذف %1 عنصرًا" +msgstr[5] "احذف %1 عنصر" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "احذف المجلد" +msgstr[1] "احذف المجلد" +msgstr[2] "احذف المجلدين" +msgstr[3] "احذف %1 مجلدات" +msgstr[4] "احذف %1 مجلدًا" +msgstr[5] "احذف %1 مجلد" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "زامِن المجلد" +msgstr[1] "زامِن المجلد" +msgstr[2] "زامِن المجلدين" +msgstr[3] "زامِن %1 مجلدات" +msgstr[4] "زامِن %1 مجلدًا" +msgstr[5] "زامِن %1 مجلد" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "احذف المورد" +msgstr[1] "احذف المورد" +msgstr[2] "احذف الموردين" +msgstr[3] "احذف %1 موارد" +msgstr[4] "احذف %1 موردًا" +msgstr[5] "احذف %1 مورد" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "زامِن المورد" +msgstr[1] "زامِن المورد" +msgstr[2] "زامِن الموردين" +msgstr[3] "زامِن %1 موارد" +msgstr[4] "زامِن %1 موردًا" +msgstr[5] "زامِن %1 مورد" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "الاسم" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "لا شيء لحذفه." +msgstr[1] "أمتأكّد من حذف هذا المجلد مع مجلداته الفرعيّة؟" +msgstr[2] "أمتأكّد من حذف هذين المجلدين مع مجلداتهما الفرعيّة؟" +msgstr[3] "أمتأكّد من حذف %1 عناصر مع مجلداتها الفرعيّة؟" +msgstr[4] "أمتأكّد من حذف %1 عنصرًا مع مجلداتها الفرعيّة؟" +msgstr[5] "أمتأكّد من حذف %1 عنصر مع مجلداتها الفرعيّة؟" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "أأحذف المجلد؟" +msgstr[1] "أأحذف المجلد؟" +msgstr[2] "أأحذف المجلدين؟" +msgstr[3] "أأحذف المجلدات؟" +msgstr[4] "أأحذف المجلدات؟" +msgstr[5] "أأحذف المجلدات؟" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "تعذّر حذف المجلد: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "فشل حذف المجلد" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "خصائص المجلد %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "أمتأكّد من حذف العنصر المحدّد؟" +msgstr[1] "أمتأكّد من حذف العنصر المحدّد؟" +msgstr[2] "أمتأكّد من حذف العنصرين المحدّدين؟" +msgstr[3] "أمتأكّد من حذف %1 عناصر محدّدة؟" +msgstr[4] "أمتأكّد من حذف %1 عنصرًا محدّدًا؟" +msgstr[5] "أمتأكّد من حذف %1 عنصر محدّد؟" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "أأحذف العنصر؟" +msgstr[1] "أأحذف العنصر؟" +msgstr[2] "أأحذف العنصرين؟" +msgstr[3] "أأحذف العناصر؟" +msgstr[4] "أأحذف العناصر؟" +msgstr[5] "أأحذف العناصر؟" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "تعذّر حذف العنصر: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "فشل حذف العنصر" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "تغيير اسم المفضّلة" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "الاسم:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "مورد جديد" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "تعذّر إنشاء المورد الجديد: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "فشل إنشاء المورد" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "أمتأكّد من حذف هذا المورد؟" +msgstr[1] "أمتأكّد من حذف هذا المورد؟" +msgstr[2] "أمتأكّد من حذف هذين الموردين؟" +msgstr[3] "أمتأكّد من حذف %1 موارد؟" +msgstr[4] "أمتأكّد من حذف %1 موردًا؟" +msgstr[5] "أمتأكّد من حذف %1 مورد؟" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "أأحذف المورد؟" +msgstr[1] "أأحذف المورد؟" +msgstr[2] "أأحذف الموردين؟" +msgstr[3] "أأحذف الموارد؟" +msgstr[4] "أأحذف الموارد؟" +msgstr[5] "أأحذف الموارد؟" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "تعذّر لصق البيانات: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "فشل اللصق" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "لا يمكننا إضافة ”/“ في اسم المجلد." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "خطأ أثناء إنشاء مجلد جديد" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "لا يمكننا إضافة ”.“ في بداية اسم المجلد أو نهايته." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"من الضروري وقبل مزامنة المجلد ”%1“ ربط المورد بالشابكة. أتريد الاتصال " +"بالشابكة الآن؟" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "الحساب ”%1“ غير متّصل" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "اتّصل بالشابكة" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "انقل إلى هذا المجلد" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "انسخ في هذا المجلد" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to update subscription: %1" +msgstr "فشل إنشاء العلاقة." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "الاشتراكات المحلية" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "الاشتراكات المحلية" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "ابحث عن:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "المشترِك فيها فقط" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "اشترك" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "أزِل الاشتراك" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "فشل إنشاء وسم جديد" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "حدث خطأ أثناء إنشاء وسم جديد" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "أمتأكّد من إزالة الوسم %1؟" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "حذف الوسم" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "احذف الوسم" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "اضبط أيّ الوسوم يجب تطبيقها." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "أنشِئ وسمًا جديدًا" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Manage Tags" +msgid "Manage Tags" +msgstr "أدِر الوسوم" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "حذف الوسم" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "امسح" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, fuzzy, kde-format +#| msgid "Click to Add Tags" +msgid "Click to add tags" +msgstr "انقر لإضافة وسوم" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "…" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "محوّل «أكونادي» إلى XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "يُحوّل شجرة تجميعة «أكونادي» فرعية إلى ملف XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "‏© ٢٠٠٩ Volker Krause ‏" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "لا بيانات محمّلة." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "لم يحدّد اسم الملف" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "تعذّر فتح ملف البيانات ”%1“." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "الملف %1 غير موجود." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "تعذّر تحليل ملف البيانات ”%1“." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "تعذّر تحميل تعريف المخطّط وتحليله." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "تعذّر إنشاء سياق محلّل المخطّطات." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "تعذّر إنشاء المخطّط." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "تعذّر إنشاء سياق التحقّق من المخطّط." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "تنسيق الملف غير صالح." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "تعذّر تحليل ملف البيانات: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "تعذّر العثور على التجميعة %1" + +#~ msgid "Id" +#~ msgstr "المعرّف" + +#~ msgid "Remote Id" +#~ msgstr "معرّف البعيد" + +#~ msgid "MimeType" +#~ msgstr "نوع Mime" + +#~ msgid "Default Name" +#~ msgstr "الاسم المبدئي" + +#, fuzzy +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "احذف مجلد" + +#, fuzzy +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "إلغاء" + +#, fuzzy +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "غير مقروء" + +#, fuzzy +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "المجموع" + +#, fuzzy +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "مورد" + +#, fuzzy +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "الاسم" + +#, fuzzy +#~ msgid "Cannot list root collection." +#~ msgstr "لا مجموعة." + +#, fuzzy +#~ msgid "New Folder..." +#~ msgstr "مجلد جديد..." + +#, fuzzy +#~ msgid "Resource Properties" +#~ msgstr "مجلّد إنشاء failed" + +#, fuzzy +#~ msgid "Cache" +#~ msgstr "إلغاء" + +#, fuzzy +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "مورد" + +#, fuzzy +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "لا مجموعة." +#~ msgstr[1] "لا مجموعة." +#~ msgstr[2] "لا مجموعة." +#~ msgstr[3] "لا مجموعة." +#~ msgstr[4] "لا مجموعة." +#~ msgstr[5] "لا مجموعة." + +#, fuzzy +#~ msgid "Copy failed" +#~ msgstr "نسخ" + +#, fuzzy +#~ msgctxt "@info" +#~ msgid "Unable to fetch collection in replay mode." +#~ msgstr "عاجز إلى جلب مجموعة بوصة إعادة نمط." diff --git a/po/az/akonadi_knut_resource.po b/po/az/akonadi_knut_resource.po new file mode 100644 index 0000000..e06a628 --- /dev/null +++ b/po/az/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the akonadi package. +# +# Kheyyam Gojayev , 2020. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2020-08-05 18:24+0400\n" +"Last-Translator: Kheyyam Gojayev \n" +"Language-Team: Azerbaijan\n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.04.3\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Verilənlər faylı seçilməyib" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "\"%1\" faylı uğurla yükləndi." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Verilənlər faylını seçmək" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut verilənlər faylı" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "remoteid %1 üçün element tapılmadı" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM ardıcıllığında ana kolleksiya tapılmadı." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Kolleksiyanı yazmaq mümkün deyil." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM ardıcıllığında dəyişdirilmiş kolleksiya tapılmadı." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM ardıcıllığında silinmiş kolleksiya tapılmadı." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM ardıcıllığında %1 ana kolleksiyası tapılmadı." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Elementi yazmaq mümkün olmadı." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM ardıcıllığında dəyişdirilmiş element tapılmadı." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM ardıcıllığında silinmiş element tapılmadı." diff --git a/po/az/libakonadi5.po b/po/az/libakonadi5.po new file mode 100644 index 0000000..9408009 --- /dev/null +++ b/po/az/libakonadi5.po @@ -0,0 +1,2580 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the akonadi package. +# +# Kheyyam Gojayev , 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 12:49+0400\n" +"Last-Translator: Kheyyam Gojayev \n" +"Language-Team: Azerbaijani \n" +"Language: az\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.12.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xəyyam Qocayev" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xxmn77@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Hal-hazırda konfiqurasiya edilmiş hesab yoxdur." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Hesabların inteqrasiyası dəstəklənmir" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Obyekt dbus-da qeydiyyata alınmadı: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%2 növünün %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Təmsilçinin kimliyi" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Təmsilçisi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Hazırdır" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Şəbəkədən kənar" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Eyniləşdirilir..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Xəta." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Tənzimlənmədi" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Məlumat mənbənin identifikatoru" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi Məlumat Mənbəyi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Yararsız element alındı" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Yaradılması zamanı xəta baş verən element: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Yenilənməsində xəta baş verən kolleksiya: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Yerli kolleksiyanın yenilənməsi baş tutmadı: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Yerli elementlərin yenilənməsi baş tutmadı: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Verilənləri şəbəkəyə qoşulmadan almaq mümkün deyil." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "\"%1\" qovluğunun eyniləşdirilməsi" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Kolleksiyaları eyniləşdirmək üçün almaq, baş tutmadı." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Atributlarının eyniləşdirilməsi üçün kolleksiyanın alınması baş tutmadı." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Tələb olunan element artıq mövcud deyil" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Əməliyyat ləğv edildi." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Kolleksiyalar tapılmadı." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Həll edilməmiş, sahibsiz kolleksiyalar tapıldı" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Ziddiyətin həlli üçün başqa element tapılmadı" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Yaradılmış təmsilçinin D-Bus interfeysinə girişi mümkün deyil." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Təmsilçi nümunəsinin yaradılması vaxtı bitdi." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "\"%1\" təmsilçi növünü əldə etmək mümkün deyil." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Təmsilçi nümunəsi yaratmaq mümkün deyil." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Xətalı kolleksiya nümunələri." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Xətalı mənbə nümunələri." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "\"%1\" mənbəsi üçün D-Bus interfeysini əldə etmək mümkün deyil" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Kolleksiya atributlarının eyniləşdirilməsi vaxtı bitdi." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Kopyalamaq üçün xətalı kolleksiyalar" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Xətalı təyinat kolleksiyası" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Xətalı ana kolleksiya" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Cavabdakı kolleksiyanı təhlil etmək baş tutmadı" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Xətalı kolleksiya" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Xətalı kolleksiya verilib." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Köçürmək üçün obyektlər göstərilməyib" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Düzgün təyinat verilməyib" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Xətalı kolleksiya." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Xətalı ana kolleksiya" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Akonadi xidmətinə qoşulmaq mümkün deyil." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi serverinin protokol versiyası uyğunsuzdur. Uyğun versiyanın " +"quraşdırıldığına əmin olun." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "İstifadəçi əməliyyatı ləğv etdi" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Naməlum xəta." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Gözlənilməz cavab" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Əlaqə yaradıla bilmədi." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Mənbə eyniləşdirilməsi üç. vaxt bitdi." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "%1 mənbənin kök kolleksiyaları almaq mümkün deyil." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Mənbə İD-si göstərilməyib." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Mənbənin xətalı \"%1\" identifikatoru" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Standart mənbəni D-Bus vasitəsi ilə tənzimləmək baş tutmadı." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Mənbə kolleksiyalarının alınması baş tutmadı." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Xidmətin kilidlənməsi cəhdinin vaxtı bitdi." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Yarlıq yaradılması baş tutmadı." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Kolleksiyanın səbətə atılması baş tutmadı, əməliyyat kəsildi" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Xətalı elemenlər göndərildi" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Xətalı kolleksiyalar göndərildi" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Xətalı kolleksiyalar və ya boş siyahı" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Bərpa etmək üçün kolleksiyalar və mənbələr tapılmadı" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Ad" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Yüklənir..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Xəta" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"\"%1\" hədəf kolleksiyasının tərkibində\n" +"artıq \"%2\" adlı kolleksiya var." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Ad" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Kopyalanması mümkün olmayan element: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kopyalanması mümkün olmayan kolleksiya: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Köçürülməsi mümkün olmayan element: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Köçürülməsi mümkün olmayan kolleksiya: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" +"Obyektə və ya kolleksiyaya keçid yaratmaq mümkün olmadı: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Xəta" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Seçmə Qovluqlar" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Ümumi İsmarıclar" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Oxunmamış İsmarıclar" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Məhdudiyyət" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Yaddaş Miqdarı" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Alt qovluğun yaddaş ölçüsü" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Oxunmamış" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Ümumi" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Ölçü" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Yarlıq" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "İndeks üçün element tapılmadı" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "İndeks daha mövcüd deyil" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Bu indeks üçün \"%1\" faydalı məlumat hissəsi yoxdur" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Bu indeks üçün sesiya yoxdur" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Bu indeks üçün element yoxdur" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Naməlum əlavə" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Açıklaması yoxdur" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi serveri protokolunun versiyası bu tətbiqin istifadə etdiyi " +"protokolun versiyasından fərqlənir.\n" +"Əgər sisteminizi yenicə yeniləmisinizsə, bütün tətbiqlərin son protokol " +"versiyasını istifadə etməsinə əmin olmaq üçün sistemdən çıxın və yenidən " +"daxil olun." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Akonadi Təmsilçisi mövcud deyil. Lütfən KDE PİM quraşdırmanızı yoxlayın" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Protokolun versiyası uyğun gəlmir. Serverdəki versiya (%1) müştəri " +"tərəfindəki versiyadan (%2) köhnədir. Əgər sisteminizi yenicə " +"yeniləmisinizsə Akonadi serverini yenidən başladın." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Protokolun versiyası uyğun gəlmir. Serverdəki versiya (%1) müştəri " +"tərəfindəki versiyadan (%2) köhnədir. Əgər sisteminizi yenicə " +"yeniləmisinizsə bütün KDE PİM tətbiqlərinii yenidən başladın." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Daxili Akonadi Yoxlaması" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi serverinin vəziyyətini yoxlamaq və hesabat vermək" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Yeni Təmsilçi Nümunəsi Yaratmaq..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Təmsilçi Nümunəsini &Silmək" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Təmsilçi Nümunəsini &Ayarlamaq" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Yeni Təmsilçi Nümunəsi" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Yaradıla bilməyən təmsilçi nümunəsi: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Təmsilçi nümunəsi yaradılması baş tutmadı" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Təmsilçi nümunəsi silinsin?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Seçilmiş təmsilçi nümunəsini silmək istədiyinizə əminsiniz?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 Tənzimləməsi" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 İstifadəçi Təlimatı" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "%1 Haqqında" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Tənzimləmə dialoqu pəncərəsi başqa pəncərədə açılıb" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "%1 üçün tənzimləmə artıq başqa bir yerdə açılıb." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "%1 tənzimləmə dialoqunun qeydiyyatı baş tutmadı." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "dəqiqə" +msgstr[1] "dəqiqələr" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Bərpa edilmə" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Ana qovluq və ya istifadəçi hesabı parametrlərini itifadə etmək" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Bu qovluğu seçən zaman eyniləşdirmək" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Avtomatik eyniləşdirmə mərhələsi:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Heç vaxt" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "dəqiqələr" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Yerli keşlənmiş hissələr" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Bərpa etmə parametrləri" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Həmişə bütüm is&marıcları bərpa etmək" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "İsmarıcın tərkiblərini lazım gəldikdə bə&rpa etmək" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "İsmarıcın tərkiblərini bu yerdə saxlamaq:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Həmişəlik" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Axtarış" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Qovluğu standart olaraq istifadə etmək" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Ye&ni alt qovluq yaratmaq..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Cari seçilmiş qovluğun altında yeni alt qovluq yaratmaq" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Yeni Qovluq" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Adı" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Yaradıla bilməyən qovluq: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Qovluq yaratmaq baş tutmadı" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Əsas" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Bir obyekt" +msgstr[1] "%1 obyekt" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Adı:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Ba&şqa nişan:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "qovluq" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Tərkibi:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 obyekt" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Ölçüsü:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 bayt" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "İndeksləmə bir neçə dəqiqə vaxta ala bilər." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Xidmət göstərmək" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "İndekslənmiş elementlərin sayının alınması xətası" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 element bu qovluqda indekslənib" +msgstr[1] "%1 element bu qovluqda indekslənib" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "İndekslənmiş elementlər hesablanır..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fayllar" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Qovluq növü:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "naməlum" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementlər" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Ümumi elementlər:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Oxunmamış elementlər:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "İndekslənmə" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Tam mətnli indekslənməni aktiv etmək" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "İndekslənmiş elementlərin sayı alınır..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Qovluqları yenidən indeksləmək" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Qovluq yoxdur" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Kolleksiya dialoqunu açmaq" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Kolleksiyanı seçmək" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Bura &Köçürmək" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Bura Kopyalamaq" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "İmtina" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Dəyişdirilmə vaxtı" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Bayraqlar" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Ziddiyyətllərin həlli" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Mənim versiyamı istifadə etmək" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Onların versiyasını istifadə etmək" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Hər iki versiyanı istifadə etmək" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Dəyişikliklər bu vaxt başqasının etdiyi dəyişikliklərlə ziddiyyət təşkil " +"edir.
Əgər bir versiya kənarlaşdırılmazsa bu dəyişiklikləri əl ilə " +"inteqrasiya etməlisiniz.
Mətnlərin nüsxəsini saxlamaq üçün \"Mətn Redaktorunu Açmaq\" klikləyin, sonra daha " +"düzgün versiyanı seçin, və onu təkrar açaraq yenidən dəyişdirin və " +"çatışmayan hissələri əlavə edin." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Verilənlər" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi serveri açılır..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi serveri bağlanır..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Bura &Köçürmək" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "Bura &Kopyalamaq" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Link Yaratmaq" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "İmtina &etmək" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Şəxsi məlumatları idarəetmə xidmətinə qoşula bilmir.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Şəxsi məlumatlar idarəetmə xidməti açılır..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Şəxsi məlumatlar idarəetmə xidməti bağlanır..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Şəxsi məlumatlar idarəetmə xidməti verilənlər bazasını yeniləyir." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Şəxsi məlumatlar idarəetmə xidməti verilənlər bazasını yeniləyir.\n" +"Bu Proqram Təminatı yeniləndikdən sonra baş verir və iş fəaliyyətini " +"optimallaşdırmaq üçün lazımlıdır.\n" +"Şəxsi məlumatların miqdarından asılı olaraq bu əməliyyat bir neçə dəqiqə " +"vaxt apara bilər." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi şəxsi məlumatlar idarəetmə xidməti fəaliyyət göstərmir. Bu tətbiq " +"onsuz istifadə oluna bilməz." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Başlatmaq" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi şəxsi məlumatlar idarəetmə xidməti strukturu işlək deyil.\n" +"Bu problem haqqında ətraflı məlumat üçün \"Təfərrüatlar...\"-a keçid edin." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi şəxsi məlumatlar idarəetmə xidməti işləmir." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Təfərrüatlar..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "\"%1\" istifadəçi hesabını silmək istəyirsiniz?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "İstifadəçi hesabı silinsin?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Daxil olan ( azı bir hesab olmalıdır):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Əlavə etmək..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Dəyişdir&mək..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "Silm&ək" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Yenidən başlatmaq" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Sonuncu qovluqlar" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Seçmə qovluqların aını dəyişmək" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Adı:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Daxili Akonadi server yoxlaması" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Hesabatı saxlamaq..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Hesabatı mübadilə beferinə kopyalamaq" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"\"%1\" QtSQL sürücüsü sizin cari Akonadi serverinin ayarını tələb edir və bu " +"ayar sisteminizdə tapıldı" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"\"%1\" QtSQL sürücüsü sizin cari Akonadi serverinin ayarını tələb edir.\n" +"Bu sürücülər quraşdırılıb: %2\n" +"Əmin olun ki, tələb olunan sürücülər quraşdırılıb." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Verilənlər bazası sürücüsü tapıldı." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Verilənlər bazası sürücüsü tapılmadı." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL icra faylı test edilmədi." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Cari tənzimləmə daxili MySQL serveri tələb etmir." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"\"%1\" MySQL serverini istifadə etmək üçün hal-hazırda Akonadi tənzimlənib.\n" +"Əmin olun ki, MySQL serveri sisteminizə quraşdırılıb. Doğru yolu seçin və " +"əmin olun ki, sizin serverdə zəruri oxuma və icra etmə imtiyazlarınız var. " +"MySQL serverinin icra faylı adətən 'mysqld' adlanır və onun yerləşmə yeri " +"ditribütordan asılı olaraq fərlənir." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL serveri tapılmadı." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL serveri oxuna bilən deyil." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL serveri icra faylı icra edilə bilən deyil." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL gözlənilməz adla tapıldı." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL serveri tapıldı." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL serveri tapıldı: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL serveri icra faylı icra edilə biləndir." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "\"%1\" MySQL serverinin işə salınması xətası: \"%2\"" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL serverinin işə salınması baş tutmadı." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL serveri jurnalı yoxlanılmadı." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Cari MySQL xəta qeydləri jurnalı tapılmadı" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL serveri başladılan zaman heç bir xəta hesabatı göndərmədi. Jurnal " +"\"%1\" faylında tapıla bilər." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL xəta jurnalı oxuna bilən deyil." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "MySQL sever xəta qyedləri faylı tapıldı, lakin, oxuna bilən deyil: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL server jurnalında səhvlər var." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL serveri xəta qeydləri \"%1\" faylında xətalar var." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL serveri jurnalında xəbərdarlıqlar var." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "\"%1\" MySQL serveri jurnalında xəbərdarlıqlar var." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL serveri jurnalında xətalar yoxdur." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"\"%1\" MySQL serveri jurnalında hər hansı xəta və ya xəbərdarlıq yoxdur." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL serverinin tənzimləmələri yoxlanılmayıb." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL serveri standart tənzimləmələri tapıldı." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "MySQL serveri üçün standart tənzimləməsi tapıldı və %1-də oxunandır." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL serverinin standart tənzimləməsi tapılmadı." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL serveri üçün standart tənzimləməsi tapılmadı və ya oxuna bilən deyil. " +"Akonadi serverinin tam quraşdırıldığını və tələb olunan giriş " +"imtiyazlarınızın mövcudluğunu yoxlayın." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Ayarlanmış MySQL tənzimləmələri mövcud deyil." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"MySQL severi üçün ayarlanmış tənzimləmələr tapılmadı, ancaq bu o qədər də " +"lazımlı deyil." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL ayarlanmış tənziçləmələri tapıldı." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "MySQL serveri üçün ayarlanmış tənzimləməsi tapıldı və %1-də oxunandır" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Ayarlanmış MySQL serveri tənzimləmələri oxuna bilən deyil." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL serveri üçün ayarlanmış tənzimləməsi %1-də(da) tapıldı, lakin oxuna " +"bilən deyil. Giriş imtiyazlarınızı yoxlayın." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL serveri tənzimləmələri tapılmadı və ya oxuna bilən deyil." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL server tənzimləmələri tapılmadı və ya oxuna biləb deyil." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL server tənzimləmələri istifadə üçün yararlıdır." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "%1-də MySQL serveri tənzimləmələri tapıldı və oxuna biləndir." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL serverinə qoşulmaq mümkün deyil." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL serveri tapıldı." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL server tapıldı və şəbəkə əlaqəsi işləyir." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl tapılmadı" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"\"akonadictl\" proqramının $PATH daxilində əlçatan olması lazımdır. Əmin " +"olun ki, Akonadi serveri quraşdırılıb." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl tapıldı və istifadəyə yararlıdır" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi serverini idarə etmək üçün \"%1\" proqramı tapıldı və " +"müvəffəqiyyətlə icra edildi.\n" +"Nəticə:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl tapıldı ancaq istifadəyə yararlı deyil" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi serverini idarə etmək üçün \"%1\" proqramı tapıldı " +"ancaqmüvəffəqiyyətlə icra edilə bilmədi.\n" +"Nəticə:\n" +"%2\n" +"Əmin olun ki, Akonadi serveri düzgün quraşdırılıb." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi idarəetmə prosesi D-Bus-da qeydə alınıb." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi idarəetmə prosesi D-Bus-da qeydə alınıb, bu da adətən onun " +"istifadəyə hazır olduğunu göstərir." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi idarəetmə prosesi D-Bus-da qeydə alınmayıb." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi idarəetmə prosesi D-Bus-da qeydə alınmayıb, bu da adətən onu " +"göstəriri ki, o işə salına bilmədi və ya başladıldıqda ciddi xəta baş verdi." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi serveri prosesi D-Bus-da qeydə alınıb." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi serveri prosesi D-Bus-da qeydə alınıb, bu da adətən onun istifadəyə " +"hazır olduğunu göstərir." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi serveri prosesi D-Bus-da qeydə alınmayıb." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi serveri prosesi D-Bus-da qeydə alınmayıb, bu da adətən onu göstəriri " +"ki, o işə salına bilmədi və ya başladıldıqda ciddi xəta baş verdi." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protokolun versiyasını yoxlamaq mümkün deyil." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Serverə qoşulmadan, protokol versiyasının tələblərə uyğun olub olmadığını " +"yoxlamaq mümkün deyil." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Server protokol versiyası çox köhnədir." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Server protokolunun versiyası %1, lakin müştəri tərəfindən %2 versiyası " +"tələb olunur. Əgər KDE PİM-i yenicə yeniləmisinizsə, lütfən hər iki - " +"Akonadi və KDE PİM tətbiqlərini yenidən başladın." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Sever protokolunun versiyası çox yenidir." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Server protokolunun versiyası uyğundur." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Caro protokol versiyası %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Mənbə təmsilçiləri tapıldı." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Əna azı bir mənbə təmsilçisi tapıldı." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Mənbə təmsilçiləri tapılmadı" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Mənbə təmsilçiləri tapıldı, bunlardan ən azı biri olmadan Akonadi istifadə " +"edilə bilməz. Bu adətən o anlama gəlir ki, mənbə təmsilçiləri " +"quraşdırılmayıb və ya onların quraşdırılma problemi var. Aşağıdakı yollar " +"axtarıldı: \"%1\". DG_DATA_DIRS dəyişkən mühiti \"%2\" kimi təyin olunub, " +"bunun Akonadi təmsilçilərinin quraşdırıldığı bütün yollardan ibarət oduğuna " +"əmin olun." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Cari Akonadi serveri xəta jurnalı tapılmadı." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi serveri sonuncu işə düşmə zamanı hər hansı xəta bildirmədi." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Cari Akonadi serveri xəta qeydləri tapıldı." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi serveri hazırkı işə düşmə zamanı xətalar olduğunu bildirdi.Bu xəta " +"hesabatını %1 faylında tapa bilərsiniz." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Əvvəlki Akonadi serveri xəta qeydləri tapıldı." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Akonadi serveri əvvəlki işə düşmə zamanı hər hansı xəta hesabatı bildirmədi." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Əvvəlki Akonadi serveri xəta jurnalı tapıldı." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi serveri onun əvvəlki işə düşməsi zamanı xətaların olduğunu bildirdi. " +"Bu xətanı %1 faylında tapa bilərsiniz." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Cari Akonadi idarəetmə xəta qeydi tapılmadı." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadi idarəetmə prosesi onun işə düşməsi zamanı hər hansı xəta bildirmədi." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Cari Akonadi idarəetmə xəta qeydi tapıldı." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi idarəetmə prosesi onun işə düşməsi zamanı xətalar olduğunu bildirdi. " +"Bu xəta haesabatını %1 faylında tapa bilərsiniz." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Əvvəlki Akonadi idarəetmə xəta qeydləri tapılmadı." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi idarəetmə prosesi əvvəlki işə düşmə zamanı hər hansı xəta bildirmədi." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Əvvəlki Akonadi idarəetmə xəta jurnalı tapıldı." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi idarəetmə orosesi onun işə düşməsi zamanı xətalar olduğunu bildirdi. " +"Xəta qeydlərini %1 faylında tapa bilərsiniz." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi kök imtiyazı ilə başladıldı." + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Kök/inzibatçı imtiyazları ilə başladılan internet tətbiqlər sisteminizi, " +"onun təhlükəsizliyinin pozulması riski altına qoyur. MySQL, bu Akonadi " +"quraşdırıcısını istifadə edərək sisteminizi təhlükələrdən qorumaq üçün kök/" +"inzibatçı imtiyazları ilə işləməyə icazə vermir." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi kök imtiyazları ilə işləmir" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi kök/inzibatçı istifadəçisi adından başladılmır, hansı ki, bu yalnız " +"təhlükəsiz sistemlər üçün tövsiyyə olunur." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Hesabatı saxlamaq" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Xəta" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Fayl '%1' açıla bilmir" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi serverinin işə düşməsi zamanı xəta baş verdi. Aşağıdakı daxili " +"yoxlamalar bu problemi izləməyə və həll etməyə kömək edə bilər. Dəstək tələb " +"edərkən və ya xətaları bildirərkən, lütfən həmişə bu hesabatı daxil edin." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Təfərrüatlar" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Problemin həlli üçün bir çox tovsiyyələri bu veb səhifəsindən tapa " +"bilərsiniz userbase.kde.org/" +"Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Yeni Qovluq..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Yeni" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Qovluğu silin" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Silmək" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "Qovluğu &Eyniləşdirin" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Eyniləşdirmə" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Qovluğun &Xüsusiyyətləri" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Xüsusiyyətlər" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Yerləşdirmək" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Yerləşdirmək" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Yerli &Abunəçilərin İdarə edilməsi..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Yerli Abunəçilərin İdarə edilməsi" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Seçmə Qovluqlara əlavə etmək" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Seçmələrə əlavə etmək" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Seçmə Qovluqlardan silmək" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Seçmələrdən silmək" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Seçmələrin adını dəyişin..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Adını Dəyişmək" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Qovluğu bura kopyalayın..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Bura kopyalamaq" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Elementi bura kopyalayın..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Elementi bura köçürün..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Bura köçürmək" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Qovluğu bura köçürün..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Elementi &kəsin" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Kəsmək" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Qovluğu &kəsin" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Mənbə yaratmaq" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Mənbəni silin" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Mənbənin xüsusiyyətləri" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Mənbəyi eyniləşdirin" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Şəbəkədən kənar işləmək" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Qovluğu rekursiv &eyniləşdirmək" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Rekursiv eyniləşdirmək" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Qovluğu &səbətə atmaq" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Qovluğu səbətə atmaq" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Elementi &səbətə atmaq" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Elementi səbətə atmaq" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Qovluğu səbətdən &qaytarmaq" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Qovluğu səbətdən qaytarmaq" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Elementi &səbətdən qaytarmaq" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Elementi səbətdən qaytarmaq" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Kolleksiyanı səbətdən &qaytarmaq" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Kolleksiyanı səbətdən qaytarmaq" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Seçmə qovluqları &eyniləşdirmək" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Seçmə qovluqları eyniləşdirmək" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Qovluq ardıcıllığını eyniləşdirilmək" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Qovluğu &kopyalamaq" +msgstr[1] "%1 qovluğu &kopyalamaq" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Elementi &kopyalamaq" +msgstr[1] "%1 elementi &kopyalamaq" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Elementi &kəsin" +msgstr[1] "%1 elementi &kəsin" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Qovluğu &kəsin" +msgstr[1] "%1 qovluğu &kəsin" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Elementi &silmək" +msgstr[1] "%1 elementi &silmək" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Qovluğu silin" +msgstr[1] "%1 Qovluğu &silin" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Qovluğu &Eyniləşdirin" +msgstr[1] "%1 Qovluğu &Eyniləşdirin" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Mənbəni &silmək" +msgstr[1] "%1 Mənbələri &silmək" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Mənbəni eyniləşdirmək" +msgstr[1] "%1 Mənbəni &eyniləşdirmək" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Qovluğu kopyalamaq" +msgstr[1] "%1 qovluğu kopyalamaq" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Elementi kopyalamaq" +msgstr[1] "%1 elementi kopyalamaq" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Elementi kəsmək" +msgstr[1] "%1 elementi kəsmək" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Qovluğu kəsmək" +msgstr[1] "%1 qovluğu kəsmək" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Elementi silmək" +msgstr[1] "%1 elementi silmək" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Qovluğu silmək" +msgstr[1] "%1 Qovluğu silmək" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Qovluğu Eyniləşdirilmək" +msgstr[1] "%1 Qovluğu Eyniləşdirilmək" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Mənbəni silin" +msgstr[1] "%1 mənbəni silin" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Mənbəyi eyniləşdirin" +msgstr[1] "%1 mənbəyi eyniləşdirin" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Ad" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Siz bu qovluğu və onun bütün alt qovluqlarını silmək istəyirsiniz?" +msgstr[1] "Siz %1 qovluğu və onun bütün alt qovluqlarını silmək istəyirsiniz?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Qovluq silinsin?" +msgstr[1] "Qovluqlar silinsin?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Silinə bilməyən qovluq: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Qovluğun silinməsi alınmadı" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "%1 qovluğunun xüsusiyyətləri" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Seçilmiş elementi silmək istəyirsiniz?" +msgstr[1] "Seçilmiş %1 elementi silmək istəyirsiniz?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Element silinsin?" +msgstr[1] "Elementlər silinsin?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Silinə bilməyən element: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Elementin silinməsi alınmadı" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Seçmənin adının dəyişdirilmək" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Adı:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Yeni mənbə" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Yaradıla bilməyən mənbə: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Mənbə yaradılması alınmadı" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Bu mənbəni silmək istəyisiniz?" +msgstr[1] "%1 mənbəni silmək istəyirsiniz?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Mənbə silinsin?" +msgstr[1] "Mənbələr silinsin?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Yerləşdirilməyən verilənlər: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Yerləşdirmə alınmadı" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Qovluğun adına \"/\" əlavə edilə bilmədik." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Yeni qovluq yaradılması xətası" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Qovluğun adının əvvəlinə və ya sonuna \".\" əlavə edə bilmədik" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"\"%1\" qovluğunun eyniləşdirilməsi üçün bu mənbənin qoşulması lazımdır. Siz " +"bu qovluğu şəbəkəyə qoşmaq istəyirsiniz?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "\"%1\" hesabı şəbəkədən kənardır" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Şəbəkəyə qoşulmaq" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Bu qovluğa köçürmək" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Bu qovluğa kopyalamaq" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Yenilənməsi mümkün olmayan abunəlik: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Abunəlik xətası" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Yerli abunəliklər" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Axtarış:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Yalnız &abunə olanlar" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "A&bunə olmaq" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Abunəlikdən çıxma&q" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Yeni yarlıq yaradılması baş tutmadı" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Yeni yarlıq yaradılmasında xəta baş verdi" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Siz %1 yarlığını silmək istəyirsiniz?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Yarlığı silmək" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Yarlığı silmək" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Tətbiq edilməli yarlıqları seçin" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Yeni yarlıq yaratmaq" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Yarlıqları idarə etmək" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Yerlıqları seçin..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Yerlıqları seçin" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Təmizləmək" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Yarlıqlar əlavə etmək üçün vurun" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi-dən XML-ə çevirici" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Akonadi kolleksiyasının alt ardıcıllıqlarını XML faylına çevirir." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Verilənlər yüklənmədi" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Fayl_adı göstərilməyib" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "\"%1\" verilənlər faylı açmaq mümkün olmadı." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "\"%1\" faylı mövcud deyil." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "\"%1\" verilənlər faylını tıhlil etmək mümkün olmadı." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" +"XML verilənləri sxeminin müəyyən edilməsi yüklənə və təhlil edilə bilmir." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "XML verilənləri sxemini analiz edən kontekst yaratmaq olmur." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "XML verilənləri sxemi yaradıla bilmir." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" +"XML verilənləri sxemi düzgünlüyünü yoxlamaq üçün kontekst yaradıla bilmədi." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Səhv fayl formatı." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Təhlil edilə bilməyən verilənlər faylı: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "%1 kolleksiyası tapmaq mümkün olmadı" diff --git a/po/be/libakonadi5.po b/po/be/libakonadi5.po new file mode 100644 index 0000000..594602b --- /dev/null +++ b/po/be/libakonadi5.po @@ -0,0 +1,2617 @@ +# translation of libakonadi.po to Belarusian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Darafei Praliaskouski , 2007. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2007-07-27 15:55+0300\n" +"Last-Translator: Darafei Praliaskouski \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, fuzzy, kde-format +#| msgid "Ready" +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Гатовы" + +#: agentbase/agentbase_p.h:51 +#, fuzzy, kde-format +#| msgid "Offline" +msgctxt "@info:status" +msgid "Offline" +msgstr "Па-за сецівам" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Akonadi Resource" +msgstr "Выдаліць тэчку" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, fuzzy, kde-format +#| msgid "Name" +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Назва" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, fuzzy, kde-format +#| msgid "Name" +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Назва" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Favorite Folders" +msgstr "Новая тэчка" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:98 +#, fuzzy, kde-format +#| msgid "Total" +msgid "Quota" +msgstr "Усяго" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:228 +#, fuzzy, kde-format +#| msgid "Unread" +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Непрачытаны" + +#: core/models/statisticsproxymodel.cpp:229 +#, fuzzy, kde-format +#| msgid "Total" +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Усяго" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Delete Agent Instance" +msgstr "Выдаліць тэчку" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Выдаліць тэчку" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize when selecting this folder" +msgstr "Выдаліць тэчку" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, fuzzy, kde-format +#| msgid "New Folder..." +msgid "&New Subfolder..." +msgstr "Новая тэчка..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, fuzzy, kde-format +#| msgid "New Folder" +msgctxt "@title:window" +msgid "New Folder" +msgstr "Новая тэчка" + +#: widgets/collectiondialog.cpp:264 +#, fuzzy, kde-format +#| msgid "Name" +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Назва" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, fuzzy, kde-format +#| msgid "Name" +msgid "&Name:" +msgstr "Назва" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "folder" +msgstr "Новая тэчка" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Items" +msgstr "Выдаліць тэчку" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread" +msgid "Unread items:" +msgstr "Непрачытаны" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Reindex folder" +msgstr "Выдаліць тэчку" + +#: widgets/collectionrequester.cpp:113 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "No Folder" +msgstr "Новая тэчка" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Скасаваць" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "" + +#: widgets/dragdropmanager.cpp:218 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "&Copy Here" +msgstr "Новая тэчка" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "" + +#: widgets/dragdropmanager.cpp:228 +#, fuzzy, kde-format +#| msgid "Cancel" +msgid "C&ancel" +msgstr "Скасаваць" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "" + +#: widgets/recentcollectionaction.cpp:43 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Recent Folder" +msgstr "Выдаліць тэчку" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgid "Name" +msgid "Name:" +msgstr "Назва" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, fuzzy, kde-format +#| msgid "New Folder..." +msgid "&New Folder..." +msgstr "Новая тэчка..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Delete Folder" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Delete" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Copy Folder To..." +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Copy To" +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Move Folder To..." +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Cut Item" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "&Cut Folder" +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:150 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Create Resource" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Delete Resource" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize Resource" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:168 +#, fuzzy, kde-format +#| msgid "Offline" +msgid "Work Offline" +msgstr "Па-за сецівам" + +#: widgets/standardactionmanager.cpp:188 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Synchronize Folder Recursively" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize Recursively" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:196 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "&Move Folder To Trash" +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:197 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Move Folder To Trash" +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Synchronize Favorite Folders" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:247 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize Favorite Folders" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize Folder Tree" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:340 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Новая тэчка" +msgstr[1] "Новая тэчка" +msgstr[2] "Новая тэчка" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "New Folder" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Новая тэчка" +msgstr[1] "Новая тэчка" +msgstr[2] "Новая тэчка" + +#: widgets/standardactionmanager.cpp:344 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:347 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:348 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:350 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Новая тэчка" +msgstr[1] "Новая тэчка" +msgstr[2] "Новая тэчка" + +#: widgets/standardactionmanager.cpp:351 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:352 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:353 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Новая тэчка" +msgstr[1] "Новая тэчка" +msgstr[2] "Новая тэчка" + +#: widgets/standardactionmanager.cpp:354 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:355 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:356 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Назва" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/standardactionmanager.cpp:371 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/standardactionmanager.cpp:380 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:385 +#, fuzzy, kde-format +#| msgid "Name" +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Назва" + +#: widgets/standardactionmanager.cpp:387 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@title:window" +msgid "New Resource" +msgstr "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/standardactionmanager.cpp:396 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Выдаліць тэчку" +msgstr[1] "Выдаліць тэчку" +msgstr[2] "Выдаліць тэчку" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, fuzzy, kde-format +#| msgid "Offline" +msgctxt "@action:button" +msgid "Go Online" +msgstr "Па-за сецівам" + +#: widgets/standardactionmanager.cpp:1592 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Move to This Folder" +msgstr "Новая тэчка" + +#: widgets/standardactionmanager.cpp:1592 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Copy to This Folder" +msgstr "Новая тэчка" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@title" +msgid "Delete tag" +msgstr "Выдаліць тэчку" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@info" +msgid "Delete tag" +msgstr "Выдаліць тэчку" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Folder" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Выдаліць тэчку" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "" + +#~ msgid "Id" +#~ msgstr "Id" + +#, fuzzy +#~| msgid "Delete Folder" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Выдаліць тэчку" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Скасаваць" + +#, fuzzy +#~| msgid "Unread" +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Непрачытаны" + +#, fuzzy +#~| msgid "Total" +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Усяго" + +#, fuzzy +#~| msgid "Name" +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Назва" + +#, fuzzy +#~| msgid "New Folder..." +#~ msgid "New Folder..." +#~ msgstr "Новая тэчка..." + +#, fuzzy +#~| msgid "Cancel" +#~ msgid "Cache" +#~ msgstr "Скасаваць" + +#, fuzzy +#~| msgid "New Folder" +#~ msgid "Copy failed" +#~ msgstr "Новая тэчка" diff --git a/po/bs/akonadi_knut_resource.po b/po/bs/akonadi_knut_resource.po new file mode 100644 index 0000000..60a78f0 --- /dev/null +++ b/po/bs/akonadi_knut_resource.po @@ -0,0 +1,87 @@ +# Bosnian translation for kdepim-runtime +# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010 +# This file is distributed under the same license as the kdepim-runtime package. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: kdepim-runtime\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2015-02-04 15:48+0000\n" +"Last-Translator: Samir Ribić \n" +"Language-Team: Bosnian \n" +"Language: bs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2015-02-05 06:26+0000\n" +"X-Generator: Launchpad (build 17331)\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Datoteka s podacima nije odabrana." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Datoteka '%1' je uspješno učitana." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Izbor datoteke s podacima" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut datoteka s podacima" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nije nađena stavka za remoteid %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Roditeljska kolekcija nije nađena u DOM stablu." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Ne mogu pisati kolekciju." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Izmijenjena kolekcija nije nađena u DOM stablu." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Obrisana kolekcija nije nađena u DOM stablu." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Roditeljska kolekcija '%1' nije nađena u DOM stablu." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Ne mogu pisati stavku." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Izmijenjena stavka nije nađena u DOM stablu." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Obrisana stavka nije nađena u DOM stablu." diff --git a/po/bs/libakonadi5.po b/po/bs/libakonadi5.po new file mode 100644 index 0000000..465f8d9 --- /dev/null +++ b/po/bs/libakonadi5.po @@ -0,0 +1,2853 @@ +# translation of libakonadi.po to bosanski +# Bosnian translation for kdepimlibs +# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010 +# This file is distributed under the same license as the kdepimlibs package. +# +# FIRST AUTHOR , 2010. +# KDE 4 , 2011. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2014-01-31 20:40+0100\n" +"Last-Translator: Samir Ribić \n" +"Language-Team: bosanski \n" +"Language: bs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Launchpad (build 16807)\n" +"X-Launchpad-Export-Date: 2013-10-19 05:20+0000\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Elmir Hadzikadunic,Samir Ribić,Vedran Ljubovic" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ek@etf.ba,samir.ribic@etf.unsa.ba,vljubovic@smartnet.ba" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Ne mogu registrovati objekat na dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 tipa %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identifikator agenta" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Spreman" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Van mreže" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sinhronizujem..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Greška." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nije konfigurisano" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identifikator resursa" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Akonadi Resurs" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Pogrešan tekst preuzet" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Greška pri kreiranju elementa: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Greška pri ažuriranju kolekcije: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Neuspjelo ažuriranje lokalne zbirke: %1." + +#: agentbase/resourcebase.cpp:718 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Neuspjelo ažuriranje lokalne zbirke: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Ne mogu van veze dohvatiti stavku." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sinhronizujem fasciklu '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for sync." +msgstr "Ne mogu da dobavim zbirku resursa." + +#: agentbase/resourcebase.cpp:983 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for attribute sync." +msgstr "Ne mogu da dobavim zbirku resursa." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Zahtijevana stavka više ne postoji." + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Posao otkazan" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nema takve kolekcije." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Pronađena nerazriješena kolekcija koja ne pripada nikome." + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Nije nađena druga stavka za obradu sukoba." + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Ne mogu da pristupim d‑bus interfejsu stvorenog agenta." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Stvaranje primjerka agenta prekoračilo vreme." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Ne mogu da dobavim tip agenta „%1“." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Loš primjerak zbirke." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Loš primjerak resursa." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Ne mogu da pristupim d‑bus interfejsu za resurs „%1“." + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Isteklo vijreme za sinhronizaciju atributa zbirke." + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Loša zbirka" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Loša zbirka" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Loš roditelj" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to parse Collection from response" +msgstr "Ne mogu da dobavim zbirku resursa." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Loša zbirka" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Data pogrešna kolekcija." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nisu zadati objekti za premještanje" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nije navedeno dobro odredište" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Neispravna kolekcija." + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Loša zbirka" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Ne mogu da se povežem sa servisom Akonadija." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Nesaglasna verzija protokola na serveru Akonadija. Morate instalirati " +"saglasnu." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Korisnik je otkazao operaciju." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Nepoznata greška." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Sinhronizacija resursa prekoračila vrijeme." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Ne mogu da dobavim korjenu zbirku resursa %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "ID resursa nije nađen." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Loš identifikator resursa %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Ne mogu da podesim podrazumevani resurs preko d‑busa." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Ne mogu da dobavim zbirku resursa." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Prekovrijeme pri pokušaju zaključavanja." + +#: core/jobs/tagcreatejob.cpp:49 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create tag." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Pomjeranje u kolekciju smeća nije uspjelo, prekida se operacija smeća" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Proslijeđene neispravne stavke" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Proslijeđena neispravna kolekcija" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Nema važeće kolekcije ili je prazna lista stavki" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Ne mogu naći kolekciju za obnavljanje a resurs za obnavljanje nije dostupan" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Naziv" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Učitavam..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Greška." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Odredišna kolekcija '%1' već sadrži\n" +"kolekciju s imenom '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Naziv" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Nije moguće kopirati stavku:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Nije moguće otvoriti kolekciju:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Nije moguće premjestiti stavku:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Nije moguće premjestiti kolekciju" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Nije moguće linkovati subjekat:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Greška." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Omiljeni direktoriji" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Ukupno poruka" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Nepročitane poruke" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Skladišna veličina" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Veličina spremnika poddirektorija" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Nepročitano" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Ukupno" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Veličina" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Ne mogu dohvatiti predmet za index" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index više nije dostupan" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Sadržajni dio „%1“ nije dostupan za ovaj indeks" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Nema dostupne sesije za dati index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Nema dostupne stavke za ovaj index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Bezimeni dodatak" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Nema dostupnog opisa" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "Samoproba servera Akonadija" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "Ne mogu da se povežem sa servisom Akonadija." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Novi primjerak agenta..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Obriši primjerak agenta" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Podesi primjerak agenta" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Novi primjerak agenta" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Ne mogu da napravim primjerak agenta: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Neuspjelo stvaranje primjerka agenta" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Obrisati primjerak agenta?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Želite li zaista da obrišete izabrani primjerak agenta?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuta" +msgstr[1] "minute" +msgstr[2] "minuta" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Dobavljanje" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Koristi opcije roditeljskog direktorija ili naloga" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sinhronizuj direktorij kada se izabere" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatski sinhronizuj nakon:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nikad" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuta" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokalno Spremljeni dijelovi" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opcije dobavljanja" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "Uvijek dobavljaj pune poruke" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "Dobavljaj tijela poruka na zahtjev" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Drži tijela poruka lokalno:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Zauvijek" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "" +#| "@info/plain Displayed grayed-out inside the textbox, verb to search" +#| msgid "Search" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Traži" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "U normalnoj situaciji koristi fasciklu" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Novi poddirektorij..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Napravi novi poddirektorij unutar trenutno izabranog direktorija" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Novi direkorij" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Naziv" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Ne mogu da napravim direktorij: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Neuspjelo stvaranje direktorija" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Opšte" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 objekt" +msgstr[1] "%1 objekta" +msgstr[2] "%1 objekata" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Naziv:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Koristi prilagođenu ikonu:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "direktorij" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistike" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Sadržaj:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objekata" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Veličina:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 bajta" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "Greška pri kreiranju elementa: %1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "&Svojstva direktorija" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "Isijeci %1 stavku" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Ukupno poruka" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Nepročitane poruke" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "Nedavna fascikla" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Nema direktorija" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Otvori prozor sa kolekcijom" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Izbor zbirke" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Pomjeri ovdje" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopiraj ovdje" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Odustani" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Vrijeme izmjene" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Indikatori" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Razrješenje konflikta" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "Uzmi desno" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Zadrži oba" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Podaci" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Pokretanje Akonadi servera..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Zaustavljanje Akonadi servera..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Premjesti ovdje" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopirajte ovdje" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Poveži ovdje" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Odustani" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Ne mogu da se povežem sa servisom Akonadija." + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Servis za upravljanje osobnim podacima je startao sa radom..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Servis za upravljanje osobnim podacima se gasi..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Servis za upravljanje osobnim podacima ima nadogradnju baze podataka..." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Softver za upravljanje ličnim informacijama obavlja nadogradnju baze " +"podataka.\n" +"To se dešava ankon nadogradnje softvera i potrebno je radi optimizacije " +"performansi.\n" +"Zavisno od obima ličnih informacija to može trajati nekoliko minuta." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi, servis za upravljanje ličnim podacima, nije u pogonu. Ovaj program " +"se ne može koristiti bez njega." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Početak" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi, radni okvir za upravljanje ličnim podacima, nije operativan.\n" +"Kliknite na Detalji... za detaljne informacije o ovom problemu." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi, servis za upravljanje ličnim podacima, nije operativan." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalji..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "Početak" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Nedavna fascikla" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Preimenuj omiljenu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Naziv:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Samoproba servera Akonadija" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Snimi izvještaj..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopiraj izveštaj u klipbord" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Tekuća postava servera Akonadija zahtijeva drajver QtSQL‑a „%1“; nađen je na " +"sistemu." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Tekuća postava servera Akonadija zahtijeva drajver QTSQL-a „%1“.\n" +"Instalirani su sljedeći drajveri: %2.\n" +"Pobrinite se da i zahtijevani bude instaliran." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Drajver baze podataka nađen." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Drajver baze podataka nije nađen." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Izvršni fajl servera MySQL nije isproban." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Trenutna postava ne zahtijeva unutrašnji server MySQL-a." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Trenutno ste podesili Akonadi da koristi server MySQL-a „%1“.\n" +"Pobrinite se da je zaista instaliran, postavite ispravno putanju i proverite " +"da li imate neophodna prava čitanja i izvršavanja za izvršnu datoteku " +"servera. Obično se zove mysqld, a tačna lokacija zavisi " +"od distribucije." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL server nije pronađen." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL server nije čitljiv." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL server nije izvršiv." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL je pronađen pod neočekivanim nazivom." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL server je pronađen." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL server je pronađen: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL server je moguće izvršiti." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "Izvršavanje servera MySQL‑a „%1“ propalo, sa sljedećom greškom: „%2“" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Izvršavanje servera MySQL‑nije uspjelo." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Dnevnik grešaka servera MySQL nije isproban." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Nema tekućeg dnevnika grešaka MySQL-a" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Server MySQL‑a nije prijavio nijednu grešku tokom ovog pokretanja. Dnevnik " +"se nalazi u %1." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Dnevnik grešaka MySQL‑a nije čitljiv." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Datoteka dnevnika grešaka servera MySQL je nađena, ali nije čitljiva: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Dnevnik servera MySQL‑a sadrži greške." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" +"Datoteka dnevnika grešaka servera MySQL %1 sadrži " +"greške." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Dnevnik servera MySQL sadrži upozorenja." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Datoteka dnevnika servera MySQL %1 sadrži upozorenja." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Dnevnik servera MySQL ne sadrži greške." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "Datoteka dnevnika servera MySQL %1 ne sadrži ni greške ni upozorenja." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Postavka servera MySQL nije isprobana." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Podrazumijevana postavka servera MySQL nađena." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Podrazumijevana postavka za server MySQL nađena je i čitljiva kod %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Podrazumevana postavka servera MySQL nije nađena." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Podrazumevana postavka za server MySQL nije nađena ili nije čitljiva. " +"Provjerite da li je instalacija Akonadija potpuna, i da li imate neophodna " +"prava pristupa." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Posebna postavka servera MySQL nije dostupna." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "Posebna postavka za server MySQL nije nađena, ali je opciona." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Posebna postavka servera MySQL nađena." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Posebna postava za server MySQL nađena je i čitljiva kod %1." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Posebna postava servera MySQL nije čitljiva." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Posebna postava za server MySQL nađena je kod %1, ali nije čitljiva. " +"Provjerite prava pristupa." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Postava servera MySQL nije nađena ili nije čitljiva." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Postavka za server MySQL ili nije nađena ili nije čitljiva." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Postava servera MySQL je upotrebljiva." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Postavka servera MySQL nađena je kod %1 i čitljiva je." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Ne mogu da se povežem sa serverom PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Server PostgreSQL nađen." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Server PostgreSQL je nađen i veza radi." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl nije nađena" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Naredba akonadictl mora biti dostupna u putanji. Proverite da li je server " +"Akonadija instaliran." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl nađena i upotrebljiva" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Naredba %1, za upravljanje serverom Akonadija, nađena je i uspješno " +"izvršena.\n" +"Rezultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl nađena ali neupotrebljiva" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Naredba %1, za upravljanje serverom Akonadija, nađena je ali nije mogla biti " +"uspješno izvršena.\n" +"Rezultat:\n" +"%2\n" +"Proverite da li je server Akonadija ispravno instaliran." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Upravljački proces Akonadija registrovan na d‑busu." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Upravljački proces Akonadija registrovan je na d‑busu, što obično znači da " +"je operativan." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Upravljački proces Akonadija nije registrovan na d‑busu." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Upravljački proces Akonadija nije registrovan na d‑busu, što obično znači " +"ili da nije pokrenut, ili da je na pokretanju došlo do kobne greške." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Serverski proces Akonadija registrovan na d‑busu" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Serverski proces Akonadija registrovan je na d‑busu, što obično znači da je " +"operativan." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Serverski proces Akonadija nije registrovan na d‑busu." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Serverski proces Akonadija nije registrovan na d‑busu, što obično znači ili " +"da nije pokrenut, ili da je na pokretanju došlo do kobne greške." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Nije moguće proveriti verziju protokola." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Bez veze sa serverom nije moguće proveriti da li verzija protokola ispunjava " +"zahteve." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Verzija protokola servera previše stara." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Verzija protokola servera je %1, a neophodna je bar %2. Instalirajte noviju " +"verziju servera Akonadija." + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "Verzija protokola servera previše stara." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "Verzija protokola servera previše stara." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "Verzija protokola servera previše stara." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Agenti resursa nađeni." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Nađen je bar jedan agent resursa." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Agenti resursa nisu nađeni." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nijedan agent resursa nije nađen, a Akonadi se ne može koristiti bez bar " +"jednog. Ovo obično znači ili da agenti resursa nisu instalirani ili da " +"postoji problem u postavi. Pretražene su sledeće putanje: %1. " +"Promenljiva okruženja XDG_DATA_DIRS postavljena je na " +"%2, proverite uključuje li ovo sve putanje gde su instalirani agenti " +"Akonadija." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Tekući dnevnik grešaka servera Akonadija nije nađen." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Server Akonadija nije prijavio nijednu grešku tokom tekućeg pokretanja." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Tekući dnevnik grešaka servera Akonadija nađen." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Server Akonadija prijavio je greške tokom tekućeg pokretanja. Dnevnik se " +"nalazi u %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Prethodni dnevnik grešaka servera Akonadija nađen." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Server Akonadija nije prijavio nijednu grešku tokom prethodnog pokretanja." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Prethodni dnevnik grešaka servera Akonadija nađen." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Server Akonadija prijavio je greške pri prethodnom pokretanju. Dnevnik se " +"nalazi u %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Tekući dnevnik grešaka upravljanja Akonadija nije nađen." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Upravljački proces Akonadija nije prijavio nijednu grešku pri tekućem " +"pokretanju." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Tekući dnevnik grešaka upravljanja Akonadija nađen." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Upravljački proces Akonadija prijavio je greške tokom tekućeg pokretanja. " +"Dnevnik se nalazi u %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Prethodni dnevnik grešaka upravljanja Akonadija nađen." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Upravljački proces Akonadija nije prijavio nijednu grešku tokom prethodnog " +"pokretanja." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Prethodni dnevnik grešaka upravljanja Akonadija nađen." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Upravljački proces Akonadija prijavio je greške tokom prethodnog pokretanja. " +"Dnevnik se nalazi u %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi pokrenut kao root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Izvršavanje programa okrenutih Internetu kao root (administratorskim " +"nalogom) izlaže vas mnogim bezbjednosnim rizicima. MySQL, koji koristi ova " +"instalacija Akonadija, neće dopustiti izvršavanje pod korenom da bi vas " +"zaštitio od ovih rizika." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi ne radi kao root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi ne radi pod root (administratorskim) korisnikom, što je i " +"preporučena postava za bezbjednost sistema." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Upisivanje izveštaja o probama" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Greška." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Ne mogu da otvorim datoteku %1" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Došlo je do greške pri pokretanju servera Akonadija. Sljedeće samoprobe " +"trebalo bi da pomognu u otkrivanju i rešavanju ovog problema. Kada tražite " +"podršku ili prijavljujete greške, molimo vas da uvek uključite ovaj izveštaj." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalji" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Još savjeta za rješavanje problema potražite na userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Novi direktorij..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nova" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Obriši %1 direktorij" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Obriši" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Sinhronizuj %1 direktorij" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sinhronizuj" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Svojstva direktorija" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Svojstva" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Umetni" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Umetni" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Uredi lokalne &pretplate..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Upravljaj lokalnim pretplatama" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Dodaj u omiljeni direktorij" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Dodaj u omiljene" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Ukloni iz omiljenih direktorija" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Ukloni iz omiljenih" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Preimenuj omiljenu..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Preimenuj" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopiraj direktorij u..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopiraj u" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopiraj stavku u..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Premjesti stavku u..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Premjesti u" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Premesti direktorij u..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Isijeci %1 stavku" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Isijeci" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Isijeci %1 direktorij" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Napravi resurs" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Obriši %1 resurs" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Svojstva &resursa" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Sinhronizuj %1 resurs" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Rad van mreže" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sinhronizuj direktorij rekurzivno" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sinhronizuj rekurzivno" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Pre&mjesti fasciklu u smeće" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Premjesti fasciklu u smeće" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Pre&mjesti stavku u smeće" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Premjesti stavku u smeće" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "V&rati fasciklu iz smeća" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Vrati fasciklu iz smeća" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "V&rati stavku iz smeća" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Vrati stavku iz smeća" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "V&rati kolekciju iz smeća" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Vrati kolekciju iz smeća" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sinhroniziraj omiljene fascikle" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sinhronizuj omiljene fascikle" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize Folder Tree" +msgstr "Sinhronizuj %1 direktorij" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopiraj %1 direktorij" +msgstr[1] "&Kopiraj %1 direktorija" +msgstr[2] "&Kopiraj %1 direktorija" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopiraj %1 stavku" +msgstr[1] "&Kopiraj %1 stavke" +msgstr[2] "&Kopiraj %1 stavki" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Isijeci %1 stavku" +msgstr[1] "&Isijeci %1 stavke" +msgstr[2] "&Isijeci %1 stavki" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Isijeci %1 direktorij" +msgstr[1] "&Isijeci %1 direktorija" +msgstr[2] "&Isijeci %1 direktorij" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Obriši %1 stavku" +msgstr[1] "&Obriši %1 stavke" +msgstr[2] "&Obriši %1 stavki" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Obriši %1 direktorij" +msgstr[1] "&Obriši %1 direktorija" +msgstr[2] "&Obriši %1 direktorija" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sinhronizuj %1 direktorij" +msgstr[1] "&Sinhronizuj %1 direktorija" +msgstr[2] "&Sinhronizuj %1 direktorija" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Obriši %1 resurs" +msgstr[1] "&Obriši %1 resursa" +msgstr[2] "&Obriši %1 resursa" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sinhronizuj %1 resurs" +msgstr[1] "&Sinhronizuj %1 resursa" +msgstr[2] "&Sinhronizuj %1 resursa" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopiraj %1 direkorij" +msgstr[1] "Kopiraj %1 direktorija" +msgstr[2] "Kopiraj %1 direktorija" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopiraj %1 stavku" +msgstr[1] "Kopiraj %1 stavke" +msgstr[2] "Kopiraj %1 stavki" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Isijeci %1 stavku" +msgstr[1] "Isijeci %1 stavke" +msgstr[2] "Isijeci %1 stavki" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Isijeci %1 direkorij" +msgstr[1] "Isijeci %1 direktorija" +msgstr[2] "Isijeci %1 direktorija" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Obriši %1 stavku" +msgstr[1] "Obriši %1 stavke" +msgstr[2] "Obriši %1 stavki" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Obriši %1 direktorij" +msgstr[1] "Obriši %1 direktorija" +msgstr[2] "Obriši %1 direktorija" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sinhronizuj %1 direktorij" +msgstr[1] "Sinhronizuj %1 direktorija" +msgstr[2] "Sinhronizuj %1 direktorij" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Obriši %1 resurs" +msgstr[1] "Obriši %1 resursa" +msgstr[2] "Obriši %1 resursa" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sinhronizuj %1 resurs" +msgstr[1] "Sinhronizuj %1 resursa" +msgstr[2] "Sinhronizuj %1 resursa" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Naziv" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Želite li zaista da obrišete %1 direktorij i sve njihove poddirektorije?" +msgstr[1] "" +"Želite li zaista da obrišete %1 direktorija i sve njihove poddirektorije?" +msgstr[2] "" +"Želite li zaista da obrišete %1 direktorija i sve njihove poddirektorije?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Obrisati direktorij?" +msgstr[1] "Obrisati direktorije?" +msgstr[2] "Obrisati direktorije?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Ne mogu da obrišem direktorij: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Neuspjelo brisanje direktorija" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Svojstva direktorija %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Želite li zaista da obrišete %1 izabranu stavku?" +msgstr[1] "Želite li zaista da obrišete %1 izabrane stavke?" +msgstr[2] "Želite li zaista da obrišete %1 izabranih stavki?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Obrisati stavke?" +msgstr[1] "Obrisati stavke?" +msgstr[2] "Obrisati stavke?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Ne mogu da obrišem stavku: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Neuspjelo brisanje stavke" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Preimenuj omiljenu" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Naziv:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Novi resurs" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Ne mogu da napravim resurs: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Neuspjelo stvaranje resursa" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Želite li zaista da obrišete %1 resurs?" +msgstr[1] "Želite li zaista da obrišete %1 resursa?" +msgstr[2] "Želite li zaista da obrišete %1 resursa?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Obrisati resurse?" +msgstr[1] "Obrisati resurse?" +msgstr[2] "Obrisati resurse?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Ne mogu da umetnem podatke: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Neuspjelo umetanje" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Ne može se dodati \"/\" u ime fascikle." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Greška u kreiranju nove fascikle" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Ne može se dodati \".\" na početku ili kraju imena direktorija." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Prije sinhronizacije direktorija \"%1\" potrebno je imati resurs na mreži. " +"Želite li ga napraviti mrežnim?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Nalog \"%1\" nije na mreži" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Radi van mreže" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Prebaci u ovaj direktorij" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopiraj u ovaj direktorij" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to update subscription: %1" +msgstr "Ne mogu da stvorim primjerak agenta." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Lokalne pretplate" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Lokalne pretplate" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Traži:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Samo pretplaćeni" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Pretplati" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Otkaži pretplatu" + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create a new tag" +msgstr "Ne mogu da stvorim primjerak agenta." + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "An error occurred while creating a new tag" +msgstr "Greška pri kreiranju elementa: %1" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete this resource?" +#| msgid_plural "Do you really want to delete %1 resources?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Želite li zaista da obrišete %1 resurs?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "Obriši %1 stavku" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "Obriši %1 stavku" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Create new tag" +msgstr "Ne mogu da stvorim primjerak agenta." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Obriši %1 stavku" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi u XML konverter" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Konvertuje Akonadi kolekciju podstabala u XML datoteku" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nema učitanih podataka" + +#: xml/xmldocument.cpp:123 +#, fuzzy, kde-format +#| msgid "No valid destination specified" +msgid "No filename specified" +msgstr "Nije navedeno dobro odredište" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to open data file '%1'." +msgstr "Ne mogu da dobavim tip agenta „%1“." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Datoteka %1 ne postoji." + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to parse data file '%1'." +msgstr "Ne mogu da dobavim tip agenta „%1“." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Definicija sheme ne može biti učitana i raščlanjena" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "Ne mogu da stvorim primjerak agenta." + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Invalid item retrieved" +msgid "Invalid file format." +msgstr "Pogrešan tekst preuzet" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "Ne mogu da umetnem podatke: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "Loša zbirka" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Daljinski Id" + +#~ msgid "MimeType" +#~ msgstr "MIME tip" + +#~ msgid "Default Name" +#~ msgstr "Podrazumijevano ime" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Obriši" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Odustani" + +#~ msgid "Take left one" +#~ msgstr "Uzmi lijevo" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Dve dopune međusobno su sukobljene.Izaberite koju ćete primijeniti." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Nepročitano" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Ukupno" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Veličina" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi Resurs" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Naziv" + +#~ msgid "Invalid collection specified" +#~ msgstr "Pogrešna kolekcija zadana" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Nađena verzija protokola %1, očekivana bar %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Verzija protokola servera dovoljno je nova." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Verzija protokola servera je %1, što je jednako ili novije od zahtevane " +#~ "%2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Otkriveno je neusaglašeno lokalno stablo zbirke." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Data je udaljena zbirka bez korijenom okončanog lanca predaka, resurs je " +#~ "iskvaren." + +#~ msgid "KDE Test Program" +#~ msgstr "Probni KDE program" + +#~ msgid "Cannot list root collection." +#~ msgstr "Nije moguće izlistati korijensku kolekciju." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Servis pretrage Nepomuka registrovan na d‑busu" + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Servis pretrage Nepomuka registrovan je na d‑busu, što obično znači da je " +#~ "operativan." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Servis pretrage Nepomuka nije registrovan na d‑busu." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Servis pretrage Nepomuka nije registrovan na d‑busu, što obično znači ili " +#~ "da nije pokrenut, ili da je na pokretanju došlo do kobne greške." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Сервис претраге Непомука користи неодговарајућу позадину." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Servis pretrage Nepomuka koristi pozadinu „%1“, koja nije preporučljiva " +#~ "za potrebe Akonadija." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Servis pretrage Nepomuka koristi odgovarajuću pozadinu. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Servis pretrage Nepomuka koristi jednu od preporučenih pozadina." + +#~ msgid "" +#~ "Personal information management service is performing a database upgrade. " +#~ "This happens after a software update and is necessary to optimize " +#~ "performance. Depending on the amount of personal information, this might " +#~ "take a few minutes." +#~ msgstr "" +#~ "Softver za upravljanje ličnim informacijama obavlja nadogradnju baze " +#~ "podataka. To se dešava ankon nadogradnje softvera i potrebno je radi " +#~ "optimizacije performansi. Zavisno od obima ličnih informacija to može " +#~ "trajati nekoliko minuta." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Priključak \"%1\" nije sagrađen statički. Molim navedite ovu " +#~ "informaciju u prijavi greške." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Priključak nije statički sagrađen" diff --git a/po/ca/akonadi_knut_resource.po b/po/ca/akonadi_knut_resource.po new file mode 100644 index 0000000..4f6e63b --- /dev/null +++ b/po/ca/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# Translation of akonadi_knut_resource.po to Catalan +# Copyright (C) 2009-2014 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Manuel Tortosa Moreno , 2009, 2010. +# Josep Ma. Ferrer , 2010. +# Manuel Tortosa , 2010. +# Antoni Bella Pérez , 2014. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2014-03-04 19:43+0100\n" +"Last-Translator: Antoni Bella Pérez \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.5\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "No s'ha seleccionat cap fitxer de dades." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "El fitxer «%1» s'ha carregat correctament." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Seleccioneu un fitxer de dades" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Fitxer de dades Knut de l'Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "No s'ha trobat cap element per a l'ID remot %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "No s'ha trobat la col·lecció pare a l'arbre DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "No s'ha pogut escriure la col·lecció." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "No s'ha trobat la col·lecció modificada a l'arbre DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "No s'ha trobat la col·lecció eliminada a l'arbre DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "No s'ha trobat la col·lecció pare «%1» a l'arbre DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "No s'ha pogut escriure l'element." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "No s'ha trobat l'element modificat a l'arbre DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "No s'ha trobat l'element eliminat a l'arbre DOM." diff --git a/po/ca/libakonadi5.po b/po/ca/libakonadi5.po new file mode 100644 index 0000000..e979fb5 --- /dev/null +++ b/po/ca/libakonadi5.po @@ -0,0 +1,2622 @@ +# Translation of libakonadi5.po to Catalan +# Copyright (C) 2007-2021 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Josep Ma. Ferrer , 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020. +# Manuel Tortosa Moreno , 2009, 2010. +# Antoni Bella Pérez , 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 13:16+0100\n" +"Last-Translator: Antoni Bella Pérez \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.12.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Josep Ma. Ferrer,Antoni Bella" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "txemaq@gmail.com,antonibella5@yahoo.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Actualment no hi ha cap compte configurat." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "La integració dels comptes no està admesa" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "No s'ha pogut registrar l'objecte en el «dbus»: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 del tipus %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificador de l'agent" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agent de l'Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Preparat" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Desconnectat" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "S'està sincronitzant..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Error." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Sense configurar" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificador del recurs" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Recurs de l'Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "S'ha recuperat un element no vàlid" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Error en crear l'element: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Error en actualitzar la col·lecció: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Ha fallat l'actualització de la col·lecció local: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Ha fallat l'actualització dels elements locals: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "No es pot recuperar l'element en el mode desconnectat." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "S'està sincronitzant la carpeta «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Ha fallat en recuperar la col·lecció per a sincronitzar-la." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Ha fallat en recuperar la col·lecció per a la sincronització dels atributs." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "L'element sol·licitat ja no existeix" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Treball cancel·lat." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Aquesta col·lecció no existeix." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "S'han trobat col·leccions òrfenes sense resoldre" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "No s'ha trobat cap element per a gestionar el conflicte" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "No s'ha pogut accedir a la interfície de D-Bus de l'agent creat." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "La creació de la instància de l'agent ha excedit el temps." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "No s'ha pogut obtenir el tipus de l'agent «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "No s'ha pogut crear la instància de l'agent." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Instància de la col·lecció no vàlida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Instància del recurs no vàlida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "No s'ha pogut obtenir la interfície de D-Bus per al recurs «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "La sincronització dels atributs de la col·lecció ha excedit el temps." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Col·lecció no vàlida per a copiar" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Destinació de la col·lecció no vàlida" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Pare no vàlid" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Ha fallat en analitzar la col·lecció de la resposta." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Col·lecció no vàlida" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Col·lecció proporcionada no vàlida." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "No s'ha especificat cap objecte per a moure" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "No s'ha especificat cap destinació vàlida" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Col·lecció no vàlida." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Col·lecció pare no vàlida" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "No s'ha pogut connectar amb el servei Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"La versió del protocol del servidor Akonadi és incompatible. Comproveu que " +"teniu instal·lada una versió compatible." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operació cancel·lada per l'usuari." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Error desconegut." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Resposta inesperada" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Ha fallat en crear la relació." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "La sincronització del recurs ha excedit el temps." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "No s'ha pogut recuperar la col·lecció arrel del recurs %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "No s'ha proporcionat l'ID del recurs." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identificador del recurs «%1» no vàlid" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Ha fallat en configurar el recurs per omissió via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Ha fallat en recuperar la col·lecció de recursos." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Ha caducat el temps en provar d'obtenir el bloqueig." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Ha fallat en crear l'etiqueta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Ha fallat en moure la col·lecció a la paperera, s'ha interromput l'operació." + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "S'han passat elements no vàlids" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "S'ha passat una col·lecció no vàlida" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Col·lecció no vàlida o llista d'elements buida" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"No s'ha pogut trobar la col·lecció a restaurar i el recurs de restauració no " +"està disponible" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nom" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "S'està carregant..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Error" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"La col·lecció de destinació «%1» ja conté\n" +"una col·lecció amb el nom «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nom" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "No s'ha pogut copiar l'element: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "No s'ha pogut copiar la col·lecció: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "No s'ha pogut moure l'element: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "No s'ha pogut moure la col·lecció: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "No s'ha pogut enllaçar l'entitat: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Error" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Carpetes preferides" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Total de missatges" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Missatges sense llegir" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Mida d'emmagatzematge" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Mida d'emmagatzematge de la subcarpeta" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Sense llegir" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Mida" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiqueta" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "No s'ha pogut recuperar l'element per a l'índex" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "L'índex ja no està disponible" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "La part de contingut «%1» no està disponible per a aquest índex" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "No hi ha disponible cap sessió per a aquest índex" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "No hi ha disponible cap element per a aquest índex" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Connector sense nom" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "No hi ha disponible cap descripció" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"La versió del protocol del servidor Akonadi difereix de la versió del " +"protocol emprat per aquesta aplicació.\n" +"Si heu actualitzat recentment el sistema, si us plau, sortiu i torneu a " +"connectar per a assegurar-vos que totes les aplicacions empren la versió " +"correcta del protocol." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"No hi ha disponible cap agent de l'Akonadi. Si us plau, verifiqueu la vostra " +"instal·lació PIM del KDE." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Les versions del protocol no coincideixen. La versió del servidor és més " +"antiga (%1) que la nostra (%2). Si heu actualitzat el vostre sistema " +"recentment, reinicieu el servidor Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Les versions del protocol no coincideixen. La versió del servidor és més " +"nova (%1) que la nostra (%2). Si heu actualitzat el vostre sistema " +"recentment, reinicieu totes les aplicacions PIM del KDE." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Autocomprovació de l'Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Comprova i informa sobre l'estat del servidor Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Instància &nova de l'agent..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Su&primeix la instància de l'agent" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configura la instància de l'agent" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Instància nova de l'agent" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "No s'ha pogut crear la instància de l'agent: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "La creació de la instància de l'agent ha fallat" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Suprimeixo la instància de l'agent?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Realment voleu suprimir la instància seleccionada de l'agent?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuració de %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manual del %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Quant al %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "El diàleg de configuració s'ha obert en una altra finestra" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "La configuració per a %1 ja es troba oberta en un altre lloc." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Ha fallat en registrar el diàleg de configuració %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minut" +msgstr[1] "minuts" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Recuperació" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usa les opcions de la carpeta pare o del compte" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronitza en seleccionar aquesta carpeta" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronitza automàticament després de:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Mai" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuts" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Parts en memòria cau localment" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opcions de recuperació" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Recupera sempre els &missatges complets" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Recupera els cossos dels missatges a petició" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Mantén localment els cossos dels missatges durant:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Per a sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Cerca" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usa la carpeta per omissió" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Subcarpeta &nova..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Crea una subcarpeta nova sota la carpeta seleccionada" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Carpeta nova" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nom" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "No s'ha pogut crear la carpeta: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Ha fallat la creació de la carpeta" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "General" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un objecte" +msgstr[1] "%1 objectes" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nom:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usa una icona personalitzada:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "carpeta" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estadístiques" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contingut:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objectes" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Mida:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bytes" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Recordeu que la indexació pot trigar uns minuts." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Manteniment" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Error en recuperar el compte dels elements indexats" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "S'ha indexat %1 element en aquesta carpeta" +msgstr[1] "S'han indexat %1 elements en aquesta carpeta" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "S'estan calculant els elements indexats..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fitxers" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipus de carpeta:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "desconegut" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elements" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elements totals:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Elements sense llegir:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexació" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Activa la indexació de text complet" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "S'està recuperant el compte dels elements indexats..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Torna a indexar la carpeta" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Cap carpeta" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Diàleg d'obertura de col·lecció" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Selecció d'una col·lecció" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mou aquí" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copia aquí" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancel·la" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Hora de modificació" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Indicadors" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolució de conflictes" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Agafa la meva versió" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Agafa la seva versió" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mantén ambdues versions" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Els vostres canvis entren en conflicte amb els d'algú altre.
A menys " +"que es pugui llençar alguna versió, els haureu d'integrar manualment.
Feu " +"clic a «Obre l'editor de text» per a mantenir " +"una còpia dels textos, seleccioneu la versió més correcta, torneu-la a obrir " +"i modifiqueu-la de nou per a afegir-hi el que falta." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dades" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "S'està iniciant el servidor Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "S'està aturant el servidor Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mou aquí" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copia aquí" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Enllaça aquí" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancel·la" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"No s'ha pogut connectar amb el servei per a la gestió de la informació " +"personal.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "S'està iniciant el servei per a la gestió de la informació personal..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "S'està aturant el servei per a la gestió de la informació personal..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"El servei per a la gestió de la informació personal està realitzant una " +"actualització de la base de dades..." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"El servei per a la gestió de la informació personal està efectuant una " +"actualització de la base de dades. Això succeeix després d'una actualització " +"del programari i, és necessari per a optimitzar el rendiment.\n" +"En funció de la quantitat d'informació personal, això pot trigar alguns " +"minuts." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"El servei per a la gestió de la informació personal Akonadi no es troba en " +"execució. Aquesta aplicació no es pot usar sense ell." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Inicia" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"El marc de treball per a la gestió de la informació personal Akonadi no es " +"troba operatiu.\n" +"Feu clic a «Detalls...» per a obtenir informació detallada quant a aquest " +"problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"El servei per a la gestió de la informació personal Akonadi no es troba " +"operatiu." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalls..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Voleu eliminar el compte «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Elimino el compte?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Comptes d'entrada (afegiu-ne un com a mínim):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&fegeix..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modifica..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Elimina" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reinicia" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Carpeta recent" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Reanomena una preferida" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nom:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Autocomprovació del servidor Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Desa l'informe..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copia l'informe al porta-retalls" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"La configuració actual del servidor Akonadi requereix el controlador «%1» de " +"QtSQL i s'ha trobat en el sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"La configuració actual del servidor Akonadi requereix el controlador «%1» de " +"QtSQL.\n" +"Estan instal·lats els següents controladors: %2.\n" +"Comproveu que el controlador requerit estigui instal·lat." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "S'ha trobat el controlador de la base de dades." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "No s'ha trobat el controlador de la base de dades." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "No s'ha provat l'executable del servidor MySQL." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "La configuració actual no requereix cap servidor intern de MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Actualment heu configurat l'Akonadi per a usar el servidor «%1» de MySQL.\n" +"Comproveu que teniu el servidor MySQL instal·lat, definit el camí correcte i " +"assegureu-vos que teniu els permisos de lectura i execució necessaris sobre " +"l'executable del servidor. L'executable del servidor normalment s'anomena " +"«mysqld», la seva ubicació varia en funció de la distribució." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "No s'ha trobat cap servidor MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "No es pot llegir des del servidor MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "El servidor MySQL no es pot executar." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "S'ha trobat el MySQL amb un nom inesperat." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "S'ha trobat el servidor MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "S'ha trobat el servidor MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "El servidor MySQL és executable." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Ha fallat en executar el servidor «%1» de MySQL amb el missatge d'error " +"següent: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Ha fallat en executar el servidor MySQL." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "No s'ha provat el registre d'error del servidor MySQL." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "No s'ha trobat cap registre d'error actual del MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"El servidor MySQL no ha informat de cap error durant aquest inici. El " +"registre es pot trobar a «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "El registre d'error del MySQL no es pot llegir." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"S'ha trobat un registre d'error del servidor MySQL però no es pot llegir: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "El registre del servidor MySQL conté errors." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "El fitxer de registre d'errors «%1» del servidor MySQL conté errors." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "El registre del servidor MySQL conté avisos." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "El fitxer de registre d'errors «%1» del servidor MySQL conté avisos." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "El registre del servidor MySQL no conté errors." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"El fitxer de registre d'errors «%1» del servidor MySQL no conté cap error ni " +"avís." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "La configuració del servidor MySQL no s'ha provat." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "S'ha trobat la configuració per omissió del servidor MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"S'ha trobat la configuració per omissió del servidor MySQL i es pot llegir a " +"%1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "No s'ha trobat la configuració per omissió del servidor MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"No s'ha trobat la configuració per omissió del servidor MySQL o no s'ha " +"pogut llegir. Comproveu que la instal·lació de l'Akonadi ha acabat i que " +"teniu els permisos d'accés requerits." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "La configuració personalitzada del servidor MySQL no està disponible." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"No s'ha trobat la configuració personalitzada del servidor MySQL, però és " +"opcional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "S'ha trobat la configuració personalitzada del servidor MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"S'ha trobat la configuració personalitzada del servidor MySQL i es pot " +"llegir a %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "La configuració personalitzada del servidor MySQL no es pot llegir." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"S'ha trobat la configuració personalitzada del servidor MySQL a %1, però no " +"es pot llegir. Comproveu els vostres permisos d'accés." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "No s'ha trobat la configuració del servidor MySQL o no es pot llegir." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "No s'ha trobat la configuració del servidor MySQL o no es pot llegir." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "La configuració del servidor MySQL es pot usar." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "S'ha trobat la configuració del servidor MySQL a %1 i es pot llegir." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "No s'ha pogut connectar amb el servidor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "S'ha trobat el servidor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "S'ha trobat el servidor PostgreSQL i la connexió funciona." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "No s'ha trobat l'«akonadictl»" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"El programa «akonadictl» cal que estigui accessible a la $PATH. Comproveu " +"que teniu instal·lat el servidor Akonadi." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "S'ha trobat l'«akonadictl» i es pot usar" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"S'ha trobat el programa «%1» per a controlar el servidor Akonadi i s'ha " +"pogut executar correctament.\n" +"Resultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "S'ha trobat l'«akonadictl» però no es pot usar" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"S'ha trobat el programa «%1» per a controlar el servidor Akonadi, però no " +"s'ha pogut executar correctament.\n" +"Resultat:\n" +"%2\n" +"Comproveu que el servidor Akonadi estigui correctament instal·lat." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "El procés de control de l'Akonadi s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"El procés de control de l'Akonadi s'ha registrat al D-Bus, el qual " +"normalment indica que es troba operatiu." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "El procés de control de l'Akonadi no s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"El procés de control de l'Akonadi no s'ha registrat al D-Bus, el qual " +"normalment vol dir que no s'ha iniciat o que ha trobat un error fatal durant " +"l'inici." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "El procés de servidor de l'Akonadi s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"El procés de servidor de l'Akonadi s'ha registrat al D-Bus, el qual " +"normalment indica que es troba operatiu." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "El procés de servidor de l'Akonadi no s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"El procés de servidor de l'Akonadi no s'ha registrat al D-Bus, el qual " +"normalment vol dir que no s'ha iniciat o que ha trobat un error fatal durant " +"l'inici." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "No és possible comprovar la versió del protocol." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"No és possible comprovar si la versió del protocol compleix els requeriments " +"sense una connexió amb el servidor." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "La versió del protocol del servidor és massa antiga." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"La versió del protocol del servidor és la %1, però el client requereix la " +"versió %2. Si heu actualitzat recentment la PIM del KDE, assegureu-vos de " +"tornar a iniciar tant Akonadi com la PIM del KDE." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "La versió del protocol del servidor és massa nova." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "La versió del protocol del servidor coincideix." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "L'actual versió del protocol del servidor és la %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "S'han trobat els agents de recursos." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "S'ha trobat com a mínim un agent de recursos." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "No s'ha trobat cap agent de recursos." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"No s'ha trobat cap agent de recursos, l'Akonadi no es pot usar sense almenys " +"un. Normalment això significa que no hi ha cap agent instal·lat o que hi ha " +"un problema d'arranjament. S'ha cercat als camins següents: «%1». La " +"variable d'entorn XDG_DATA_DIRS s'ha definit com a «%2», comproveu que " +"inclogui tots els camins on s'han instal·lat els agents de l'Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "No s'ha trobat cap registre actual d'error del servidor Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"El servidor Akonadi no ha informat de cap error durant la seva engegada " +"actual." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "S'ha trobat el registre actual d'error del servidor Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"El servidor Akonadi ha informat d'errors durant la seva engegada actual. Es " +"pot trobar el registre a %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "No s'ha trobat cap registre d'error anterior del servidor Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"El servidor Akonadi no ha informat de cap error durant la seva engegada " +"anterior." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "S'ha trobat el registre d'error anterior del servidor Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"El servidor Akonadi ha informat d'errors durant la seva engegada anterior. " +"Es pot trobar el registre a %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "No s'ha trobat cap registre d'error actual del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"El procés de control de l'Akonadi no ha informat de cap error durant la seva " +"engegada actual." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "S'ha trobat el registre d'errors actual del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"El procés de control de l'Akonadi ha informat d'errors durant la seva " +"engegada actual. El registre es pot trobar a %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "No s'ha trobat cap registre d'error anterior del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"El procés de control de l'Akonadi no ha informat de cap error durant la seva " +"engegada anterior." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "S'ha trobat el registre d'errors anterior del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"El procés de control de l'Akonadi ha informat d'errors durant l'engegada " +"anterior. El registre es pot trobar a %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "L'Akonadi s'ha iniciat com a root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Executar aplicacions de cara a Internet com a root/administrador us exposa a " +"molts riscos de seguretat. El MySQL, usat per aquesta instal·lació de " +"l'Akonadi, no permet executar-se com a root per a protegir-vos d'aquests " +"riscos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "L'Akonadi no s'està executant com a root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"L'Akonadi no s'està executant com a usuari root/administrador, el qual és " +"l'arranjament recomanat per a un sistema segur." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Desa l'informe de la prova" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Error" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "No s'ha pogut obrir el fitxer «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"S'ha produït un error durant l'inici del servidor Akonadi. Se suposa que les " +"autocomprovacions següents ajudaran a determinar i solucionar aquest " +"problema. Quan sol·liciteu ajuda o informeu d'errors, cal incloure sempre " +"aquest informe, si us plau." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalls" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Per a més consells sobre la resolució de problemes, vegeu userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Carpeta &nova..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nou" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Su&primeix la carpeta" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Suprimeix" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sincronitza la carpeta" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronitza" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propietats de la carpeta" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propietats" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Enganxa" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Enganxa" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gestiona les &subscripcions locals..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gestió de les subscripcions locals" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Afegeix a les carpetes preferides" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Afegeix a les preferides" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Elimina de les carpetes preferides" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Elimina de les preferides" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Reanomena una preferida..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Reanomena" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copia la carpeta a..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copia a" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copia l'element a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mou l'element a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mou a" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mou la carpeta a..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Retalla l'element" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Retalla" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Retalla la carpeta" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Crea un recurs" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Suprimeix el recurs" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Propietats del recurs" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sincronitza el recurs" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Treballa desconnectat" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronitza les carpetes recursivament" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronitza recursivament" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mou la carpeta a la paperera" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mou la carpeta a la paperera" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Mou l'element a la paperera" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Mou l'element a la paperera" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restaura la carpeta des de la paperera" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restaura la carpeta des de la paperera" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restaura l'element des de la paperera" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restaura l'element des de la paperera" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restaura la col·lecció des de la paperera" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restaura la col·lecció des de la paperera" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronitza les carpetes preferides" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronitza les carpetes preferides" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronitza l'arbre de carpetes" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copia la carpeta" +msgstr[1] "&Copia %1 carpetes" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copia l'element" +msgstr[1] "&Copia %1 elements" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Retalla l'element" +msgstr[1] "&Retalla %1 elements" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Retalla la carpeta" +msgstr[1] "&Retalla %1 carpetes" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Su&primeix l'element" +msgstr[1] "Su&primeix %1 elements" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Su&primeix la carpeta" +msgstr[1] "Su&primeix %1 carpetes" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronitza la carpeta" +msgstr[1] "&Sincronitza %1 carpetes" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Su&primeix el recurs" +msgstr[1] "Su&primeix %1 recursos" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronitza el recurs" +msgstr[1] "&Sincronitza %1 recursos" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copia la carpeta" +msgstr[1] "Copia %1 carpetes" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copia l'element" +msgstr[1] "Copia %1 elements" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Retalla l'element" +msgstr[1] "Retalla %1 elements" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Retalla la carpeta" +msgstr[1] "Retalla %1 carpetes" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Suprimeix element" +msgstr[1] "Suprimeix %1 elements" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Suprimeix la carpeta" +msgstr[1] "Suprimeix %1 carpetes" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronitza la carpeta" +msgstr[1] "Sincronitza %1 carpetes" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Suprimeix el recurs" +msgstr[1] "Suprimeix %1 recursos" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronitza el recurs" +msgstr[1] "Sincronitza %1 recursos" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nom" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Realment voleu suprimir aquesta carpeta i totes les seves subcarpetes?" +msgstr[1] "Realment voleu suprimir %1 carpetes i totes les seves subcarpetes?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Suprimeixo la carpeta?" +msgstr[1] "Suprimeixo les carpetes?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "No s'ha pogut suprimir la carpeta: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Ha fallat l'eliminació de la carpeta" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propietats de la carpeta %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Realment voleu suprimir l'element seleccionat?" +msgstr[1] "Realment voleu suprimir %1 elements?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Suprimeixo l'element?" +msgstr[1] "Suprimeixo els elements?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "No s'ha pogut suprimir l'element: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Ha fallat la supressió de l'element" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Reanomena una preferida" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nom:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Recurs nou" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "No s'ha pogut crear el recurs: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Ha fallat la creació del recurs" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Realment voleu suprimir aquest recurs?" +msgstr[1] "Realment voleu suprimir %1 recursos?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Suprimeixo el recurs?" +msgstr[1] "Suprimeixo els recursos?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "No s'ha pogut enganxar les dades: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Ha fallat en enganxar" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "No es pot afegir «/» en el nom de la carpeta." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Error en crear una carpeta nova" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "No es pot afegir «.» a l'inici o al final del nom de la carpeta." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Abans de sincronitzar la carpeta «%1» cal tenir el recurs en línia. El voleu " +"posar en línia?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "El compte «%1» està desconnectat" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Connecta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mou a aquesta carpeta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copia a aquesta carpeta" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Ha fallat en actualitzar la subscripció: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Error de subscripció" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Subscripcions locals" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Cerca:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Només els &subscrits" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Su&bscriu" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Cancel·la la s&ubscripció" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Ha fallat en crear una etiqueta nova" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Hi ha hagut un error en crear una etiqueta nova" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Esteu segur que voleu eliminar l'etiqueta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Elimina l'etiqueta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Elimina l'etiqueta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Selecciona les etiquetes que s'hauran d'aplicar." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Crea una etiqueta nova" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gestió de les etiquetes" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Selecció d'etiquetes..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Selecció d'etiquetes" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Neteja" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Feu clic per a afegir les etiquetes" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Convertidor a XML de l'Akonadi" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Converteix un subarbre de col·lecció de l'Akonadi en un fitxer XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "No hi ha dades carregades." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "No s'ha especificat cap nom de fitxer vàlid" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "No s'ha pogut obrir el fitxer de dades «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "El fitxer %1 no existeix." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "No s'ha pogut analitzar el fitxer de dades «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "No s'ha pogut carregar i analitzar la definició de l'esquema." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "No s'ha pogut crear l'esquema d'anàlisi del context." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "No s'ha pogut crear l'esquema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "No s'ha pogut crear l'esquema de validació del context." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Format de fitxer no vàlid." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "No s'ha pogut analitzar el fitxer de dades: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "No s'ha pogut trobar la col·lecció %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "ID remot" + +#~ msgid "MimeType" +#~ msgstr "Tipus MIME" + +#~ msgid "Form" +#~ msgstr "Formulari" + +#~ msgid "Default Name" +#~ msgstr "Nom per defecte" diff --git a/po/ca@valencia/akonadi_knut_resource.po b/po/ca@valencia/akonadi_knut_resource.po new file mode 100644 index 0000000..6538e33 --- /dev/null +++ b/po/ca@valencia/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# Translation of akonadi_knut_resource.po to Catalan (Valencian) +# Copyright (C) 2009-2014 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Manuel Tortosa Moreno , 2009, 2010. +# Josep Ma. Ferrer , 2010. +# Manuel Tortosa , 2010. +# Antoni Bella Pérez , 2014. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2014-03-04 19:43+0100\n" +"Last-Translator: Antoni Bella Pérez \n" +"Language-Team: Catalan \n" +"Language: ca@valencia\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.5\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "No s'ha seleccionat cap fitxer de dades." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "El fitxer «%1» s'ha carregat correctament." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Seleccioneu un fitxer de dades" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Fitxer de dades Knut de l'Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "No s'ha trobat cap element per a l'ID remot %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "No s'ha trobat la col·lecció pare a l'arbre DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "No s'ha pogut escriure la col·lecció." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "No s'ha trobat la col·lecció modificada a l'arbre DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "No s'ha trobat la col·lecció eliminada a l'arbre DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "No s'ha trobat la col·lecció pare «%1» a l'arbre DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "No s'ha pogut escriure l'element." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "No s'ha trobat l'element modificat a l'arbre DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "No s'ha trobat l'element eliminat a l'arbre DOM." diff --git a/po/ca@valencia/libakonadi5.po b/po/ca@valencia/libakonadi5.po new file mode 100644 index 0000000..2d7724e --- /dev/null +++ b/po/ca@valencia/libakonadi5.po @@ -0,0 +1,2622 @@ +# Translation of libakonadi5.po to Catalan (Valencian) +# Copyright (C) 2007-2021 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Josep Ma. Ferrer , 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020. +# Manuel Tortosa Moreno , 2009, 2010. +# Antoni Bella Pérez , 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 13:16+0100\n" +"Last-Translator: Antoni Bella Pérez \n" +"Language-Team: Catalan \n" +"Language: ca@valencia\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.12.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Josep Ma. Ferrer,Antoni Bella" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "txemaq@gmail.com,antonibella5@yahoo.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Actualment no hi ha cap compte configurat." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "La integració dels comptes no està admesa" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "No s'ha pogut registrar l'objecte en el «dbus»: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 del tipus %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificador de l'agent" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agent de l'Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Preparat" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Desconnectat" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "S'està sincronitzant..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Error." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Sense configurar" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificador del recurs" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Recurs de l'Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "S'ha recuperat un element no vàlid" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Error en crear l'element: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Error en actualitzar la col·lecció: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Ha fallat l'actualització de la col·lecció local: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Ha fallat l'actualització dels elements locals: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "No es pot recuperar l'element en el mode desconnectat." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "S'està sincronitzant la carpeta «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Ha fallat en recuperar la col·lecció per a sincronitzar-la." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Ha fallat en recuperar la col·lecció per a la sincronització dels atributs." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "L'element sol·licitat ja no existeix" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Treball cancel·lat." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Aquesta col·lecció no existeix." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "S'han trobat col·leccions òrfenes sense resoldre" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "No s'ha trobat cap element per a gestionar el conflicte" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "No s'ha pogut accedir a la interfície de D-Bus de l'agent creat." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "La creació de la instància de l'agent ha excedit el temps." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "No s'ha pogut obtindre el tipus de l'agent «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "No s'ha pogut crear la instància de l'agent." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Instància de la col·lecció no vàlida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Instància del recurs no vàlida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "No s'ha pogut obtindre la interfície de D-Bus per al recurs «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "La sincronització dels atributs de la col·lecció ha excedit el temps." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Col·lecció no vàlida per a copiar" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Destinació de la col·lecció no vàlida" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Pare no vàlid" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Ha fallat en analitzar la col·lecció de la resposta." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Col·lecció no vàlida" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Col·lecció proporcionada no vàlida." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "No s'ha especificat cap objecte per a moure" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "No s'ha especificat cap destinació vàlida" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Col·lecció no vàlida." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Col·lecció pare no vàlida" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "No s'ha pogut connectar amb el servei Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"La versió del protocol del servidor Akonadi és incompatible. Comproveu que " +"teniu instal·lada una versió compatible." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operació cancel·lada per l'usuari." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Error desconegut." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Resposta inesperada" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Ha fallat en crear la relació." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "La sincronització del recurs ha excedit el temps." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "No s'ha pogut recuperar la col·lecció arrel del recurs %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "No s'ha proporcionat l'ID del recurs." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identificador del recurs «%1» no vàlid" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Ha fallat en configurar el recurs per omissió via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Ha fallat en recuperar la col·lecció de recursos." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Ha caducat el temps en provar d'obtindre el bloqueig." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Ha fallat en crear l'etiqueta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Ha fallat en moure la col·lecció a la paperera, s'ha interromput l'operació." + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "S'han passat elements no vàlids" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "S'ha passat una col·lecció no vàlida" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Col·lecció no vàlida o llista d'elements buida" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"No s'ha pogut trobar la col·lecció a restaurar i el recurs de restauració no " +"està disponible" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nom" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "S'està carregant..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Error" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"La col·lecció de destinació «%1» ja conté\n" +"una col·lecció amb el nom «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nom" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "No s'ha pogut copiar l'element: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "No s'ha pogut copiar la col·lecció: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "No s'ha pogut moure l'element: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "No s'ha pogut moure la col·lecció: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "No s'ha pogut enllaçar l'entitat: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Error" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Carpetes preferides" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Total de missatges" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Missatges sense llegir" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Mida d'emmagatzematge" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Mida d'emmagatzematge de la subcarpeta" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Sense llegir" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Mida" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiqueta" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "No s'ha pogut recuperar l'element per a l'índex" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "L'índex ja no està disponible" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "La part de contingut «%1» no està disponible per a aquest índex" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "No hi ha disponible cap sessió per a aquest índex" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "No hi ha disponible cap element per a aquest índex" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Connector sense nom" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "No hi ha disponible cap descripció" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"La versió del protocol del servidor Akonadi difereix de la versió del " +"protocol emprat per aquesta aplicació.\n" +"Si heu actualitzat recentment el sistema, per favor, eixiu i torneu a " +"connectar per a assegurar-vos que totes les aplicacions empren la versió " +"correcta del protocol." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"No hi ha disponible cap agent de l'Akonadi. Per favor, verifiqueu la vostra " +"instal·lació PIM del KDE." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Les versions del protocol no coincideixen. La versió del servidor és més " +"antiga (%1) que la nostra (%2). Si heu actualitzat el vostre sistema " +"recentment, reinicieu el servidor Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Les versions del protocol no coincideixen. La versió del servidor és més " +"nova (%1) que la nostra (%2). Si heu actualitzat el vostre sistema " +"recentment, reinicieu totes les aplicacions PIM del KDE." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Autocomprovació de l'Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Comprova i informa sobre l'estat del servidor Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Instància &nova de l'agent..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Su&primeix la instància de l'agent" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configura la instància de l'agent" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Instància nova de l'agent" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "No s'ha pogut crear la instància de l'agent: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "La creació de la instància de l'agent ha fallat" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Suprimeixo la instància de l'agent?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Realment voleu suprimir la instància seleccionada de l'agent?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuració de %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manual del %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Quant al %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "El diàleg de configuració s'ha obert en una altra finestra" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "La configuració per a %1 ja es troba oberta en un altre lloc." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Ha fallat en registrar el diàleg de configuració %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minut" +msgstr[1] "minuts" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Recuperació" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usa les opcions de la carpeta pare o del compte" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronitza en seleccionar aquesta carpeta" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronitza automàticament després de:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Mai" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuts" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Parts en memòria cau localment" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opcions de recuperació" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Recupera sempre els &missatges complets" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Recupera els cossos dels missatges a petició" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Mantén localment els cossos dels missatges durant:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Per a sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Cerca" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usa la carpeta per omissió" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Subcarpeta &nova..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Crea una subcarpeta nova sota la carpeta seleccionada" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Carpeta nova" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nom" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "No s'ha pogut crear la carpeta: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Ha fallat la creació de la carpeta" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "General" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un objecte" +msgstr[1] "%1 objectes" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nom:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usa una icona personalitzada:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "carpeta" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estadístiques" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contingut:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objectes" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Mida:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bytes" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Recordeu que la indexació pot trigar uns minuts." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Manteniment" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Error en recuperar el compte dels elements indexats" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "S'ha indexat %1 element en aquesta carpeta" +msgstr[1] "S'han indexat %1 elements en aquesta carpeta" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "S'estan calculant els elements indexats..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fitxers" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipus de carpeta:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "desconegut" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elements" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elements totals:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Elements sense llegir:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexació" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Activa la indexació de text complet" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "S'està recuperant el compte dels elements indexats..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Torna a indexar la carpeta" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Cap carpeta" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Diàleg d'obertura de col·lecció" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Selecció d'una col·lecció" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mou ací" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copia ací" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancel·la" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Hora de modificació" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Indicadors" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolució de conflictes" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Agafa la meua versió" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Agafa la seua versió" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mantén ambdues versions" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Els vostres canvis entren en conflicte amb els d'algú altre.
A menys " +"que es puga llençar alguna versió, els haureu d'integrar manualment.
Feu " +"clic a «Obri l'editor de text» per a " +"mantindre una còpia dels textos, seleccioneu la versió més correcta, torneu-" +"la a obrir i modifiqueu-la de nou per a afegir-hi el que falta." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dades" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "S'està iniciant el servidor Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "S'està aturant el servidor Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mou ací" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copia ací" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Enllaça ací" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancel·la" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"No s'ha pogut connectar amb el servei per a la gestió de la informació " +"personal.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "S'està iniciant el servei per a la gestió de la informació personal..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "S'està aturant el servei per a la gestió de la informació personal..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"El servei per a la gestió de la informació personal està realitzant una " +"actualització de la base de dades..." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"El servei per a la gestió de la informació personal està efectuant una " +"actualització de la base de dades. Això succeeix després d'una actualització " +"del programari i, és necessari per a optimitzar el rendiment.\n" +"En funció de la quantitat d'informació personal, això pot trigar alguns " +"minuts." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"El servei per a la gestió de la informació personal Akonadi no es troba en " +"execució. Aquesta aplicació no es pot usar sense ell." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Inicia" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"El marc de treball per a la gestió de la informació personal Akonadi no es " +"troba operatiu.\n" +"Feu clic a «Detalls...» per a obtindre informació detallada quant a aquest " +"problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"El servei per a la gestió de la informació personal Akonadi no es troba " +"operatiu." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalls..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Voleu eliminar el compte «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Elimino el compte?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Comptes d'entrada (afegiu-ne un com a mínim):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&fig..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modifica..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Elimina" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reinicia" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Carpeta recent" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Reanomena una preferida" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nom:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Autocomprovació del servidor Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Guarda l'informe..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copia l'informe al porta-retalls" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"La configuració actual del servidor Akonadi requereix el controlador «%1» de " +"QtSQL i s'ha trobat en el sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"La configuració actual del servidor Akonadi requereix el controlador «%1» de " +"QtSQL.\n" +"Estan instal·lats els següents controladors: %2.\n" +"Comproveu que el controlador requerit estiga instal·lat." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "S'ha trobat el controlador de la base de dades." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "No s'ha trobat el controlador de la base de dades." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "No s'ha provat l'executable del servidor MySQL." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "La configuració actual no requereix cap servidor intern de MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Actualment heu configurat l'Akonadi per a usar el servidor «%1» de MySQL.\n" +"Comproveu que teniu el servidor MySQL instal·lat, definit el camí correcte i " +"assegureu-vos que teniu els permisos de lectura i execució necessaris sobre " +"l'executable del servidor. L'executable del servidor normalment s'anomena " +"«mysqld», la seua ubicació varia en funció de la distribució." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "No s'ha trobat cap servidor MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "No es pot llegir des del servidor MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "El servidor MySQL no es pot executar." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "S'ha trobat el MySQL amb un nom inesperat." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "S'ha trobat el servidor MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "S'ha trobat el servidor MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "El servidor MySQL és executable." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Ha fallat en executar el servidor «%1» de MySQL amb el missatge d'error " +"següent: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Ha fallat en executar el servidor MySQL." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "No s'ha provat el registre d'error del servidor MySQL." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "No s'ha trobat cap registre d'error actual del MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"El servidor MySQL no ha informat de cap error durant aquest inici. El " +"registre es pot trobar a «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "El registre d'error del MySQL no es pot llegir." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"S'ha trobat un registre d'error del servidor MySQL però no es pot llegir: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "El registre del servidor MySQL conté errors." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "El fitxer de registre d'errors «%1» del servidor MySQL conté errors." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "El registre del servidor MySQL conté avisos." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "El fitxer de registre d'errors «%1» del servidor MySQL conté avisos." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "El registre del servidor MySQL no conté errors." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"El fitxer de registre d'errors «%1» del servidor MySQL no conté cap error ni " +"avís." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "La configuració del servidor MySQL no s'ha provat." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "S'ha trobat la configuració per omissió del servidor MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"S'ha trobat la configuració per omissió del servidor MySQL i es pot llegir a " +"%1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "No s'ha trobat la configuració per omissió del servidor MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"No s'ha trobat la configuració per omissió del servidor MySQL o no s'ha " +"pogut llegir. Comproveu que la instal·lació de l'Akonadi ha acabat i que " +"teniu els permisos d'accés requerits." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "La configuració personalitzada del servidor MySQL no està disponible." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"No s'ha trobat la configuració personalitzada del servidor MySQL, però és " +"opcional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "S'ha trobat la configuració personalitzada del servidor MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"S'ha trobat la configuració personalitzada del servidor MySQL i es pot " +"llegir a %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "La configuració personalitzada del servidor MySQL no es pot llegir." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"S'ha trobat la configuració personalitzada del servidor MySQL a %1, però no " +"es pot llegir. Comproveu els vostres permisos d'accés." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "No s'ha trobat la configuració del servidor MySQL o no es pot llegir." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "No s'ha trobat la configuració del servidor MySQL o no es pot llegir." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "La configuració del servidor MySQL es pot usar." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "S'ha trobat la configuració del servidor MySQL a %1 i es pot llegir." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "No s'ha pogut connectar amb el servidor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "S'ha trobat el servidor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "S'ha trobat el servidor PostgreSQL i la connexió funciona." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "No s'ha trobat l'«akonadictl»" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"El programa «akonadictl» cal que estiga accessible a la $PATH. Comproveu que " +"teniu instal·lat el servidor Akonadi." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "S'ha trobat l'«akonadictl» i es pot usar" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"S'ha trobat el programa «%1» per a controlar el servidor Akonadi i s'ha " +"pogut executar correctament.\n" +"Resultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "S'ha trobat l'«akonadictl» però no es pot usar" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"S'ha trobat el programa «%1» per a controlar el servidor Akonadi, però no " +"s'ha pogut executar correctament.\n" +"Resultat:\n" +"%2\n" +"Comproveu que el servidor Akonadi estiga correctament instal·lat." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "El procés de control de l'Akonadi s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"El procés de control de l'Akonadi s'ha registrat al D-Bus, el qual " +"normalment indica que es troba operatiu." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "El procés de control de l'Akonadi no s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"El procés de control de l'Akonadi no s'ha registrat al D-Bus, el qual " +"normalment vol dir que no s'ha iniciat o que ha trobat un error fatal durant " +"l'inici." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "El procés de servidor de l'Akonadi s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"El procés de servidor de l'Akonadi s'ha registrat al D-Bus, el qual " +"normalment indica que es troba operatiu." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "El procés de servidor de l'Akonadi no s'ha registrat al D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"El procés de servidor de l'Akonadi no s'ha registrat al D-Bus, el qual " +"normalment vol dir que no s'ha iniciat o que ha trobat un error fatal durant " +"l'inici." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "No és possible comprovar la versió del protocol." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"No és possible comprovar si la versió del protocol compleix els requeriments " +"sense una connexió amb el servidor." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "La versió del protocol del servidor és massa antiga." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"La versió del protocol del servidor és la %1, però el client requereix la " +"versió %2. Si heu actualitzat recentment la PIM del KDE, assegureu-vos de " +"tornar a iniciar tant Akonadi com la PIM del KDE." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "La versió del protocol del servidor és massa nova." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "La versió del protocol del servidor coincideix." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "L'actual versió del protocol del servidor és la %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "S'han trobat els agents de recursos." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "S'ha trobat com a mínim un agent de recursos." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "No s'ha trobat cap agent de recursos." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"No s'ha trobat cap agent de recursos, l'Akonadi no es pot usar sense almenys " +"un. Normalment això significa que no hi ha cap agent instal·lat o que hi ha " +"un problema d'arranjament. S'ha cercat als camins següents: «%1». La " +"variable d'entorn XDG_DATA_DIRS s'ha definit com a «%2», comproveu que " +"incloga tots els camins on s'han instal·lat els agents de l'Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "No s'ha trobat cap registre actual d'error del servidor Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"El servidor Akonadi no ha informat de cap error durant la seua engegada " +"actual." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "S'ha trobat el registre actual d'error del servidor Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"El servidor Akonadi ha informat d'errors durant la seua engegada actual. Es " +"pot trobar el registre a %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "No s'ha trobat cap registre d'error anterior del servidor Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"El servidor Akonadi no ha informat de cap error durant la seua engegada " +"anterior." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "S'ha trobat el registre d'error anterior del servidor Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"El servidor Akonadi ha informat d'errors durant la seua engegada anterior. " +"Es pot trobar el registre a %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "No s'ha trobat cap registre d'error actual del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"El procés de control de l'Akonadi no ha informat de cap error durant la seua " +"engegada actual." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "S'ha trobat el registre d'errors actual del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"El procés de control de l'Akonadi ha informat d'errors durant la seua " +"engegada actual. El registre es pot trobar a %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "No s'ha trobat cap registre d'error anterior del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"El procés de control de l'Akonadi no ha informat de cap error durant la seua " +"engegada anterior." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "S'ha trobat el registre d'errors anterior del control de l'Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"El procés de control de l'Akonadi ha informat d'errors durant l'engegada " +"anterior. El registre es pot trobar a %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "L'Akonadi s'ha iniciat com a root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Executar aplicacions de cara a Internet com a root/administrador vos exposa " +"a molts riscos de seguretat. El MySQL, usat per aquesta instal·lació de " +"l'Akonadi, no permet executar-se com a root per a protegir-vos d'aquests " +"riscos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "L'Akonadi no s'està executant com a root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"L'Akonadi no s'està executant com a usuari root/administrador, el qual és " +"l'arranjament recomanat per a un sistema segur." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Guarda l'informe de la prova" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Error" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "No s'ha pogut obrir el fitxer «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"S'ha produït un error durant l'inici del servidor Akonadi. Se suposa que les " +"autocomprovacions següents ajudaran a determinar i solucionar aquest " +"problema. Quan sol·liciteu ajuda o informeu d'errors, cal incloure sempre " +"aquest informe, per favor." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalls" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Per a més consells sobre la resolució de problemes, vegeu userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Carpeta &nova..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nou" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Su&primeix la carpeta" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Suprimeix" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sincronitza la carpeta" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronitza" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propietats de la carpeta" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propietats" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Enganxa" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Enganxa" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gestiona les &subscripcions locals..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gestió de les subscripcions locals" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Afig a les carpetes preferides" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Afig a les preferides" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Elimina de les carpetes preferides" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Elimina de les preferides" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Reanomena una preferida..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Reanomena" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copia la carpeta a..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copia a" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copia l'element a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mou l'element a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mou a" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mou la carpeta a..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Retalla l'element" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Retalla" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Retalla la carpeta" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Crea un recurs" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Suprimeix el recurs" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Propietats del recurs" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sincronitza el recurs" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Treballa desconnectat" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronitza les carpetes recursivament" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronitza recursivament" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mou la carpeta a la paperera" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mou la carpeta a la paperera" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Mou l'element a la paperera" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Mou l'element a la paperera" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restaura la carpeta des de la paperera" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restaura la carpeta des de la paperera" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restaura l'element des de la paperera" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restaura l'element des de la paperera" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restaura la col·lecció des de la paperera" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restaura la col·lecció des de la paperera" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronitza les carpetes preferides" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronitza les carpetes preferides" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronitza l'arbre de carpetes" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copia la carpeta" +msgstr[1] "&Copia %1 carpetes" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copia l'element" +msgstr[1] "&Copia %1 elements" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Retalla l'element" +msgstr[1] "&Retalla %1 elements" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Retalla la carpeta" +msgstr[1] "&Retalla %1 carpetes" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Su&primeix l'element" +msgstr[1] "Su&primeix %1 elements" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Su&primeix la carpeta" +msgstr[1] "Su&primeix %1 carpetes" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronitza la carpeta" +msgstr[1] "&Sincronitza %1 carpetes" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Su&primeix el recurs" +msgstr[1] "Su&primeix %1 recursos" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronitza el recurs" +msgstr[1] "&Sincronitza %1 recursos" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copia la carpeta" +msgstr[1] "Copia %1 carpetes" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copia l'element" +msgstr[1] "Copia %1 elements" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Retalla l'element" +msgstr[1] "Retalla %1 elements" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Retalla la carpeta" +msgstr[1] "Retalla %1 carpetes" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Suprimeix element" +msgstr[1] "Suprimeix %1 elements" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Suprimeix la carpeta" +msgstr[1] "Suprimeix %1 carpetes" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronitza la carpeta" +msgstr[1] "Sincronitza %1 carpetes" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Suprimeix el recurs" +msgstr[1] "Suprimeix %1 recursos" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronitza el recurs" +msgstr[1] "Sincronitza %1 recursos" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nom" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Realment voleu suprimir aquesta carpeta i totes les seues subcarpetes?" +msgstr[1] "Realment voleu suprimir %1 carpetes i totes les seues subcarpetes?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Suprimeixo la carpeta?" +msgstr[1] "Suprimeixo les carpetes?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "No s'ha pogut suprimir la carpeta: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Ha fallat l'eliminació de la carpeta" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propietats de la carpeta %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Realment voleu suprimir l'element seleccionat?" +msgstr[1] "Realment voleu suprimir %1 elements?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Suprimeixo l'element?" +msgstr[1] "Suprimeixo els elements?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "No s'ha pogut suprimir l'element: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Ha fallat la supressió de l'element" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Reanomena una preferida" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nom:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Recurs nou" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "No s'ha pogut crear el recurs: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Ha fallat la creació del recurs" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Realment voleu suprimir aquest recurs?" +msgstr[1] "Realment voleu suprimir %1 recursos?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Suprimeixo el recurs?" +msgstr[1] "Suprimeixo els recursos?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "No s'ha pogut enganxar les dades: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Ha fallat en enganxar" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "No es pot afegir «/» en el nom de la carpeta." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Error en crear una carpeta nova" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "No es pot afegir «.» a l'inici o al final del nom de la carpeta." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Abans de sincronitzar la carpeta «%1» cal tindre el recurs en línia. El " +"voleu posar en línia?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "El compte «%1» està desconnectat" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Connecta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mou a aquesta carpeta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copia a aquesta carpeta" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Ha fallat en actualitzar la subscripció: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Error de subscripció" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Subscripcions locals" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Cerca:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Només els &subscrits" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Su&bscriu" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Cancel·la la s&ubscripció" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Ha fallat en crear una etiqueta nova" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Hi ha hagut un error en crear una etiqueta nova" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Esteu segur que voleu eliminar l'etiqueta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Elimina l'etiqueta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Elimina l'etiqueta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Selecciona les etiquetes que s'hauran d'aplicar." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Crea una etiqueta nova" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gestió de les etiquetes" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Selecció d'etiquetes..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Selecció d'etiquetes" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Neteja" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Feu clic per a afegir les etiquetes" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Convertidor a XML de l'Akonadi" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Converteix un subarbre de col·lecció de l'Akonadi en un fitxer XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "No hi ha dades carregades." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "No s'ha especificat cap nom de fitxer vàlid" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "No s'ha pogut obrir el fitxer de dades «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "El fitxer %1 no existeix." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "No s'ha pogut analitzar el fitxer de dades «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "No s'ha pogut carregar i analitzar la definició de l'esquema." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "No s'ha pogut crear l'esquema d'anàlisi del context." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "No s'ha pogut crear l'esquema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "No s'ha pogut crear l'esquema de validació del context." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Format de fitxer no vàlid." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "No s'ha pogut analitzar el fitxer de dades: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "No s'ha pogut trobar la col·lecció %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "ID remot" + +#~ msgid "MimeType" +#~ msgstr "Tipus MIME" + +#~ msgid "Form" +#~ msgstr "Formulari" + +#~ msgid "Default Name" +#~ msgstr "Nom per defecte" diff --git a/po/cs/akonadi_knut_resource.po b/po/cs/akonadi_knut_resource.po new file mode 100644 index 0000000..6ba7081 --- /dev/null +++ b/po/cs/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Vít Pelčák , 2010, 2011, 2013, 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2014-04-10 13:21+0200\n" +"Last-Translator: Vít Pelčák \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nebyl vybrán datový soubor." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Soubor '%1' byl úspěšně načten." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Vyberte datový soubor" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Datový soubor Akonadi Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Položka nenalezena pro remoteid %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Rodičovská sbírka v DOM stromu nenalezena." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Nelze zapsat sbírku." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Upravená sbírka v DOM stromu nenalezena." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Smazaná sbírka nenalezena v DOM stromu." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Rodičovská sbírka '%1' v DOM stromu nenalezena." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Nelze zapsat položku." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Upravená položka nenalezena v DOM stromu." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Smazaná položka nenalezena v DOM stromu." diff --git a/po/cs/libakonadi5.po b/po/cs/libakonadi5.po new file mode 100644 index 0000000..bc34577 --- /dev/null +++ b/po/cs/libakonadi5.po @@ -0,0 +1,2517 @@ +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Vít Pelčák , 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020. +# Lukáš Tinkl , 2011, 2012. +# Tomáš Chvátal , 2012. +# Vit Pelcak , 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-07-23 10:19+0200\n" +"Last-Translator: Vit Pelcak \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Lokalize 21.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Vít Pelčák, Tomáš Chvátal" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "vit@pelcak.org, tomas.chvatal@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Nyní není nastaven žádný účet." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integrace účtů není podporována" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Nelze registrovat objekt na dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 typu %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identifikátor agenta" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agent Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Připraven" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Odpojen" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synchronizování..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Chyba." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nenastaveno" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identifikátor zdroje" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Zdroj Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Stažena neplatná položka" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Chyba při vytváření položky: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Chyba při aktualizaci místní sbírky: %1." + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Aktualizace místní sbírky selhala: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Aktualizace místních položek selhala: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Nelze stahovat položky v odpojeném režimu." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synchronizuje se složka '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Požadovaná položka už neexistuje" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Úloha byla zrušena." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Taková sbírka není." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Nelze přistupovat k rozhraní D-Bus vytvořeného agenta." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Vypršel čas na vytvoření instance agenta." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Nelze získat agenta typu '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Nelze vytvořit instanci agenta." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Neplatná instance sbírky." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Neplatná instance zdroje." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Nelze získat rozhraní D-Bus zdroje '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Synchronizaci atributů sbírky vypršel časový limit." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Neplatná sbírka pro kopírování" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Neplatná cílová sbírka" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Neplatný rodič" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Selhala analýza sbírky z odpovědi" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Neplatná sbírka" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Zadána neplatná sbírka." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nezadány objekty pro přesunutí" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nezadán platný cíl" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Neplatná sbírka." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Neplatná rodičovská sbírka" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Nelze se připojit ke službě Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Verze protokolu Akonadi serveru je nekompatibilní. Ujistěte se, že máte " +"nainstalovanou kompatibilní verzi." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Uživatel přerušil operaci." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Neznámá chyba." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Neočekávaná odpověď" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Nelze vytvořit vztah." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Synchronizace zdroje vypršela." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Nelze vytvořit značku." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Předány neplatné položky" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Jméno" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Načítání..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Chyba" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Cílová sbírka '%1' již obsahuje\n" +"sbírku s názvem '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Název" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Nelze kopírovat položku: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Nelze kopírovat sbírku: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Nelze přesunout položku: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Nelze přesunout sbírku: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Nelze propojit entitu: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Chyba" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Oblíbené složky" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Celkem zpráv" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Nepřečtené zprávy" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvóta" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Velikost úložiště" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Velikost úložiště podsložky" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Nepřečtené" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Celkem" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Velikost" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Značka" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Nelze stáhnout položku indexu" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index již není dostupný" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Pro index nejsou dostupná žádná sezení" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Pro index nejsou dostupné žádné položky" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Nepojmenovaný modul" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Popis není dostupný" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Nejsou dostupní žádní agenti Akonadi. Prosím zkontrolujte instalaci KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Test akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Kontroluje a hlásí stav serveru Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nová instance agenta" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Smazat instanci agenta" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Nastavit instanci agenta" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nová instance agenta" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Nelze vytvořit instanci agenta: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Vytvoření instance agenta selhalo" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Smazat instanci agenta?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Opravdu si přejete smazat vybranou instanci agenta?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Nastavení %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Příručka %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "O aplikaci %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Konfigurační dialog byl otevřen v jiném okně" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Nastavení pro %1 je již otevřeno jinde." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Selhala registrace konfiguračního dialogu %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuta" +msgstr[1] "minuty" +msgstr[2] "minut" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Stažení" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Použít možnosti nadřazené složky nebo účtu" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchronizovat při označení této složky" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automaticky synchronizovat po:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nikdy" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutách" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokálně uložené části" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Možnosti stahování" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Vždy získávat &celé zprávy" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Získávat těla zpráv na požádání" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Uschovat těla zpráv lokálně po:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Napořád" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Hledat" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nová podřízená složka..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Vytvořit podsložku ve vybrané složce" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nová složka" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Název" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Nelze vytvořit složku: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Vytvoření složky selhalo" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Obecné" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Jeden objekt" +msgstr[1] "%1 objekty" +msgstr[2] "%1 objektů" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Název:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Po&užít vlastní ikonu:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "složka" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistiky" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Obsah:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objektů" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Velikost:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bajtů" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Správa" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Chyba při získávání počtu indexovaných položek" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexována %1 položka v této složce" +msgstr[1] "Indexovány %1 položky v této složce" +msgstr[2] "Indexováno %1 položek v této složce" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Počítám indexované položky..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Soubory" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Typ složky:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "neznámý" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Položky" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Celkem položek:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Nepřečtené položky:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexování" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Povolit indexování plného textu" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Získávání počtu indexovaných položek..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Znovu indexovat složku" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Žádná složka" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Otevřít dialog sbírky" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Vybrat sbírku" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Přesunout se&m" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopírovat sem" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Zrušit" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Čas poslední změny" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Příznaky" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Vyřešení konfliktu" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Vzít moji verzi" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Vzít jejich verzi" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Ponechat obě verze" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Data" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Spouštím server Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Zastavuji server Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Přesunout se&m" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopírovat sem" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Od&kaz sem" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "Z&rušit" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Nelze se připojit ke službě správy osobních informací.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Spouští se Správce osobních informací..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Ukončuje se Správce osobních informací..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Služba správy osobních informací provádí aktualizaci databáze." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Správce osobních informací Akonadi není spuštěn. Tuto aplikaci bez něj nelze " +"používat." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Spustit" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Správa osobních informací Akonadi není funkční.\n" +"Klikněte na \"Detaily...\" pro více informací o problému." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Správce osobních informací Akonadi není spuštěn." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Podrobnosti..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Chcete odstranit účet '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Odstranit účet?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Příchozí účty (přidejte alespoň jeden):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "Při&dat..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Z&měnit..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "O&dstranit" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Restartovat" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Nedávná složka" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Přejmenovat oblíbenou" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Název:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Test akonadi serveru" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Uložit hlášení..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Zkopírovat hlášení do schránky" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Ovladač databáze nalezen." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Ovladač databáze nenalezen." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Spustitelný soubor serveru MySQL netestován." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Aktuální nastavení nevyžaduje interní server MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Server MySQL nenalezen." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Server MySQL nečitelný." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Server MySQL nespustitelný." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Server MySQL nalezen." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Server MySQL nalezen: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Server MySQL je spustitelný." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Selhalo spuštění MySQL serveru." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Záznam MySQL serveru obsahuje chyby." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Záznam serveru MySQL obsahuje varování." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Soubor záznamu '%1' serveru MySQL obsahuje varování." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Záznam serveru MySQL neobsahuje chyby." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Nastavení serveru MySQL netestováno." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Nalezena výchozí konfigurace MySQL serveru." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Výchozí konfigurace serveru MySQL nebyla nalezena." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Nastavení serveru MySQL je použitelné." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "V %1 bylo nalezeno čitelné nastavení serveru MySQL." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Server PostgreSQL nalezen." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl nenalezen" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl nalezen a je použitelný" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Program '%1' který ovládá Akonadi server byl úspěšně nalezen a spuštěn.\n" +"Výsledek:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl nalezen ale nepoužitelný" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Program '%1' který ovládá Akonadi server byl nalezen ale nemohl být úspěšně " +"spuštěn.\n" +"Výsledek:\n" +"%2\n" +"Ujistěte se, že je Akonadi server správně nainstalován." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Řídicí proces Akonadi je v D-Busu registrován." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Řídicí proces Akonadi není v D-Busu registrován." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Serverový proces Akonadi je v D-Busu registrován." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Serverový proces Akonadi není v D-Busu registrován." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Není možná kontrola verze protokolu." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Verze serverového protokolu je příliš stará." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Verze serverového protokolu je příliš nová." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Verze serverového protokolu odpovídá." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Současná verze protokolu je %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Nalezeni agenti zdroje." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Byl nalezen přinejmenším jeden agent zdroje." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Nenalezen žádný agent zdroje." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Nenalezen současný chybový záznam serveru Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Při svém spuštění server Akonadi nenahlásil žádnou chybu." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Nalezen současný chybový záznam serveru Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Předchozí chybový záznam serveru Akonadi nenalezen." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Při svém předchozím spuštění server Akonadi nenahlásil žádnou chybu." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Předchozí chybový záznam serveru Akonadi nalezen." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Nenalezen současný chybový záznam ovládání Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Nalezen současný chybový záznam ovládání Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Předchozí chybový záznam ovládání Akonadi nenalezen." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Předchozí chybový záznam ovládání Akonadi nalezen." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Uložit výsledky testu" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Chyba" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Není možné otevřít soubor '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Podrobnosti" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Pro více informací o řešení problémů prosím navštivte userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nová složka..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nový" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "S&mazat složku" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Smazat" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Synchronizovat složku" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchronizovat" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Vlastnosti složky" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Vlastnosti" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "V&ložit" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Vložit" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "&Spravovat lokální přihlášení..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Spravovat lokální přihlášení" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Přidat do oblíbených složek" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Přidat do oblíbených" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Odstranit z oblíbených složek" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Odstranit z oblíbených" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Přejmenovat oblíbenou..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Přejmenovat" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopírovat složku do..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopírovat do" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopírovat položku do..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Přesunout položku do ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Přesunout do" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Přesunout složku do..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Vyjmout položku" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Vyjmout" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Vyjmout složku" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Vytvořit zdroj" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Smazat zdroj" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Vlastnosti zd&roje" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synchronizovat zdroj" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Pracovat v odpojeném režimu" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synchronizovat složku rekurzivně" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synchronizovat rekurzivně" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Přesu&nout složku do koše" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Přesunout složku do koše" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Přesu&nout položku do koše" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Přesunout položku do koše" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Obnovit složku z koše" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Obnovit složku z koše" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Obnovit položku z koše" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Obnovit položku z koše" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Obnovit sbírku z koše" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Obnovit sbírku z koše" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synchronizovat oblíbené složky" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synchronizovat oblíbené složky" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synchronizovat strom složky" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopírovat složku" +msgstr[1] "&Kopírovat %1 složky" +msgstr[2] "&Kopírovat %1 složek" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopírovat položku" +msgstr[1] "&Kopírovat %1 položky" +msgstr[2] "&Kopírovat %1 položek" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Vyjmout položku" +msgstr[1] "&Vyjmout %1 položky" +msgstr[2] "&Vyjmout %1 položek" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Vyjmout složku" +msgstr[1] "&Vyjmout %1 složky" +msgstr[2] "&Vyjmout %1 složek" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "S&mazat položku" +msgstr[1] "S&mazat %1 položky" +msgstr[2] "S&mazat %1 položek" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "S&mazat složku" +msgstr[1] "S&mazat %1 složky" +msgstr[2] "S&mazat %1 složek" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synchronizovat složku" +msgstr[1] "&Synchronizovat %1 složky" +msgstr[2] "&Synchronizovat %1 složek" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Smazat z&droj" +msgstr[1] "Smazat %1 z&droje" +msgstr[2] "Smazat %1 z&drojů" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synchronizovat zdroj" +msgstr[1] "&Synchronizovat %1 zdroje" +msgstr[2] "&Synchronizovat %1 zdrojů" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopírovat složku" +msgstr[1] "Kopírovat %1 složky" +msgstr[2] "Kopírovat %1 složek" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopírovat položku" +msgstr[1] "Kopírovat %1 položky" +msgstr[2] "Kopírovat %1 položek" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Vyjmout položku" +msgstr[1] "Vyjmout %1 položky" +msgstr[2] "Vyjmout %1 položek" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Vyjmout složku" +msgstr[1] "Vyjmout %1 složky" +msgstr[2] "Vyjmout %1 složek" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Smazat položku" +msgstr[1] "Smazat %1 položky" +msgstr[2] "Smazat %1 položek" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Smazat složku" +msgstr[1] "Smazat %1 složky" +msgstr[2] "Smazat %1 složek" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synchronizovat složku" +msgstr[1] "Synchronizovat %1 složky" +msgstr[2] "Synchronizovat %1 složek" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Smazat zdroj" +msgstr[1] "Smazat %1 zdroje" +msgstr[2] "Smazat %1 zdrojů" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synchronizovat zdroj" +msgstr[1] "Synchronizovat %1 zdroje" +msgstr[2] "Synchronizovat %1 zdrojů" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Název" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Opravdu si přejete smazat tuto složku a všechny její podsložky?" +msgstr[1] "Opravdu si přejete smazat %1 složky a všechny jejich podsložky?" +msgstr[2] "Opravdu si přejete smazat %1 složky a všechny jejich podsložky?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Smazat složku?" +msgstr[1] "Smazat složky?" +msgstr[2] "Smazat složky?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Nelze smazat složku: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Smazání složky selhalo" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Vlastnosti složky %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Opravdu si přejete smazat zvolenou položku?" +msgstr[1] "Opravdu si přejete smazat %1 zvolené položky?" +msgstr[2] "Opravdu si přejete smazat %1 zvolených položek?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Smazat položku?" +msgstr[1] "Smazat položky?" +msgstr[2] "Smazat položky?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Nelze smazat položku: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Smazání položky selhalo" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Přejmenovat oblíbenou" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Název:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nový zdroj" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Nelze vytvořit zdroj: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Vytvoření zdroje selhalo" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Smazat zdroj?" +msgstr[1] "Smazat zdroje?" +msgstr[2] "Smazat zdroje?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Nelze vložit data: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Vložení selhalo" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Účet \"%1\" je odpojen" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Přepnout na stav připojen" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Přesunout do této složky" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Zkopírovat do této složky" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Chyba registrace" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Lokální přihlášení" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Hledat:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Pouze při&hlášené" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Zare&gistrovat" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Zrušit registraci" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Nelze vytvořit značku" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Opravdu si přejete odstranit značku %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Smazat značku" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Smazat značku" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Vyberte značky, které by měly být použity." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Vytvořit novou značku" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Spravovat značky" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Vyberte značky..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Vyberte značky" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Vyprázdnit" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Klikněte pro přidání značek" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Převodník Akonadi do XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Převádí podstrom sbírky Akonadi do souboru XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nebyla načten žádná data." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Není vybrán žádný název souboru" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Datový soubor '%1' nelze otevřít." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Soubor %1 neexistuje." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Datový soubor '%1' nelze zpracovat." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Nelze vytvořit schéma." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Neplatný formát souboru." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Datový soubor nelze zpracovat: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Nelze najít sbírku %1" diff --git a/po/da/akonadi_knut_resource.po b/po/da/akonadi_knut_resource.po new file mode 100644 index 0000000..6411044 --- /dev/null +++ b/po/da/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Martin Schlander , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-06-30 11:15+0200\n" +"Last-Translator: Martin Schlander \n" +"Language-Team: Danish \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Ingen datafil valgt." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Filen \"%1\" blev indlæst." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Vælg datafil" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut-datafil" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Intet element fundet for eksternt id %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Forælder-samling ikke fundet i DOM-træet." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Kan ikke skrive samling" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Ændret samling ikke fundet i DOM-træet." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Slettet samling ikke fundet i DOM-træet." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Forælder-samlingen \"%1\" ikke fundet i DOM-træet." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Kan ikke skrive elementet." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Ændret element ikke fundet i DOM-træet." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Slettet element ikke fundet i DOM-træet." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Sti til Knut-datafil." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Ændr ikke de faktiske motor-data." diff --git a/po/da/libakonadi5.po b/po/da/libakonadi5.po new file mode 100644 index 0000000..a636ec2 --- /dev/null +++ b/po/da/libakonadi5.po @@ -0,0 +1,2818 @@ +# translation of libakonadi.po to dansk +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Martin Schlander , 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2020. +# Jan Madsen , 2008. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2020-11-17 19:45+0100\n" +"Last-Translator: Martin Schlander \n" +"Language-Team: Danish \n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 20.04.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Martin Schlander" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "mschlander@opensuse.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Der er i øjeblikket ikke konfigureret nogen konto." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Konto-integration er ikke understøttet" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Kan ikke registrere objekt hos dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 af typen %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agent-identifikator" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Klar" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Offline" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synkroniserer..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Fejl." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Ikke konfigureret" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Ressourceidentifikator" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-ressource" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Ugyldigt element hentet" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Fejl under hentning af element: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Fejl under opdatering af samling: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Opdatering af lokal samling mislykkedes: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Opdatering af lokale elementer mislykkedes: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Kan ikke hente element i offline-tilstand." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synkroniserer mappen \"%1\"" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Kunne ikke hente samlingen til synkronisering." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Kunne ikke hente samlingen til attributsynkronisering." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Det anmodede element findes ikke længere" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Job annulleret." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Ingen sådan samling." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Fandt uløste forældreløse samlinger" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Fandt intet andet element til konflikthåndtering" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Kan ikke tilgå den oprettede agents D-Bus-grænseflade." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Oprettelse af agentinstans tidsudløb." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Kan ikke modtage agenttypen \"%1\"." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Kan ikke oprette agentinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Ugyldig samlingsinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Ugyldig ressourceinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Kan ikke nå D-Bus-grænseflade for ressourcen \"%1\"" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Tiden til synkronisering af samlingsattributter havde løb ud." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Ugyldig samling til kopiering" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Ugyldig målsamling" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Ugyldig forælder" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Kunne ikke fortolke samlingen fra svaret" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Ugyldig samling" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Ugyldig samling angivet." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Ingen objekter angivet til flytning" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Ingen gyldig destination angivet" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Ugyldig samling." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Ugyldig forældersamling" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Kan ikke forbinde til Akonadi-tjenesten" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Protokolversionen af Akonadi-serveren er ikke kompatibel. Tjek at du har en " +"kompatibel version installeret." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Bruger afbrød operation." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Ukendt fejl." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Uventet svar" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Kunne ikke oprette relation." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Ressourcesynkronisering havde tidsudløb." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Kunne ikke hente rodsamling for ressourcen %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Intet ressource-id angivet." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Ugyldig ressourceidentifikator \"%1\"" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Kunne ikke indstille standardressource via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Kunne ikke hente ressourcesamlingen." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tidsudløb under forsøg på at hente lås." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Kan ikke oprette mærke." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Flytning til papirkurvsamling mislykkedes, afbryder papirkurvsoperation" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Ugyldige elementer sendt" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Ugyldig samling sendt" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Ingen gyldig samling eller tom elementliste" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Kunne ikke finde genskabssamling og genskabsressourcen er ikke tilgængelig" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Navn" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Indlæser..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Fejl" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Målsamlingen \"%1\" indeholder allerede\n" +"en samling med navnet \"%2\"." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Navn" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Kunne ikke kopiere element: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kunne ikke kopiere samling: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Kunne ikke flytte element: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Kunne ikke flytte samling: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Kunne ikke linke enhed: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Fejl" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Favoritmapper" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Breve i alt" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Ulæste breve" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvote" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Lagerstørrelse" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Lagerstørrelse for undermappe" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Ulæste" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "I alt" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Størrelse" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Mærke" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Kan ikke hente element til indeks" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indekset er ikke længere tilgængeligt" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Ladningsdelen \"%1\" er ikke tilgængelig for dette indeks" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Ingen session tilgængelig for dette indeks" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Intet element tilgængeligt for dette indeks" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Unavngivet plugin" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Ingen beskrivelse tilgængelig" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi-serverprotokollen afviger fra den protokolversion der benyttes af " +"dette program.\n" +"Hvis du for nyligt har opdateret dit system, så log venligst ud og ind igen " +"for at sikre at alle programmer bruger den korrekte protokolversion." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Der er ingen Akonadi-agenter tilgængelige. Kontrollér din KDE PIM-" +"installation." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Uoverensstemmelse i protokolversion. Serverversionen er ældre (%1) end din " +"(%2). Hvis du har opdateret dit system for nyligt, så genstart venligst " +"Akonadi-serveren." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Uoverensstemmelse i protokolversion. Serverversionen er nyere (%1) end din " +"(%2). Hvis du har opdateret dit system for nyligt, så genstart venligst alle " +"KDE PIM-programmer." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi-selvtest" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Tjekker og rapporterer om Akonadi-serverens tilstand" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Ny agentinstans..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Slet agentinstans" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Indstil agentinstans" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Ny agentinstans" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Kunne ikke oprette agentinstans: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Oprettelse af agentinstans mislykkedes" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Vil du slette agentinstansen?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Vil du virkelig slette den markerede agentinstans?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Konfiguration af %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Håndbog til %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Om %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Konfigurationsdialogen er blevet åbnet i et andet vindue" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Konfiguration af %1 er allerede åbnet et andet sted." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Kunne ikke registrere konfigurationsdialogen til %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minut" +msgstr[1] "minutter" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Hentning" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Brug indstillinger fra overmappen eller kontoen" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synkronisér når denne mappe markeres" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synkronisér automatisk efter:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Aldrig" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutter" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokalt cachede dele" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Indstillinger for hentning" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Hent altid hele &breve" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Hent brevkroppe efter behov" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Behold brevkroppe lokalt i:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "For evigt" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Søg" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Brug mappe som standard" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Ny undermappe..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Opret en ny undermappe i den valgte mappe" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Ny mappe" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Navn" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Kunne ikke oprette mappe: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Oprettelse af mappe fejlede" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Generelt" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "1 objekt" +msgstr[1] "%1 objekter" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Navn" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Brug selvvalgt ikon:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "mappe" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistikker" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Indhold:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objekter" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Størrelse:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Bemærk at indeksering kan tage nogle minutter." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Vedligeholdelse" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Fejl under hentning af antal indekserede elementer" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 element indekseret i denne mappe" +msgstr[1] "%1 elementer indekseret i denne mappe" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Beregner antal indekserede elementer..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Filer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Mappetype:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "ukendt" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementer" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elementer i alt:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Ulæste elementer:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indeksering" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Aktivér fuldtekstindeksering" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Henter antal indekserede elementer..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Genindeksér mappe" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ingen mappe" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Åbn samlingsdialog" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Vælg en samling" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Flyt hertil" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopiér hertil" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Annullér" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Ændringstidspunkt" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flag" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Konfliktløsning" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Tag min version" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Tag deres version" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Behold begge versioner" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Dine ændringer er i konflikt med dem en anden har lavet i mellemtiden." +"
Medmindre den ene version kan smides bort, bliver du nødt til at " +"integrere ændringerne manuelt.
Klik på \"Åbn " +"teksteditor\" for at beholde en kopi af teksterne, vælg så hvilken " +"version der er mest korrekt, åbn den så igen og ændr den igen for at tilføje " +"det der mangler." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Data" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Starter Akonadi-server..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stopper Akonadi-server..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Flyt hertil" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopiér hertil" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Link hertil" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Annullér" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Kan ikke forbinde til personlig informationshåndtering-serveren (PIM).\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Tjeneste til håndtering af personlig information starter..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Tjeneste til håndtering af personlig information lukkes ned..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Tjeneste til håndtering af personlig information udfører en " +"databaseopgradering." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Tjeneste til håndtering af personlig information udfører en " +"databaseopgradering.\n" +"Dette sker efter en softwareopdatering og er nødvendig for at optimere " +"ydelsen.\n" +"Afhængigt af mængden af personlig information kan dette tage flere minutter." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Tjenesten til håndtering af personlig information, Akonadi, kører ikke. " +"Dette program kan ikke bruges uden den." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Start" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi, systemet til håndtering af personlig information (PIM), er ikke " +"funktionsdygtigt.\n" +"Tryk på \"Detaljer...\" for at få detaljeret information om dette problem." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Tjenesten til håndtering af personlig information, Akonadi, er ikke klar til " +"brug." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detaljer..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Vil du virkelig fjerne kontoen \"%1\"?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Fjern konto?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Indkommende konti (tilføj mindst en):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Tilføj..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Æ&ndr..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Fjern" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Genstart" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Nylig mappe" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Omdøb favorit" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Navn:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi-server selvtest" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Gem rapport..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopiér rapport til udklipsholderen" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"QtSQL-driveren \"%1\" kræves af din nuværende Akonadi-server-konfiguration " +"og blev ikke fundet på dit system." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"QtSQL-driveren \"%1\" kræves af din nuværende Akonadi-server-konfiguration.\n" +"Følgende drivere er installerede: %2.\n" +"Sørg for at den krævede driver er installeret." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Fandt database-driver." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Fandt ikke database-driver." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Kørbar fil for MySQL-server ikke testet." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Den nuværende konfiguration kræver ikke en intern MySQL-server." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Du har i øjeblikket konfigureret Akonadi til at bruge MySQL-serveren " +"\"%1\".\n" +"Sørg for at du har MySQL-server installeret, angiv den korrekte sti og " +"kontrollér at du har de nødvendige læse- og kørselsrettigheder på den " +"kørbare fil for serveren. Den kørbare fil hedder normalt \"mysqld\", dens " +"placering varierer afhængigt af distribution." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Fandt ikke MySQL-server." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-server kan ikke læses." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-server kan ikke køres." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL fundet med uventet navn." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Fandt MySQL-server." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Fandt MySQL-server: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-serveren er kørbar." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Kørsel af MySQL-serveren \"%1\" mislykkedes med følgende fejlmeddelelse: " +"\"%2\"" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Kørsel af MySQL-serveren mislykkedes." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Fejllog for MySQL-server ikke testet." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Ingen aktuel fejllog for MySQL fundet." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL-serveren rapporterede ikke nogen fejl under opstarten. Loggen kan " +"findes i \"%1\"." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Fejllog for MySQL kan ikke læses." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "En fejllog for MySQL-server blev fundet, men den kan ikke læses: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-serverens log indeholder fejl." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL-serverens fejllog-fil \"%1\" indeholder fejl." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-serverens log indeholder advarsler." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL-serverens logfil \"%1\" indeholder advarsler." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-serverens log indeholder ingen fejl." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL-serverens logfil \"%1\" indeholder ingen fejl eller advarsler." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Konfiguration af MySQL-server er ikke testet." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Standardkonfiguration af MySQL-server fundet." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Standardkonfiguration af MySQL-serveren blev fundet og kan læses i %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Fandt ikke standardkonfiguration af MySQL-server." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Standardkonfiguration af MySQL-serveren blev ikke fundet eller kunne ikke " +"læses. Tjek at din Akonadi-installation er komplet og at du har alle krævede " +"adgangsrettigheder." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Brugertilpasset konfiguration af MySQL-server ikke tilgængelig." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Brugertilpasset konfiguration af MySQL-serveren blev ikke fundet, men den er " +"valgfri." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Fandt brugertilpasset konfiguration af MySQL-server." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Den brugertilpassede konfiguration af MySQL-serveren blev fundet og kan " +"læses i %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Brugertilpasset konfiguration af MySQL-serveren kan ikke læses." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Den brugertilpassede konfiguration af MySQL-serveren blev fundet i %1 men " +"kan ikke læses. Tjek dine adgangsrettigheder." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Konfiguration af MySQL-server blev ikke fundet eller kunne ikke læses." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Konfiguration af MySQL-serven blev ikke fundet eller kan ikke læses." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Konfiguration af MySQL-serveren er brugbar." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Konfiguration af MySQL-serveren blev fundet i %1 og kan læses." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Kan ikke forbinde til PostgreSQL-server." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL-server fundet." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL-serveren blev fundet og forbindelsen fungerer." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "Fandt ikke akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Programmet \"akonadictl\" skal være tilgængeligt i $PATH. Sørg for at du har " +"Akonadi-serveren installeret." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "Fandt brugbar akonadictl" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Programmet \"%1\", til at styre Akonadi-serveren, blev fundet og kunne " +"køres.\n" +"Resultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "Fandt ikke-brugbar akonadictl" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Programmet \"%1\", til at styre Akonadi-serveren, blev fundet, men kunne " +"ikke køres.\n" +"Resultat:\n" +"%2 Sørg for at Akonadi-serveren er installeret korrekt." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi-kontrolproces registreret hos D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-kontrolprocessen er registreret hos D-Bus, hvilket normalt betyder " +"at den er funktionsdygtig." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi-kontrolproces ikke registreret hos D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-kontrolprocessen er ikke registreret hos D-Bus, hvilket normalt " +"betyder at den ikke blev startet eller at en fatal fejl opstod under opstart." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi-serverproces registreret hos D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-serverprocessen er registreret hos D-Bus, hvilket normalt betyder at " +"den er funktionsdygtig." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi-serverproces ikke registreret hos D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-serverprocessen er ikke registreret hos D-Bus, hvilket normalt " +"betyder at den ikke blev startet eller at en fatal fejl opstod under opstart." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Tjek af protokolversion ikke muligt." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Uden forbindelse til serveren er det ikke muligt at tjekke om " +"protokolversionen opfylder kravene." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Serverens protokolversion er for gammel." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Serverens protokolversion er %1, men klienten kræver %2. Hvis du for nylig " +"har opdateret KDE PIM, så sørg for at genstarte både Akonadi og KDE PIM-" +"programmer." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Serverens protokolversion er for ny." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Serverens protokolversion matcher." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Den aktuelle protokolversion er %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Fandt ressourceagenter." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Mindst en ressourceagent er blevet fundet." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Ingen ressourceagenter fundet." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Ingen ressourceagenter er blevet fundet. Akonadi er ikke brugbar uden mindst " +"en. Dette betyder normalt, at ingen ressourceagenter er installeret, eller " +"at der er et opsætningsproblem. Følgende stier er blevet gennemsøgt: \"%1\". " +"Miljøvariablen XDG_DATA_DIRS er sat til \"%2\", sørg for at den inkluderer " +"alle stier hvori Akonadi-agenter er installeret." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Ingen aktuel fejllog for Akonadi-server fundet." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi-serveren rapporterede ingen fejl under den aktuelle opstart." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Fandt aktuel fejllog for Akonadi-server." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-serveren rapporterede fejl under den aktuelle opstart. Loggen kan " +"findes i %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Ingen forrige-fejllog for Akonadi-server fundet." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi-serveren rapporterede ingen fejl under dens forrige opstart." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Fandt forrige-fejllog for Akonadi-server." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-serveren rapporterede fejl under den forrige opstart. Loggen kan " +"findes i %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Faindt ingen aktuel fejllog for Akonadi-kontrol." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadi-kontrolprocessen rapporterede ingen fejl under dens aktuelle opstart." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Fandt aktuelt fejllog for Akonadi-kontrol." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi-kontrolprocessen rapporterede fejl under dens aktuelle opstart. " +"Loggen kan findes i %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Fandt ingen forrige-fejllog for Akonadi-kontrol." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi-kontrolprocessen rapporterede ingen fejl under dens forrige opstart." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Fandt forrige-fejllog for Akonadi-kontrol." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi-kontrolprocessen rapporterede fejl under dens forrige opstart. " +"Loggen kan findes i %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi blev startet som root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Kørsel af programmer der er rettet mod internettet som root, udsætter dig " +"for mange sikkerhedsrisici. MySQL, som bruges af denne Akonadi-installation, " +"vil ikke lade sig blive kørt som root, for at beskytte dig mod disse risici." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi kører ikke som root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi kører ikke som root-/administratorbruger, hvilket også er den " +"anbefalede opsætning for et sikkert system." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Gem testrapport" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Fejl" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Kunne ikke åbne filen \"%1\"" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"En fejl opstod under opstart af Akonadi-serveren. Følgende selvtester skulle " +"gerne hjælpe med at finde og løse problemet. Inkludér venligst altid denne " +"rapport når du anmoder om support eller rapporterer programfejl." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaljer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Se userbase.kde.org/Akonadi for flere tips vedrørende fejlsøgning.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Ny mappe..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Ny" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Slet mappe" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Slet" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Synkronisér mappe" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synkronisér" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Ma&ppeegenskaber" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Egenskaber" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Indsæt" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Indsæt" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Håndtér lokale &abonnementer..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Håndtér lokale abonnementer" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Føj til favoritmapper" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Føj til favoritter" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Fjern fra favoritmapper" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Fjern fra favoritter" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Omdøb favorit..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Omdøb" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopiér mappe til..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopiér til" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopiér element til..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Flyt element til..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Flyt til" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Flyt mappe til..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Klip element" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Klip" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Klip mappe" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Opret ressource" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Slet ressource" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Ressourceegenskaber" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Synkronisér ressource" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Arbejd offline" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synkronisér mappe rekursivt" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synkronisér rekursivt" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Flyt mappen til papirkurv" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Flyt mappen til papirkurv" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Flyt element til papirkurv" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Flyt element til papirkurv" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Genskab mappe fra papirkurv" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Genskab mappe fra papirkurv" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Genskab element fra papirkurv" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Genskab element fra papirkurv" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Genskab samling fra papirkurv" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Genskab samling fra papirkurv" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synkronisér favoritmapper" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synkronisér favoritmapper" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synkronisér mappetræ" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopiér mappe" +msgstr[1] "&Kopiér %1 mapper" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopiér element" +msgstr[1] "&Kopiér %1 elementer" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Klip element" +msgstr[1] "&Klip %1 elementer" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Klip mappe" +msgstr[1] "&Klip %1 mapper" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Slet element" +msgstr[1] "&Slet %1 elementer" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Slet mappe" +msgstr[1] "&Slet %1 mapper" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synkronisér mappe" +msgstr[1] "&Synkronisér %1 mapper" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Slet ressource" +msgstr[1] "&Slet %1 ressourcer" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synkronisér ressource" +msgstr[1] "&Synkronisér %1 ressourcer" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopiér mappe" +msgstr[1] "Kopiér %1 mapper" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopiér element" +msgstr[1] "Kopiér %1 elementer" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Klip element" +msgstr[1] "Klip %1 elementer" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Klip mappe" +msgstr[1] "Klip %1 mapper" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Slet element" +msgstr[1] "Slet %1 elementer" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Slet mappe" +msgstr[1] "Slet %1 mapper" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synkronisér mappe" +msgstr[1] "Synkronisér %1 mapper" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Slet ressource" +msgstr[1] "Slet %1 ressourcer" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synkronisér ressource" +msgstr[1] "Synkronisér %1 ressourcer" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Navn" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Vil du virkelig slette denne mappe og alle dens undermapper?" +msgstr[1] "Vil du virkelig slette %1 mapper og alle deres undermapper?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Vil du slette mappen?" +msgstr[1] "Vil du slette mapperne?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Kunne ikke slette mappe: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Sletning af mappe fejlede" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Egenskaber for mappen %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Vil du virkelig slette det markerede element?" +msgstr[1] "Vil du virkelig slette %1 markerede elementer?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Slet element?" +msgstr[1] "Slet elementer?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Kunne ikke slette element: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Sletning af element mislykkedes" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Omdøb favorit" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Navn:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Ny ressource" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Kunne ikke oprette ressource: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Oprettelse af ressource mislykkedes" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Vil du virkelig slette denne ressource?" +msgstr[1] "Vil du virkelig slette %1 ressourcer?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Vil du slette ressourcen?" +msgstr[1] "Vil du slette ressourcerne?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Kunne ikke sætte data ind: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Indsættelse fejlede" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Vi kan ikke tilføje \"/\" i et mappenavn." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Fejl ved oprettelse af ny mappe" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Vi kan ikke tilføje \".\" ved begyndelsen eller slutningen af et mappenavn." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Før du synkroniserer mappen \"%1\" er det nødvendigt at have ressourcen " +"online. Vil du bringe den online?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Kontoen \"%1\" er offline" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Gå online" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Flyt til denne mappe" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopiér til denne mappe" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Kunne ikke opdatere abonnement: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Abonnementsfejl" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Lokale abonnementer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Søg:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Kun abonnerede" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "A&bonnér" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Af&meld" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Kunne ikke oprette et nyt mærke" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Der opstod en fejl under oprettelse af nyt mærke" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Vil du virkelig fjerne mærket %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Slet mærke" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Slet mærke" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Vælg hvilke mærker der skal anvendes." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Opret nyt mærke" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Håndtér mærker" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Slet mærker..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Vælg mærker" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Ryd" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Klik for at tilføje mærker" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi til XML-konvertering" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Konverterer et deltræ af en Akonadi-samling til en XML-fil." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Ingen data indlæst." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Intet filnavn angivet" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Kan ikke åbne datafilen \"%1\"." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Filen %1 findes ikke." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Kan ikke fortolke datafilen \"%1\"." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Skemadefinitionen kunne ikke indlæses og fortolkes." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Kan ikke oprette kontekst til skemafortolker." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Kan ikke oprette skema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Kan ikke oprette kontekst til skemavalidering." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Ugyldigt filformat." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Kan ikke fortolke datafilen: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Kan ikke finde samlingen %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "Eksternt ID" + +#~ msgid "MimeType" +#~ msgstr "Mime-type" + +#~ msgid "Default Name" +#~ msgstr "Standardnavn" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Slet" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Annullér" + +#~ msgid "Take left one" +#~ msgstr "Tag den venstre" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "To opdateringer er i konflikt med hinanden.Vælg hvilke opdateringer " +#~ "der skal anvendes." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Ulæste" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "I alt" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Størrelse" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-ressource" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Navn" + +#~ msgid "Invalid collection specified" +#~ msgstr "Ugyldig samling specificeret" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Protokolversion %1 fundet. Forventede mindst %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Serverens protokolversion er ny nok." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Serverens protokolversion er %1, hvilket er lig med eller nyere end den " +#~ "krævede version %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Inkonsistent lokalt samlingstræ detekteret." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Ekstern samling uden root-afsluttet forfaderkæde leveret, ressourcen er " +#~ "defekt." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE testprogram" + +#~ msgid "Cannot list root collection." +#~ msgstr "Kan ikke opliste rodsamling." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk søgetjeneste registreret hos D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Nepomuk søgetjeneste er registreret hos D-Bus, hvilket normalt betyder at " +#~ "den er funktionsdygtig." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk søgetjeneste ikke registreret hos D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Nepomuk søgetjeneste er ikke registreret hos D-Bus, hvilket normalt " +#~ "betyder at den ikke blev startet eller at en fatal fejl opstod under " +#~ "opstart." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuk søgetjeneste bruger uegnet motor." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Nepomuk søgetjeneste bruger motoren \"%1\", hvilket ikke anbefales til " +#~ "brug med Akonadi." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuk søgetjeneste bruger en egnet motor. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Nepomuk søgetjeneste bruger en af de anbefalede motorer." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Pluginet \"%1\" er ikke indbygget statisk, angiv venligst denne " +#~ "information i fejlrapporten." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Plugin ikke bygget statisk" + +#~ msgid "Fetch Job Error" +#~ msgstr "Fejl i hentningsjob" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Ny mappe..." + +#, fuzzy +#~| msgid "Folder &Properties" +#~ msgid "Resource Properties" +#~ msgstr "Ma&ppeegenskaber" + +#~ msgid "Cache" +#~ msgstr "Cache" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Overtag cache-politik fra forælder" + +#~ msgid "Cache Policy" +#~ msgstr "Cache-politik" + +#~ msgid "Interval check time:" +#~ msgstr "Tidsinterval for tjek:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Tidsudløb for lokal cache:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Synkronisér på forlangende" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Håndtér hvilke mapper, du vil se i mappetræet" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Søg" + +#~ msgid "Available Folders" +#~ msgstr "Tilgængelige mapper" + +#~ msgid "Current Changes" +#~ msgstr "Nuværende ændringer" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Opsig abonnement på valgt mappe" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Akonadi-serveren rapporterede fejl til %1 under opstart." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "Akonadi-kontrolprocessen rapporterede fejl til %1 under opstart." + +#~ msgid "TODO" +#~ msgstr "GØREMÅL" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi er ikke funktionsdygtig.
Detaljer...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-ressource" + +#, fuzzy +#~| msgid "no collection" +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "ingen samling" +#~ msgstr[1] "ingen samling" + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "&Kopiér mappe" diff --git a/po/de/akonadi_knut_resource.po b/po/de/akonadi_knut_resource.po new file mode 100644 index 0000000..f356dc8 --- /dev/null +++ b/po/de/akonadi_knut_resource.po @@ -0,0 +1,88 @@ +# Burkhard Lück , 2009. +# Thomas Reitelbach , 2009. +# Frederik Schwarzer , 2010, 2016. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2016-01-12 09:04+0100\n" +"Last-Translator: Frederik Schwarzer \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Keine Datendatei ausgewählt." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Datei „%1“ erfolgreich geladen" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Datendatei auswählen" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Knut-Datendatei von Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Kein Element für Remote-ID %1 gefunden" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Im DOM-Baum wurde keine übergeordnete Sammlung gefunden." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Problem beim Schreiben der Sammlung." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Im DOM-Baum wurde keine veränderte Sammlung gefunden." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Im DOM-Baum wurde keine gelöschte Sammlung gefunden." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Im DOM-Baum wurde keine übergeordnete Sammlung „%1“ gefunden." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Problem beim Schreiben des Elements." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Im DOM-Baum wurde kein verändertes Element gefunden." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Im DOM-Baum wurde kein gelöschtes Element gefunden." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Pfad zur Knut-Datendatei" + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Die tatsächlichen Treiber-Daten nicht ändern." diff --git a/po/de/libakonadi5.po b/po/de/libakonadi5.po new file mode 100644 index 0000000..ffc9219 --- /dev/null +++ b/po/de/libakonadi5.po @@ -0,0 +1,2838 @@ +# Thomas Reitelbach , 2007, 2008, 2009. +# Frederik Schwarzer , 2008, 2010, 2011, 2012, 2013, 2015, 2016, 2018, 2020. +# Burkhard Lück , 2008, 2009, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +# Johannes Obermayr , 2010. +# Intevation GmbH, 2010. +# Panagiotis Papadopoulos , 2010. +# Torbjörn Klatt , 2011. +# Rolf Eike Beer , 2012. +# Markus Slopianka , 2013. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-04-14 06:33+0200\n" +"Last-Translator: Burkhard Lück \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Thomas Reitelbach, Frederik Schwarzer" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "tr@erdfunkstelle.de, schwarzer@kde.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Es ist zur Zeit kein Zugang eingerichtet." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integration des Zugangs wird nicht unterstützt" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Das Objekt kann nicht am D-Bus registriert werden: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 vom Typ %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agent-Bezeichner" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Bereit" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Offline" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Abgleich läuft ..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Fehler." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nicht eingerichtet" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Ressourcen-Bezeichner" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-Ressource" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Ungültigen Eintrag erhalten" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Fehler beim Erstellen des Eintrags: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Aktualisierung der Sammlung fehlgeschlagen: %1." + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Aktualisierung der lokalen Sammlung fehlgeschlagen: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Aktualisierung des lokalen Eintrags ist fehlgeschlagen: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Der Eintrag kann im Offline-Modus nicht geholt werden." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Abgleich des Ordners „%1“" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Das Abholen der Sammlung zum Abgleichen ist fehlgeschlagen." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Das Abholen der Sammlung zum Abgleich der Attribute ist fehlgeschlagen." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Der angefragte Eintrag ist nicht mehr vorhanden." + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Der Auftrag wurde abgebrochen" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Es ist keine solche Sammlung vorhanden." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Nicht aufgelöste verwaiste Sammlungen gefunden." + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Es wurde kein weiterer Eintrag für die Konfliktbehandlung gefunden" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" +"Der Zugriff auf die D-Bus-Schnittstelle des erzeugten Agenten ist nicht " +"möglich." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Zeitüberschreitung beim Erstellen einer Agent-Instanz." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Der Agent-Typ „%1“ kann nicht bezogen werden." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Agent-Instanz kann nicht erstellt werden." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Ungültige Sammlungs-Instanz." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Ungültige Ressourcen-Instanz." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "D-Bus-Schnittstelle für die Ressource „%1“ kann nicht erhalten werden." + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Zeitüberschreitung beim Abgleich der Sammlungs-Attribute." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Ungültige Sammlung zum Kopieren" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Ungültige Ziel-Sammlung" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Ungültiges Eltern-Objekt" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Das Einlesen der Sammlung aus der Antwort ist fehlgeschlagen." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Ungültige Sammlung" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Ungültige Sammlung angegeben." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Keine Objekte zum Verschieben angegeben" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Kein gültiges Ziel angegeben" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Ungültige Sammlung." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Ungültige übergeordnete Sammlung" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Verbindung zum Akonadi-Dienst kann nicht hergestellt werden." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Die Protokollversion des Akonadi-Dienstes ist nicht kompatibel. Bitte " +"stellen Sie sicher, dass Sie eine kompatible Version installiert haben." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Abbruch durch Benutzer" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Unbekannter Fehler." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Unerwartete Antwort" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Erstellung der Beziehung ist fehlgeschlagen." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Zeitüberschreitung beim Abgleich der Ressource." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Die Haupt-Sammlung der Ressource „%1“ kann nicht geholt werden." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Keine Ressourcen-ID angegeben." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Ungültiger Ressourcen-Bezeichner „%1“." + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Fehler beim Einrichten der Standard-Ressource über D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Fehler beim Einholen der Ressourcen-Sammlung." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Zeitüberschreitung beim Erhalten der Sperre." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Erstellung des Stichworts ist fehlgeschlagen." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Das Verschieben in die Papierkorb-Sammlung ist fehlgeschlagen. Diese " +"Papierkorb-Aktion wird abgebrochen." + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Ungültiger Eintrag übergeben" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Ungültige Sammlung übergeben" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Keine gültige Sammlung oder leere Eintragsliste" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Sammlung zur Wiederherstellung kann nicht gefunden werden und die Ressource " +"zur Wiederherstellung ist nicht verfügbar" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Name" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Wird geladen ..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Fehler" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Die Ziel-Sammlung „%1“ enthält bereits\n" +"eine Sammlung mit dem Namen „%2“." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Name" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Eintrag lässt sich nicht kopieren: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Sammlung lässt sich nicht kopieren: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Eintrag lässt sich nicht verschieben: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Sammlung lässt sich nicht verschieben: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Entität lässt sich nicht verknüpfen: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Fehler" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Bevorzugte Ordner" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Gesamtzahl Nachrichten" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Ungelesene Nachrichten" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Speicherplatzkontingent" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Speicherplatz" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Speicherplatz der Unterordner" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Ungelesen" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Gesamt" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Größe" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Stichwort" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Eintrag für Index kann nicht geholt werden." + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Der Index ist nicht mehr verfügbar." + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Nutzdaten-Teil „%1“ ist für diesen Index nicht verfügbar." + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Für diesen Index ist keine Sitzung verfügbar." + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Für diesen Index ist kein Eintrag verfügbar." + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Unbenanntes Modul" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Keine Beschreibung verfügbar" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Die Protokollversion des Akonadi-Servers unterscheidet sich von der " +"verwendeten Protokollversion dieser Anwendung.Wenn Sie Ihr System vor kurzem " +"aktualisiert haben, melden Sie sich bitte ab- und wieder an, damit alle " +"Anwendungen die richtige Protokollversion verwenden." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Es sind keine Akonadi-Agenten verfügbar. Bitte überprüfen Sie Ihre KDE-PIM-" +"Installation." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Die Protokollversionen stimmen nicht überein. Die Serverversion (%1) ist " +"älter als Ihre Version (%2). Haben Sie Ihr System gerade aktualisiert, " +"starten Sie bitte den Akonadi-Server neu." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Die Protokollversionen stimmen nicht überein. Die Serverversion (%1) ist " +"neuer als Ihre Version (%2). Haben Sie Ihr System gerade aktualisiert, " +"starten Sie bitte alle KDE-PIM-Anwendungen neu." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi-Selbsttest" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Überprüft und berichtet den Status des Akonadi-Servers" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Neue Agent-Instanz ..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Agent-Instanz &löschen" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Agent-Instanz &konfigurieren" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Neue Agent-Instanz" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Agent-Instanz kann nicht erstellt werden: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Das Erstellen einer Agent-Instanz ist fehlgeschlagen." + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Agent-Instanz löschen?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Möchten Sie die ausgewählte Agent-Instanz wirklich löschen?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Einstellungen für %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Handbuch zu %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Über %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Der Einrichtungsdialog wurde in einem anderen Fenster geöffnet" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Die Einstellungen für %1 sind bereits anderweitig geöffnet." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Die Registrierung des Einrichtungsdialogs %1 ist fehlgeschlagen." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "Minute" +msgstr[1] "Minuten" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Abruf" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Einstellungen des Elternordners oder Postfachs verwenden" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Beim Auswählen des Ordners abgleichen" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatisch abgleichen nach:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nie" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "Minuten" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokal zwischengespeicherte Teile" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Abrufeinstellungen" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Immer vollständige &Nachrichten abrufen" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Nachrichteninhalt nach Bedarf abrufen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Nachrichteninhalt lokal vorhalten für:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Für immer" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Suchen" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Ordner als Standard verwenden" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Neuer Unterordner ..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Neuen Unterordner im aktuell ausgewählten Ordner erstellen" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Neuer Ordner" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Name" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Der Ordner kann nicht angelegt werden: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Erstellen des Ordners fehlgeschlagen" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Allgemein" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Ein Objekt" +msgstr[1] "%1 Objekte" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Name:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Eigenes &Symbol verwenden:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "Ordner" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistik" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Inhalt:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 Objekte" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Größe:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Beachten Sie, dass die Indizierung einige Minuten dauern kann." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Wartung" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Fehler beim Holen der Anzahl der indizierten Einträge" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 Eintrag in diesem Ordner indiziert" +msgstr[1] "%1 Einträge in diesem Ordner indiziert" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Indizierte Einträge werden berechnet ..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Dateien" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Ordnertyp:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "unbekannt" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Einträge" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Gesamtzahl der Einträge:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Ungelesene Einträge:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indizierung" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Volltextindizierung aktivieren" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Anzahl der indizierten Einträge wird abgeholt ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Ordner erneut indizieren" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Kein Ordner" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Sammlungs-Dialog öffnen" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Wählen Sie eine Sammlung" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Hierher &verschieben" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Hierher &kopieren" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Abbrechen" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Änderungszeit" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flaggen" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Konfliktlösung" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Meine Version wählen" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Andere Version wählen" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Beide Versionen behalten" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Ihre Änderungen stehen im Konflikt mit denen, die in der Zwischenzeit " +"von jemand anderem vorgenommen wurden.
Wenn eine Version nicht einfach " +"verworfen werden kann, müssen Sie die Änderungen manuell zusammenführen." +"
Klicken Sie auf Texteditor öffnen, um " +"eine Kopie der Texte zu behalten. Wählen Sie dann die Version, die zum " +"größeren Teil richtig ist, öffnen sie erneut und fügen den fehlenden Text " +"ein." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Daten" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi-Server wird gestartet ..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi-Server wird angehalten ..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Hierher &verschieben" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "Hierher &kopieren" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Hiermit &verknüpfen" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Abbrechen" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Verbindung zum Dienst für die persönliche Informationsverwaltung kann nicht " +"hergestellt werden.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Der Dienst zur persönlichen Informationsverwaltung wird gestartet ..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" +"Der Dienst zur persönlichen Informationsverwaltung wird heruntergefahren ..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Die Datenbank für den Dienst zur persönlichen Informationsverwaltung wird " +"aktualisiert." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Der persönliche Informationsverwaltungs-Dienst (PIM) führt gerade eine " +"Datenbankaktualisierung durch.\n" +"Dies erfolgt nach einer Software-Aktualisierung und ist für die Optimierung " +"der Leistung erforderlich.\n" +"Abhängig vom Umfang der persönlichen Informationen kann dies einige Minuten " +"dauern." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Der Akonadi-Dienst zur persönlichen Informationsverwaltung läuft nicht. " +"Diese Anwendung kann ohne ihn nicht verwendet werden." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Starten" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Das Akonadi-Framework zur persönlichen Informationsverwaltung arbeitet nicht " +"richtig.\n" +"Klicken Sie auf „Details ...“, um ausführliche Informationen zu dem Problem " +"zu erhalten." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Der Akonadi-Dienst zur persönlichen Informationsverwaltung arbeitet nicht " +"richtig." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Details ..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Möchten Sie den Zugang „%1“ wirklich entfernen?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Zugang entfernen?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Eingangspostfächer (fügen Sie mindestens eines hinzu):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Hinzufügen ..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Bearbeiten ..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Entfernen" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Neu starten" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Zuletzt benutzte Ordner" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Favoriten umbenennen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Name:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Selbsttest des Akonadi-Servers" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Bericht speichern ..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Bericht in Zwischenablage kopieren" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Der QtSQL-Treiber „%1“ wird von Ihrer aktuellen Akonadi-Serverkonfiguration " +"benötigt und wurde auch auf Ihrem System gefunden." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Der QtSQL-Treiber „%1“ wird von Ihrer aktuellen Akonadi-Serverkonfiguration " +"benötigt.\n" +"Folgende Treiber sind installiert: %2.\n" +"Bitte installieren Sie den benötigten Treiber." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Datenbank-Treiber gefunden." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Datenbank-Treiber nicht gefunden." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL-Server-Programmdatei nicht getestet." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Die aktuelle Konfiguration benötigt keinen internen MySQL-Server." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Sie haben Akonadi derzeit so eingerichtet, dass der MySQL-Server „%1“ " +"verwendet wird.\n" +"Stellen Sie sicher, dass Sie den MySQL-Server installiert haben, die Pfade " +"richtig gesetzt sind und dass Sie die notwendigen Lese- und " +"Ausführungsberechtigungen für die Programmdatei haben. Die Programmdatei " +"heißt üblicherweise „mysqld“. Ihr Speicherort ist von der Distribution " +"abhängig." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-Server nicht gefunden." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-Server nicht lesbar." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-Server nicht ausführbar." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL unter unerwartetem Namen gefunden." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL-Server gefunden." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL-Server gefunden: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-Server ist ausführbar." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Die Ausführung des MySQL-Servers „%1“ ist mit folgender Fehlermeldung " +"fehlgeschlagen: „%2“" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Die Ausführung des MySQL-Servers ist fehlgeschlagen." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Das Fehlerprotokoll des MySQL-Servers wurde nicht getestet." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Kein aktuelles MySQL-Fehlerprotokoll gefunden." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Der MySQL-Server hat bei diesem Start keine Fehler gemeldet. Das Protokoll " +"kann hier gefunden: %1" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-Fehlerprotokoll nicht lesbar." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Ein MySQL-Fehlerprotokoll wurde gefunden, kann aber nicht gelesen werden: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-Serverprotokoll enthält Fehler." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Das Fehlerprotokoll des MySQL-Servers „%1“ enthält Fehler." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-Serverprotokoll enthält Warnungen." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Das MySQL-Serverprotokoll „%1“ enthält Warnungen." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-Serverprotokoll enthält keine Fehler." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "Das MySQL-Serverprotokoll „%1“ enthält keine Fehler oder Warnungen." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-Serverkonfiguration nicht getestet." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Standard-Konfiguration des MySQL-Servers gefunden." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Die Standard-Konfiguration für den MySQL-Server wurde unter %1 gefunden und " +"ist lesbar." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Standard-Konfiguration des MySQL-Servers nicht gefunden." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Die Standard-Konfiguration des MySQL-Servers kann nicht gefunden oder " +"gelesen werden. Überprüfen Sie, ob Ihre Akonadi-Installation vollständig ist " +"und Sie die nötigen Zugriffsrechte besitzen." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Angepasste Konfiguration des MySQL-Server nicht gefunden." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Eine angepasste Konfiguration des MySQL-Server wurde nicht gefunden, sie ist " +"aber auch nicht vorgeschrieben." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Angepasste Konfiguration des MySQL-Servers gefunden." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Die angepasste Konfiguration des MySQL-Servers wurde unter %1 gefunden und " +"ist lesbar." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Angepasste Konfiguration des MySQL-Servers nicht lesbar." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Die angepasste Konfiguration des MySQL-Servers wurde unter %1 gefunden, ist " +"aber nicht lesbar. Überprüfen Sie Ihre Zugriffsrechte." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Konfiguration des MySQL-Servers nicht gefunden oder nicht lesbar." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"Die Konfiguration des MySQL-Servers wurde nicht gefunden oder ist nicht " +"lesbar." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Konfiguration des MySQL-Servers ist verwendbar." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"Die Konfiguration des MySQL-Servers wurde unter %1 gefunden und ist lesbar." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Verbindung zum PostgreSQL-Dienst kann nicht hergestellt werden." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL-Server gefunden." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Der PostgreSQL-Server wurde gefunden und die Verbindung funktioniert." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl nicht gefunden" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Das Programm „akonadictl“ muss in Ihrem $PATH liegen. Stellen Sie sicher, " +"dass Sie den Akonadi-Server installiert haben." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl gefunden und verwendbar." + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Das Programm „%1“ zur Steuerung des Akonadi-Servers wurde gefunden und kann " +"erfolgreich ausgeführt werden.\n" +"Ergebnis:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl gefunden aber nicht verwendbar." + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Das Programm „%1“ zur Steuerung des Akonadi-Servers wurde gefunden, kann " +"aber nicht erfolgreich ausgeführt werden.\n" +"Ergebnis:\n" +"%2\n" +"Stellen Sie sicher, dass der Akonadi-Server richtig installiert ist." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi-Steuerprogramm am D-Bus registriert." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Das Akonadi-Steuerprogramm ist am D-Bus registriert, was normalerweise " +"bedeutet, dass es funktionsfähig ist." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi-Steuerprogramm nicht am D-Bus registriert." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Das Akonadi-Steuerprogramm ist nicht am D-Bus registriert, was normalerweise " +"bedeutet, dass es nicht gestartet wurde oder beim Start ein schwerer Fehler " +"aufgetreten ist." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi-Serverprogramm am D-Bus registriert." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Das Akonadi-Serverprogramm ist am D-Bus registriert, was normalerweise " +"bedeutet, dass es funktionsfähig ist." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi-Serverprogramm nicht am D-Bus registriert." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Das Akonadi-Serverprogramm ist nicht am D-Bus registriert, was normalerweise " +"bedeutet, dass es nicht gestartet wurde oder beim Start ein schwerer Fehler " +"aufgetreten ist." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Überprüfung der Protokollversion nicht möglich." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Ohne eine Verbindung zum Server ist es nicht möglich zu prüfen, ob die " +"Protokollversion den Anforderungen entspricht." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Version des Server-Protokolls zu alt." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Die Version des Server-Protokolls ist %1, es ist aber mindestens Version %2 " +"durch das Anwendungsprogrammerforderlich. Haben Sie KDE-PIM gerade " +"aktualisiert, starten Sie bitte sowohl den Akonadi-Server als auch die KDE-" +"PIM-Anwendungen neu.." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Version des Server-Protokolls zu neu." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Die Version des Server-Protokolls passt." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Die aktuelle Version des Protokolls ist %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Ressourcen-Vermittler gefunden." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Es wurde mindestens ein Ressourcen-Vermittler gefunden." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Keine Ressourcen-Vermittler gefunden." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Es können keine Ressourcen-Vermittler gefunden werden. Akonadi ist nicht " +"verwendbar, wenn nicht mindestens einer verfügbar ist. Das bedeutet " +"normalerweise, dass keine Ressourcen-Vermittler installiert sind oder ein " +"Einrichtungsproblem vorliegt. Folgende Pfade wurden durchsucht: „%1“. Die " +"Umgebungsvariable XDG_DATA_DIRS ist auf „%2“ gesetzt. Überprüfen Sie, ob " +"darin alle Pfade mit installierten Akonadi-Vermittlern enthalten sind." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Kein aktuelles Fehlerprotokoll des Akonadi-Servers gefunden." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Der Akonadi-Server hat während des aktuellen Starts keine Fehler gemeldet." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Aktuelles Fehlerprotokoll des Akonadi-Servers gefunden." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Der Akonadi-Server hat während des aktuellen Starts Fehler gemeldet. Das " +"Protokoll kann hier gefunden werden: %1" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Kein früheres Fehlerprotokoll des Akonadi-Servers gefunden." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Der Akonadi-Server hat beim vorherigen Start keine Fehler gemeldet." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Früheres Fehlerprotokoll des Akonadi-Servers gefunden." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Der Akonadi-Server hat während des letzten Starts Fehler gemeldet. Das " +"Protokoll kann hier gefunden werden: %1" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Kein aktuelles Fehlerprotokoll des Akonadi-Steuerprogramms gefunden." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Das Akonadi-Steuerprogramm hat während des aktuellen Starts keine Fehler " +"gemeldet." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Aktuelles Fehlerprotokoll des Akonadi-Steuerprogramms gefunden." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Das Akonadi-Steuerprogramm hat während des aktuellen Starts Fehler gemeldet. " +"Das Protokoll kann hier gefunden werden: %1" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Kein früheres Fehlerprotokoll des Akonadi-Steuerprogramms gefunden." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Das Akonadi-Steuerprogramm hat beim vorherigen Start keine Fehler gemeldet." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Früheres Fehlerprotokoll des Akonadi-Steuerprogramms gefunden." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Das Akonadi-Steuerprogramm hat während des letzten Starts Fehler gemeldet. " +"Das Protokoll kann hier gefunden werden: %1" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi wurde als „root“ gestartet." + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Das Ausführen von mit dem Internet verbundenen Anwendungen als „root“ bzw. " +"Administrator verursacht erhebliche Sicherheitsrisiken. Das vom " +"installierten Akonadi verwendete MySQL erlaubt selbst kein Ausführen als " +"„root“, um diese Risiken auszuschließen." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi läuft nicht als „root“." + +# zweideutigkeit... +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi wird nicht als „root“ bzw. Administrator ausgeführt. Es nicht als " +"„root“ auszuführen ist die empfohlene Einstellung für ein sicheres System." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Testbericht speichern" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Fehler" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Datei „%1“ kann nicht geöffnet werden" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Beim Start des Akonadi-Servers ist ein Fehler aufgetreten. Die folgenden " +"Selbsttests sollen dabei helfen das Problem einzugrenzen und zu beheben. " +"Wenn Sie Unterstützung erfragen oder einen Fehlerbericht schreiben, geben " +"Sie bitte immer diesen Bericht mit an." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Details" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Weitere Hilfe bei Problemen finden Sie auf userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Neuer Ordner ..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Neu" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Ordner &löschen" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Löschen" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "Ordner &abgleichen" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Abgleichen" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Ordner-&Eigenschaften" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Eigenschaften" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "E&infügen" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Einfügen" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "&Lokale Abonnements verwalten" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Lokale Abonnements verwalten" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Zu bevorzugten Ordnern hinzufügen" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Zu Favoriten hinzufügen" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Aus bevorzugten Ordnern entfernen" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Aus Favoriten entfernen" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Bevorzugten Ordner umbenennen ..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Umbenennen" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Ordner kopieren nach ..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopieren nach" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Eintrag kopieren nach ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Eintrag verschieben nach ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Verschieben nach" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Ordner verschieben nach ..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Eintrag &ausschneiden" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Ausschneiden" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Ordner &ausschneiden" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Ressource erstellen" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Ressource löschen" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Ressourcen-Eigenschaften" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Ressource abgleichen" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Offline arbeiten" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Ordner rekursiv &abgleichen" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Rekursiv abgleichen" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Ordner in den &Papierkorb verschieben" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Ordner in den Papierkorb verschieben" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Eintrag in den &Papierkorb verschieben" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Eintrag in den Papierkorb verschieben" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Ordner aus dem Papierkorb &wiederherstellen" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Ordner aus dem Papierkorb wiederherstellen" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Eintrag aus dem Papierkorb &wiederherstellen" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Eintrag aus dem Papierkorb wiederherstellen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Sammlung aus dem Papierkorb &wiederherstellen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Sammlung aus dem Papierkorb wiederherstellen" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Bevorzugte Ordner &abgleichen" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Bevorzugte Ordner abgleichen" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Ordnerbaum abgleichen" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Ordner &kopieren" +msgstr[1] "%1 Ordner &kopieren" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Eintrag &kopieren" +msgstr[1] "%1 Eintrage &kopieren" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Eintrag &ausschneiden" +msgstr[1] "%1 Einträge &ausschneiden" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Ordner &ausschneiden" +msgstr[1] "%1 Ordner &ausschneiden" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Eintrag &löschen" +msgstr[1] "%1 Einträge &löschen" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Ordner &löschen" +msgstr[1] "%1 Ordner &löschen" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Ordner &abgleichen" +msgstr[1] "%1 Ordner &abgleichen" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Resource &löschen" +msgstr[1] "%1 Ressourcen &löschen" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Ressource &abgleichen" +msgstr[1] "%1 Ressourcen &abgleichen" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Ordner kopieren" +msgstr[1] "%1 Ordner kopieren" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Eintrag kopieren" +msgstr[1] "%1 Einträge kopieren" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Eintrag ausschneiden" +msgstr[1] "%1 Einträge ausschneiden" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Ordner ausschneiden" +msgstr[1] "%1 Ordner ausschneiden" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Eintrag löschen" +msgstr[1] "%1 Einträge löschen" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Ordner löschen" +msgstr[1] "%1 Ordner löschen" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Ordner abgleichen" +msgstr[1] "%1 Ordner abgleichen" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Ressource löschen" +msgstr[1] "%1 Ressourcen löschen" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Ressource abgleichen" +msgstr[1] "%1 Ressourcen abgleichen" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Name" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Möchten Sie den Ordner und alle Unterordner wirklich löschen?" +msgstr[1] "Möchten Sie %1 Ordner und all deren Unterordner wirklich löschen?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Ordner löschen?" +msgstr[1] "Ordner löschen?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Der Ordner kann nicht gelöscht werden: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Löschen des Ordners fehlgeschlagen" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Eigenschaften des Ordners %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Möchten Sie den ausgewählten Eintrag wirklich löschen?" +msgstr[1] "Möchten Sie diese %1 Einträge wirklich löschen?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Eintrag löschen?" +msgstr[1] "Einträge löschen?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Der Eintrag kann nicht gelöscht werden: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Löschen des Eintrags fehlgeschlagen" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Favoriten umbenennen" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Name:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Neue Ressource" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Ressource kann nicht erstellt werden: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Erstellen der Ressource fehlgeschlagen" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Möchten Sie die Ressource wirklich entfernen?" +msgstr[1] "Möchten Sie diese %1 Ressourcen wirklich entfernen?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Ressource löschen?" +msgstr[1] "Ressourcen löschen?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Die Daten können nicht eingefügt werden: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Einfügen fehlgeschlagen" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Das „/“-Zeichen kann dem Ordnernamen nicht hinzugefügt werden." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Fehler beim Erstellen eines neuen Ordners" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Das „.“-Zeichen kann nicht am Anfang oder Ende des Ordnernamens hinzugefügt " +"werden." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Um den Ordner „%1“ abgleichen zu können, muss die Ressource online sein. " +"Möchten Sie sie jetzt online schalten?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Zugang „%1“ ist offline" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Online gehen" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "In diesen Ordner verschieben" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "In diesen Ordner kopieren" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Aktualisierung des Abonnements fehlgeschlagen: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Abonnement-Fehler" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Lokale Abonnements" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Suche:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Nur Abonnierte" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Abonnieren" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Abonnement &kündigen" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Erstellen des neuen Stichworts ist fehlgeschlagen" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Bei der Erstellung eines neuen Stichworts ist ein Fehler aufgetreten" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Möchten Sie das Stichwort %1 wirklich entfernen?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Stichwort löschen" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Stichwort löschen" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Wählen Sie Stichwörter, die angewendet werden sollen." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Neues Stichwort erstellen" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Stichwörter verwalten" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Stichwörter auswählen ..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Stichwörter auswählen" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Leeren" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Klicken, um Stichwörter hinzuzufügen" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Umwandlung von Akonadi zu XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Wandelt den Unterbaum einer Akonadi-Sammlung in eine XML-Datei um." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Es wurden keine Daten geladen." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Kein Dateiname angegeben" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Die Datei „%1“ kann nicht geöffnet werden." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Datei %1 existiert nicht." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Die Datei „%1“ kann nicht verarbeitet werden." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Die Schemadefinition kann nicht geladen und verarbeitet werden." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Der Kontext für die Analyse des Schemas kann nicht erstellt werden." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Das Schema kann nicht erstellt werden." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" +"Der Kontext für die Prüfung der Gültigkeit des Schemas kann nicht erstellt " +"werden." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Ungültiges Dateiformat." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Datei kann nicht verarbeitet werden: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Die Sammlung %1 wurde nicht gefunden" + +#~ msgid "Id" +#~ msgstr "Kennung" + +#~ msgid "Remote Id" +#~ msgstr "Entfernte Kennung" + +#~ msgid "MimeType" +#~ msgstr "MIME-Typ" + +#~ msgid "Form" +#~ msgstr "Form" + +#~ msgid "Default Name" +#~ msgstr "Standardname" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Löschen" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Abbrechen" + +#~ msgid "Take left one" +#~ msgstr "Linke Version wählen" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Zwei Aktualisierungen stehen miteinander in Konflikt.Bitte wählen " +#~ "Sie aus, welche angewendet werden soll." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Ungelesen" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Gesamt" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Größe" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-Ressource" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Name" + +#~ msgid "Invalid collection specified" +#~ msgstr "Ungültige Collection angegeben" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "" +#~ "Protokollversion %1 wurde gefunden, erwartet wurde jedoch mindestens %2." + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Version des Server-Protokolls aktuell genug." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Die Version des Server-Protokolls ist %1, ist also neuer als oder " +#~ "entspricht der erforderlichen Version %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Inkonsistenter lokaler Collection-Baum erkannt." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Es wurde eine Netzwerk-Collection ohne root-terminierte Stammkette " +#~ "angegeben. Die Ressource ist ungültig." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE-Test-Programm" + +#~ msgid "Cannot list root collection." +#~ msgstr "Haupt-Collection kann nicht aufgelistet werden." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk-Suchdienst am D-Bus registriert." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Der Nepomuk-Suchdienst ist am D-Bus registriert, was normalerweise " +#~ "bedeutet, dass er funktionsfähig ist." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk-Suchdienst nicht am D-Bus registriert." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Der Nepomuk-Suchdienst ist nicht am D-Bus registriert, was normalerweise " +#~ "bedeutet, dass er nicht gestartet wurde oder beim Start ein schwerer " +#~ "Fehler aufgetreten ist." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Der Nepomuk-Suchdienst verwendet einen ungeeigneten Treiber." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Der Nepomuk-Suchdienst verwendet den Treiber „%1“, der nicht für die " +#~ "Verwendung mit Akonadi empfohlen wird." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Der Nepomuk-Suchdienst verwendet einen geeigneten Treiber." + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Der Nepomuk-Suchdienst verwendet einen der empfohlenen Treiber." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Das Modul „%1“ ist nicht statisch eingebunden. Bitte geben Sie diese " +#~ "Information im Fehlerbericht an." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Das Modul ist nicht statisch eingebunden" + +#~ msgid "Fetch Job Error" +#~ msgstr "Fehler beim Abholen" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Neuer Ordner ..." + +#, fuzzy +#~| msgid "&Resource Properties" +#~ msgid "Resource Properties" +#~ msgstr "&Ressourcen-Eigenschaften" + +#~ msgid "Cache" +#~ msgstr "Zwischenspeicher" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Zwischenspeicherstrategie vom Elternelement übernehmen" + +#~ msgid "Cache Policy" +#~ msgstr "Zwischenspeicherstrategie" + +#~ msgid "Interval check time:" +#~ msgstr "Prüfintervall:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Zeitüberschreitung für lokalen Zwischenspeicher:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Auf Wunsch synchronisieren" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "" +#~ "Legen Sie fest, welche Ordner in der Ordneransicht erscheinen sollen" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Suchen" + +#~ msgid "Available Folders" +#~ msgstr "Verfügbare Ordner" + +#~ msgid "Current Changes" +#~ msgstr "Aktuelle Änderungen" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Abonnement für ausgewählten Ordner aufheben" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "" +#~ "Der Akonadi-Server hat seit dem letzten Start Fehler nach %1 gemeldet." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Das Akonadi-Steuerprogramm hat seit dem letzten Start Fehler nach %1 " +#~ "gemeldet." + +#~ msgid "TODO" +#~ msgstr "NOCH ZU ERLEDIGEN" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi arbeitet nicht richtig.
Details ...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Ressource für Akonadi" + +#, fuzzy +#~| msgid "no collection" +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "Keine Sammlung" +#~ msgstr[1] "Keine Sammlung" + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "Ordner &kopieren" + +#~ msgid "TextLabel" +#~ msgstr "TextLabel" diff --git a/po/el/akonadi_knut_resource.po b/po/el/akonadi_knut_resource.po new file mode 100644 index 0000000..f85e20a --- /dev/null +++ b/po/el/akonadi_knut_resource.po @@ -0,0 +1,91 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Toussis Manolis , 2009. +# Giorgos Katsikatsos , 2010. +# Stelios , 2011. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-01-18 20:14+0100\n" +"Last-Translator: Stelios \n" +"Language-Team: Greek \n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Δεν επιλέχθηκε αρχείο δεδομένων." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Το αρχείο '%1' φορτώθηκε με επιτυχία." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Επιλογή αρχείου δεδομένων" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Αρχείο δεδομένων Knut του Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Δεν βρέθηκε αντικείμενο αναγνωριστικού απομακρυσμένης σύνδεσης %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Δεν βρέθηκε ιεραρχικά ανώτερη συλλογή στη δενδρική δομή DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Αδυναμία εγγραφής συλλογής." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Δεν βρέθηκε τροποποιημένη συλλογή στη δενδρική δομή DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Δεν βρέθηκε διαγραμμένη συλλογή στη δενδρική δομή DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Δεν βρέθηκε ιεραρχικά ανώτερη συλλογή '%1' στη δενδρική δομή DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Αδυναμία εγγραφής αντικειμένου." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Δεν βρέθηκε τροποποιημένο αντικείμενο στη δενδρική δομή DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Δεν βρέθηκε διαγραμμένο αντικείμενο στη δενδρική δομή DOM." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Διαδρομή αρχείου δεδομένων Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Να μην αλλαχθούν τα δεδομένα του συστήματος υποστήριξης." diff --git a/po/el/libakonadi5.po b/po/el/libakonadi5.po new file mode 100644 index 0000000..2ab4a14 --- /dev/null +++ b/po/el/libakonadi5.po @@ -0,0 +1,2813 @@ +# translation of libakonadi.po to Greek +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Spiros Georgaras , 2007, 2008. +# Toussis Manolis , 2007, 2008, 2009. +# Spiros Georgaras , 2008. +# Dimitrios Glentadakis , 2011. +# Stelios , 2021. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-07-25 18:16+0300\n" +"Last-Translator: Stelios \n" +"Language-Team: Greek \n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.04.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Τούsης Μανώλης" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "manolis@koppermind.homelinux.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Προς το παρόν δεν υπάρχει διαμορφωμένος λογαριασμός" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Η ενσωμάτωση λογαριασμών δεν υποστηρίζεται" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Αδυναμία εγγραφής αντικειμένου στο dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 τύπου %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Αναγνωριστικό πράκτορα" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Πράκτορας του Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Έτοιμο" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Χωρίς σύνδεση" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Συγχρονισμός..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Σφάλμα." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Δεν έχει διαμορφωθεί" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Αναγνωριστικό πόρου" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Πόρος του Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Ανακτήθηκε μη έγκυρο αντικείμενο" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Σφάλμα κατά τη δημιουργία αντικειμένου: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Σφάλμα κατά την ενημέρωση της συλλογής: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Αποτυχία ενημέρωσης τοπικής συλλογής: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Αποτυχία ενημέρωσης τοπικών αντικειμένων: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Αδυναμία λήψης του αντικειμένου σε λειτουργία χωρίς σύνδεση." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Συγχρονισμός φακέλου '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Αποτυχία ανάκτησης συλλογής για συγχρονισμό." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Αποτυχία ανάκτησης συλλογής για συγχρονισμό ιδιοτήτων." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Το ζητούμενο αντικείμενο δεν υπάρχει πλέον." + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Η εργασία ακυρώθηκε." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Δεν υπάρχει τέτοια συλλογή." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Βρέθηκαν ανεπίλυτες ορφανές συλλογές" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Δεν βρέθηκε άλλο αντικείμενο για χειρισμό της σύγκρουσης" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Αδυναμία πρόσβασης στη D-Bus διεπαφή του δημιουργημένου πράκτορα." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Λήξη χρονικού ορίου δημιουργίας πελάτη." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Αδυναμία απόκτησης πράκτορα τύπου '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Αδυναμία δημιουργίας αντιγράφου πράκτορα." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Μη έγκυρο στιγμιότυπο συλλογής." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Μη έγκυρο στιγμιότυπο πόρου." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Αδυναμία απόκτησης D-Bus διεπαφής για τον πόρο '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Λήξη χρονικού ορίου για τον συγχρονισμό των ιδιοτήτων της συλλογής." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Μη έγκυρη συλλογή για αντιγραφή" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Μη έγκυρη συλλογή προορισμού" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Μη έγκυρος γονέας" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Αποτυχία ανάλυσης συλλογής από απόκριση" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Μη έγκυρη συλλογή" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Δόθηκε μη έγκυρη συλλογή." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Δεν ορίσθηκαν αντικείμενα για μεταφορά" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Δεν καθορίστηκε έγκυρος προορισμός" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Μη έγκυρη συλλογή." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Μη έγκυρη συλλογή γονέα" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Αδυναμία σύνδεσης στην υπηρεσία Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Η έκδοση του πρωτοκόλλου του εξυπηρετητή Akonadi δεν είναι συμβατή. " +"Σιγουρευτείτε ότι έχετε εγκατεστημένη μια συμβατή έκδοση." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Η λειτουργία ακυρώθηκε από το χρήστη." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Άγνωστο σφάλμα." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Απρόσμενη απάντηση" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Αποτυχία δημιουργίας σχέσης." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Λήξη χρονικού ορίου για τον συγχρονισμό πόρου." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Αδυναμία προσκόμισης της ριζικής συλλογής του πόρου %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Δεν δόθηκε αναγνωριστικό πόρου." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Μη έγκυρο αναγνωριστικό πόρου '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Αποτυχία διαμόρφωσης του προκαθορισμένου πόρου μέσω D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Αποτυχία προσκόμισης της συλλογής πόρων." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Τέλος χρόνου στην προσπάθεια αντησης κλειδώματος." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Αδυναμία δημιουργίας ετικέτας." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Αποτυχία μετακίνησης στη συλλογή απορριμμάτων, εγκατάλειψη αποκομιδής" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Πέρασαν μη έγκυρα αντικείμενα" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Πέρασε μη έγκυρη συλλογή" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Μη έγκυρη συλλογή ή λίστα αντικειμένων κενή" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Αδυναμία εύρεσης συλλογής αποκατάστασης και ο πόρος αποκατάστασης δεν είναι " +"διαθέσιμος" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Όνομα" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Φόρτωση..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Σφάλμα" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Η συλλογή προορισμού '%1' περιέχει ήδη\n" +"μια συλλογή με όνομα '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Όνομα" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Αδυναμία αντιγραφής αντικειμένου: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Αδυναμια αντιγραφής συλλογής: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Αδυναμία μετακίνησης αντικειμένου: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Αδυναμία μετακίνησης συλλογής: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Αδυναμία σύνδεσης οντότητας: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Σφάλμα" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Προτιμώμενοι φάκελοι" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Σύνολο μηνυμάτων" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Μη αναγνωσμένα μηνύματα" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Ποσοστό χρήσης" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Μέγεθος αποθηκευτικού χώρου" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Μέγεθος αποθηκευτικού χώρου υποφακέλου" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Μη αναγνωσμένα" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Σύνολο" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Μέγεθος" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Ετικέτα" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Αδυναμία προσκόμισης αντικειμένου για το ευρετήριο" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Το ευρετήριο δεν είναι πλέον διαθέσιμο" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Το τμήμα '%1' του φορτίου δεν είναι διαθέσιμο για αυτό το ευρετήριο" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Μη διαθέσιμη συνεδρία για αυτό το ευρετήριο" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Μη διαθέσιμο αντικείμενο για αυτό το ευρετήριο" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Πρόσθετο χωρίς όνομα" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Μη διαθέσιμη περιγραφή" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Η έκδοση του πρωτοκόλλου του εξυπηρετητή Akonadi διαφέρει από την έκδοση " +"πρωτοκόλλου που χρησιμοποιεί αυτή η εφαρμογή.\n" +"Αν έχετε ενημερώσει το σύστημά σας πρόσφατα αποσυνδεθείτε και συνδεθείτε " +"πάλι για να βεβαιωθείτε ότι όλες οι εφαρμογές χρησιμοποιούν τη σωστή έκδοση " +"του πρωτοκόλλου." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Δεν υπάρχουν διαθέσιμοι πράκτορες Akonadi. Επιβεβαιώστε ότι το KDE PIM έχει " +"εγκατασταθεί." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Ασυμφωνία εκδόσεων πρωτοκόλλου. Η έκδοση του εξυπηρετητή είναι παλαιότερη " +"(%1) από τη δική μας (%2). Αν έχετε ενημερώσει το σύστημά σας πρόσφατα " +"επανεκκινήστε τον εξυπηρετητή Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Ασυμφωνία εκδόσεων πρωτοκόλλου. Η έκδοση του εξυπηρετητή είναι νεότερη (%1) " +"από τη δική μας (%2). Αν έχετε ενημερώσει το σύστημά σας πρόσφατα " +"επανεκκινήστε όλες τις KDE PIM εφαρμογές." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Αυτοέλεγχος Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Ελέγχει και αναφέρει την κατάσταση του εξυπηρετητή Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Νέος πράκτορας..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Διαγραφή πράκτορα" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Διαμόρφωση πράκτορα" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Νέος πράκτορας" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Αδυναμία δημιουργίας πράκτορα: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Αποτυχία δημιουργίας πράκτορα" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Να διαγραφεί η διεργασία πράκτορα;" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Θέλετε πράγματι να διαγραφεί η επιλεγμένη διεργασία πράκτορα;" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 διαμόρφωση" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 εγχειρίδιο" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Σχετικά %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Ο διάλογος διαμόρφωσης έχει ανοίξει σε άλλο παράθυρο" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Η διαμόρφωση για το %1 είναι ήδη ανοιχτή κάπου αλλού." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Αποτυχία εγγραφής %1 διαλόγου διαμόρφωσης." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "λεπτό" +msgstr[1] "λεπτά" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Ανάκτηση" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Να γίνει χρήση επιλογών από φάκελο γονέα ή από το λογαριασμό" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Να γίνεται συγχρονισμός όταν επιλέγεται αυτός ο φάκελος" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, fuzzy, kde-format +msgid "Automatically synchronize after:" +msgstr "Συγχρονισμός μετά από:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Ποτέ" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "λεπτά" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Τοπικά τμήματα στη λανθάνουσα" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, fuzzy, kde-format +msgid "Retrieval Options" +msgstr "Επιλογές ανάκτησης:" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Πάντα να ανκτώνται πλήρη &μηνύματα" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Ανάκτηση πε&ριεχομένου μηνυμάτων αν ζητηθεί" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, fuzzy, kde-format +msgid "Keep message bodies locally for:" +msgstr "Ανάκτηση πε&ριεχομένου μηνυμάτων αν ζητηθεί" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Για πάντα" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Αναζήτηση" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Προκαθορισμένη χρήση φακέλου" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Νέος υποφάκελος..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Δημιουργία νέου υποφακέλου κάτω από τον τρέχοντα επιλεγμένο φάκελο" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Νέος φάκελος" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Όνομα" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Αδύνατη η δημιουργία του φακέλου: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Η δημιουργία του φακέλου απέτυχε" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Γενικά" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Ένα αντικείμενο" +msgstr[1] "%1 αντικείμενα" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "Ό&νομα:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Προσαρμοσμένο εικονίδιο:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "φάκελος" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Στατιστικά" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Περιεχόμενο:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 αντικείμενα" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Μέγεθος:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Θυμηθείτε ότι η δικτοδότηση απαιτεί κάποιο χρόνο." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Συντήρηση" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Σφάλμα κατά την ανάκτηση του πλήθους των δεικτοδοτημένων αντικειμένων" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Δεικτοδοτήθηκε %1 αντιμείμενο σε αυτόν το φάκελο" +msgstr[1] "Δεικτοδοτήθηκαν %1 αντιμείμενα σε αυτόν το φάκελο" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Υπολογισμός δεικτοδοτημένων αντικειμένων..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, fuzzy, kde-format +msgid "Files" +msgstr "Μέγεθος φακέλου:" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Τύπος φακέλου:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "άγνωστο" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +msgid "Items" +msgstr "&Αντιγραφή αντικειμένου" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Σύνολο αντικειμένων:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Μη αναγνωσμένα αντικείμενα:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, fuzzy, kde-format +msgid "Indexing" +msgstr "Δεικτοδότηση:" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Ενεργοποίηση δεικτοδότησης για πλήρες κείμενο" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, fuzzy, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Ανάκτηση πλήθους δεικτοδοτημένων αντικειμένων..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Αναδεικτοδότηση φακέλου" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Κανείς φάκελος" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Άνοιγμα διαλόγου συλλογής" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Επιλογή συλλογής" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Μετακίνηση εδώ" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Αντιγραφή εδώ" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Ακύρωση" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Χρόνος τροποποίησης" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Σημαίες" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Ιδιότητα: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Επίλυση σύγκρουσης" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Με τη δική μου έκδοση" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Με τη δική τους έκδοση" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Να διατηρηθούν και οι δύο εκδόσεις" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Οι αλλαγές σας είναι σε σύγκρουση με εκείνες που έκανε στο μεταξύ " +"κάποιος άλλος.
Εκτός εάν μια έκδοση μπορεί απλώς να απορριφθεί, θα " +"χρειαστεί να ενσωματώσετε τις αλλαγές αυτές χειροκίνητα.
Με κλικ στο«Άνοιγμα επεξεργαστή κειμένου» διατηρείτε ένα " +"αντίγραφο των κειμένων και έπειτα επιλέγετε ποια έκδοση είναι η σωστότερη " +"και έπειτα την ανοίγετε και την τροποποιείτε ξανά για να προσθέσετε ό,τι " +"λείπει." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Δεδομένα" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Εκκίνηση εξυπηρετητή Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Διακοπή εξυπηρετητή Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Μετακίνηση εδώ" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Αντιγραφή εδώ" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Διασύνδεση εδώ" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Ακύρωση" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Αδυναμία σύνδεσης στην υπηρεσία διαχείρισης προσωπικών πληροφοριών.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Η υπηρεσία διαχείρισης προσωπικών πληροφοριών ξεκινά..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Η υπηρεσία διαχείρισης προσωπικών πληροφοριών τερματίζεται..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Η υπηρεσία διαχείρισης προσωπικών πληροφοριών αναβαθμίζει τη βάση δεδομένων." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Η υπηρεσία διαχείρισης προσωπικών πληροφοριών αναβαθμίζει τη βάση " +"δεδομένων.\n" +"Αυτό συμβαίνει μετά από μια αναβάθμιση λογισμικού και είναι απαραίτητο για " +"τη βελτιστοποίηση των επιδόσεων.\n" +"Ανάλογα με τον όγκο των προσωπικών πληροφοριών, μπορεί να διαρκέσει κάποια " +"λεπτά." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Η υπηρεσία διαχείρισης προσωπικών πληροφοριών Akonadi δεν εκτελείται και " +"έτσι η εφαρμογή αυτή δεν θα μπορεί να χρησιμοποιηθεί." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Εκκίνηση" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Το σύστημα διαχείρισης προσωπικών πληροφοριών Akonadi δε λειτουργεί.\n" +"
Κάντε κλικ στις \"Λεπτομέρειες...\" για πληροφορίες σχετικά με αυτό το " +"πρόβλημα." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Η υπηρεσία διαχείρισης προσωπικών πληροφοριών Akonadi δε λειτουργεί." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Λεπτομέρειες..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Θέλετε πράγματι να διαγραφεί ο λογαριασμός '%1';" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Να διαγραφεί ο λογαριασμός;" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Εισερχόμενοι λογαριασμοί (προσθέστε τουλάχιστον έναν):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "Προσ&θήκη..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Τροποποίηση..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "Δ&ιαγραφή" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Επανεκκίνηση" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Πρόσφατος φάκελος" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Μετονομασία προτιμώμενου" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Όνομα:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Αυτοέλεγχος εξυπηρετητή Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Αποθήκευση αναφοράς..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Αντιγραφή αναφοράς στο πρόχειρο" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Ο οδηγός QtSQL '%1' απαιτείται από την τρέχουσα διαμόρφωση του εξυπηρετητή " +"Akonadi και βρέθηκε στο σύστημά σας." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Ο οδηγός QtSQL '%1' απαιτείται από τις τρέχουσες ρυθμίσεις του εξυπηρετητή " +"Akonadi.\n" +"Οι παρακάτω οδηγοί είναι εγκατεστημένοι: %2.\n" +"Σιγουρευτείτε ότι ο απαιτούμενος οδηγός είναι εγκατεστημένος." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Βρέθηκε οδηγός βάσης δεδομένων." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Δε βρέθηκε οδηγός βάσης δεδομένων." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Το εκτελέσιμο του εξυπηρετητή MySQL δεν ελέγχθηκε." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Οι τρέχουσες ρυθμίσεις δεν απαιτούν έναν εσωτερικό εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Έχετε διαμορφώσει τον Akonadi να χρησιμοποιεί τον εξυπηρετητή MySQL '%1'.\n" +"Βεβαιωθείτε ότι ο εξυπηρετητής MySQL είναι εγκατεστημένος, ορίστε τη σωστή " +"διαδρομή και ότι έχετε τα απαραίτητα δικαιώματα ανάγνωσης και εκτέλεσης στο " +"εκτελέσιμο του εξυπηρετητή. Το εκτελέσιμο συνήθως ονομάζεται 'mysqld', με τη " +"θέση του να διαφέρει ανάλογα με τη διανομή." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Δε βρέθηκε ο εξυπηρετητής MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Αδύνατη η ανάγνωση του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Μη εκτελέσιμος ο εξυπηρετητής MySQL." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Βρέθηκε η MySQL με μη αναμενόμενο όνομα." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Βρέθηκε ο εξυπηρετητής MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Βρέθηκε ο εξυπηρετητής MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Ο εξυπηρετητής MySQL είναι εκτελέσιμος." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Η εκτέλεση του εξυπηρετητή MySQL '%1' απέτυχε με το παρακάτω μήνυμα " +"σφάλματος: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Αποτυχία εκτέλεσης του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Δεν ελέγχθηκε η καταγραφή σφαλμάτων του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Δε βρέθηκε τρέχουσα καταγραφή σφαλμάτων MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Ο εξυπηρετητής MySQL δεν ανέφερε σφάλματα κατά την εκκίνηση. Η καταγραφή " +"βρίσκεται στο '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Αδύνατη η ανάγνωση της καταγραφής σφαλμάτων MySQL." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Βρέθηκε αρχείο καταγραφής σφαλμάτων MySQL αλλά δεν είναι αναγνώσιμο: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Η καταγραφή του εξυπηρετητή MySQL περιέχει σφάλματα." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Το αρχείο καταγραφής σφαλμάτων MySQL '%1' περιέχει σφάλματα." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Η καταγραφή του εξυπηρετητή MySQL περιέχει προειδοποιήσεις." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" +"Το αρχείο καταγραφής του εξυπηρετητή MySQL '%1' περιέχει προειδοποιήσεις." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Η καταγραφή του εξυπηρετητή MySQL δεν περιέχει σφάλματα." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Το αρχείο καταγραφής σφαλμάτων MySQL '%1' δεν περιέχει σφάλματα ή " +"προειδοποιήσεις." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Δεν ελέγχθηκαν οι ρυθμίσεις του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Βρέθηκαν οι προκαθορισμένες ρυθμίσεις του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Οι προκαθορισμένες ρυθμίσεις του εξυπηρετητή MySQL βρέθηκαν και είναι " +"αναγνώσιμες στο %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Δε βρέθηκαν οι προκαθορισμένες ρυθμίσεις του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Οι προκαθορισμένες ρυθμίσεις του εξυπηρετητή MySQL δε βρέθηκαν ή δεν ήταν 
αναγνώσιμες. Ελέγξτε την εγκατάσταση του Akonadi και ότι έχετε τις " +"απαραίτητες 
άδειες πρόσβασης." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Μη διαθέσιμες οι προσαρμοσμένες ρυθμίσεις του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Οι προσαρμοσμένες ρυθμίσεις του εξυπηρετητή MySQL δε βρέθηκαν αλλά είναι " +"προαιρετικές." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Βρέθηκαν προσαρμοσμένες ρυθμίσεις του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Οι προσαρμοσμένες ρυθμίσεις του εξυπηρετητή MySQL βρέθηκαν και είναι " +"αναγνώσιμες στο %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Αδύνατη η ανάγνωση των προσαρμοσμένων ρυθμίσεων του εξυπηρετητή MySQL." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Οι σαρμοσμένες ρυθμίσεις του εξυπηρετητή MySQL βρέθηκαν στο %1 αλλά δεν " +"είναι 
αναγνώσιμες. Ελέγξτε τις άδειές σας πρόσβασης." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" +"Δε βρέθηκαν οι ρυθμίσεις του εξυπηρετητή MySQL ή δεν είναι αναγνώσιμες." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"Δε βρέθηκαν οι ρυθμίσεις του εξυπηρετητή MySQL ή δεν είναι αναγνώσιμες." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Οι ρυθμίσεις του εξυπηρετητή MySQL μπορούν να χρησιμοποιηθούν." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"Βρέθηκαν οι ρυθμίσεις του εξυπηρετητή MySQL στο %1 και είναι αναγνώσιμες." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Αδυναμία σύνδεσης στον εξυπηρετητή PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Βρέθηκε ο εξυπηρετητής PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Ο εξυπηρετητής PostgreSQL βρέθηκε και η σύνδεση λειτουργεί." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "δε βρέθηκε το akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Το πρόγραμμα 'akonadictl' πρέπει να είναι προσβάσιμο από το $PATH. " +"Σιγουρευτείτε ότι ο εξυπηρετητής Akonadi είναι εγκατεστημένος." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "βρέθηκε το akonadictl και μπορεί να χρησιμοποιηθεί" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Το πρόγραμμα '%1' για τον έλεγχο του εξυπηρετητή Akonadi βρέθηκε και μπορεί " +"να εκτελεστεί με επιτυχία.\n" +"Αποτέλεσμα:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "βρέθηκε το akonadictl αλλά δεν μπορεί να χρησιμοποιηθεί" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Το πρόγραμμα '%1' για τον έλεγχο του εξυπηρετητή Akonadi βρέθηκε και μπορεί " +"να εκτελεστεί με επιτυχία.\n" +"Αποτέλεσμα:\n" +"%2\n" +"Σιγουρευτείτε ότι ο εξυπηρετητής Akonadi είναι εγκατεστημένος σωστά." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Η διεργασία ελέγχου του Akonadi καταχωρήθηκε στο D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Η διεργασία ελέγχου του Akonadi καταχωρήθηκε στο D-Bus το οποίο υποδηλώνει " +"και τη λειτουργικότητά του." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Η διεργασία ελέγχου του Akonadi δεν καταχωρήθηκε στο D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Η διεργασία ελέγχου του Akonadi δεν καταχωρήθηκε στο D-Bus το οποίο " +"υποδηλώνει ότι δεν εκκίνησε ή συνέβη κάποιο κρίσιμο σφάλμα κατά την εκκίνηση." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Η διεργασία εξυπηρετητή του Akonadi καταχωρήθηκε στο D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Η διεργασία εξυπηρετητή του Akonadi καταχωρήθηκε στο D-Bus το οποίο " +"υποδηλώνει και τη λειτουργικότητά του." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Η διεργασία εξυπηρετητή του Akonadi δεν καταχωρήθηκε στο D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Η διεργασία εξυπηρετητή του Akonadi δεν καταχωρήθηκε στο D-Bus το οποίο " +"υποδηλώνει ότι δεν εκκίνησε ή συνέβη ένα κρίσιμο σφάλμα κατά την εκκίνησή " +"του." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Αδύνατος ο έλεγχος έκδοσης του πρωτοκόλλου." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "Χωρίς σύνδεση με τον εξυπηρετητή δεν είναι δυνατός ο έλεγχος της 
έκδοσης πρωτοκόλλου και αν αυτή συμμορφώνεται με τις απαιτήσεις." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Η έκδοση πρωτοκόλλου του εξυπηρετητή είναι πολύ παλιά." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Η έκδοση πρωτοκόλλου του εξυπηρετητή είναι η %1, ενώ η εφαρμογή απαιτεί την " +"%2. Αν έχετε ενημερώσει το KDE PIM πρόσφατα, βεβαιωθείτε ότι έχετε " +"επανεκκινήσει και το Akonadi και τις εφαρμογές KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Η έκδοση πρωτοκόλλου του εξυπηρετητή είναι πολύ νέα." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Η έκδοση πρωτοκόλλου του εξυπηρετητή ταιριάζει." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Η τρέχουσα έκδοση πρωτοκόλλου είναι η %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Βρέθηκαν πελάτες πόρων." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Βρέθηκε τουλάχιστον ένας πελάτης πόρου." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Δε βρέθηκαν πελάτες πόρων." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Δε βρέθηκαν πελάτες πόρων. Το Akonadi δεν είναι λειτουργικό χωρίς έστω έναν. " +"Αυτό συνήθως σημαίνει ότι δεν είναι εγκατεστημένοι ή ότι υπάρχει κάποιο " +"πρόβλημα στην εγκατάστασή τους. Αναζητήθηκαν οι παρακάτω διαδρομές: '%1'. Η " +"μεταβλητή περιβάλλοντος XDG_DATA_DIRS είναι ορισμένη σε '%2'. Βεβαιωθείτε " +"ότι αυτό περιλαμβάνει όλες τις διαδρομές όπου είναι εγκατεστημένοι πελάτες " +"του Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Δε βρέθηκε η τρέχουσα καταγραφή σφαλμάτων του Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Ο εξυπηρετητής Akonadi δεν ανέφερε σφάλματα κατά την τρέχουσα εκκίνησή του." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Βρέθηκε τρέχουσα καταγραφή σφαλμάτων του εξυπηρετητή Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Ο εξυπηρετητής Akonadi ανέφερε σφάλματα κατά την τρέχουσα εκκίνησή του. Η " +"καταγραφή μπορεί να βρεθεί στο %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Δε βρέθηκε προηγούμενη καταγραφή σφαλμάτων του εξυπηρετητή Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Ο εξυπηρετητής Akonadi δεν ανέφερε σφάλματα κατά την προηγούμενη εκκίνησή " +"του." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Βρέθηκε προηγούμενη καταγραφή σφαλμάτων του εξυπηρετητή Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Ο εξυπηρετητής Akonadi ανέφερε σφάλματα κατά την προηγούμενη εκκίνησή του. Η " +"καταγραφή μπορεί να βρεθεί στο %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Δε βρέθηκε τρέχουσα καταγραφή σφαλμάτων ελέγχου του Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Η διεργασία ελέγχου του Akonadi δεν ανέφερε σφάλματα κατά την τρέχουσα 
εκκίνησή του." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Βρέθηκε τρέχουσα καταγραφή σφαλμάτων ελέγχου του Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "Η διεργασία ελέγχου του Akonadi ανέφερε σφάλματα κατά την τρέχουσα 
εκκίνησή του. Η καταγραφή βρίσκεται στο %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Δε βρέθηκε προηγούμενη καταγραφή σφαλμάτων ελέγχου του Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Η διεργασία ελέγχου του Akonadi δεν ανέφερε σφάλματα κατά την προηγούμενη 
εκκίνησή της." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Βρέθηκε προηγούμενη καταγραφή σφαλμάτων ελέγχου του Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Η διεργασία ελέγχου του Akonadi ανέφερε σφάλματα κατά την προηγούμενη " +"εκκίνησή της. Η καταγραφή βρίσκεται στο %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Το akonadi ξεκίνησε ως root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Η εκτέλεση εφαρμογών που βλέπουν το διαδίκτυο με χρήστη root/διαχειριστή σάς " +"εκθέτει σε πολλούς κινδύνους ασφάλειας. Η MySQL σε αυτήν την εγκατάσταση " +"του Akonadi, δεν θα εκτελεστεί ως root για να σας προστατέψει από αυτούς " +"τους κινδύνους." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Το akonadi δεν εκτελείται ως root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"iΤο Akonadi δεν εκτελείται με χρήστη root/διαχειριστή και αυτή είναι η " +"συνιστώμενη ρύθμιση για ένα ασφαλές σύστημα." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Αποθήκευση αναφοράς ελέγχου" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Σφάλμα" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Αδύνατο το άνοιγμα του αρχείου '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Συνέβη ένα σφάλμα κατά την εκκίνηση του εξυπηρετητή Akonadi. Οι παρακάτω " +"αυτοέλεγχοι βοηθούν στον εντοπισμό και την επίλυση του προβλήματος. Όταν " +"ζητείτε υποστήριξη ή αναφέρετε σφάλματα, παρακαλώ συμπεριλάβετε και αυτήν " +"την αναφορά." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Λεπτομέρειες" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Για περισσότερες υποδείξεις επίλυσης προβλημάτων, ανατρέξτε στο userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Νέος φάκελος..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Νέος" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Διαγραφή φακέλου" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Διαγραφή" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Συγχρονισμός φακέλου" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Συγχρονισμός" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Ι&διότητες φακέλου" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Ιδιότητες" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Επικόλληση" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Επικόλληση" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Διαχείριση τοπικών &συνδρομών..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Διαχείριση τοπικών συνδρομών" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Προσθήκη στους προτιμώμενους φακέλους" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Προσθήκη στους προτιμώμενους" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Διαγραφή από τους προτιμώμενους φακέλους" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Διαγραφή από τον προτιμώμενο" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Μετονομασία προτιμώμενου..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Μετονομασία" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Αντιγραφή φακέλου στο..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Αντιγραφή στο" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Αντιγραφή αντικειμένου στο..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Μετακίνηση αντικειμένου στο..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Μετακίνηση στο" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Μετακίνηση φακέλου δακέλου στο..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Αποκοπή αντικειμένου" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Αποκοπή" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Αποκοπή φακέλου" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Δημιουργία πόρου" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Διαγραφή πόρου" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Ι&διότητες πόρου" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Συγχρονισμός πόρου" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Εργασία χωρίς σύνδεση" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Αναδρομικός &συγχρονισμός φακέλου" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "αναδρομικός συγχρονισμός" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Μετακίνηση φακέλου στα απορρίμματα" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Μετακίνηση φακέλου στα απορρίμματα" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Μετακίνηση αντικειμένου στα απορρίμματα" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Μετακίνηση αντικειμένου στα απορρίμματα" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Επαναφο&ρά φακέλου από τα απορρίμματα" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Επαναφορά φακέλου από τα απορρίμματα" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Επαναφο&ρά αντικειμένου από τα απορρίμματα" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Επαναφορά αντικειμένου από τα απορρίμματα" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Επαναφο&ρά συλλογής από τα απορρίμματα" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Επαναφορά συλλογής από τα απορρίμματα" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Συγχρονισμός προτιμώμενων φακέλων" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Συγχρονισμός προτιμώμενων φακέλων" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Συγχρονισμός δενδρικής δομής φακέλων" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Αντιγραφή φακέλου" +msgstr[1] "&Αντιγραφή %1 φακέλων" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Αντιγραφή αντικειμένου" +msgstr[1] "&Αντιγραφή %1 αντικειμένων" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Αποκοπή αντικειμένου" +msgstr[1] "&Αποκοπή %1 αντικειμένων" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Αποκοπή φακέλου" +msgstr[1] "&Αποκοπή %1 φακέλων" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Διαγραφή αντικειμένου" +msgstr[1] "&Διαγραφή %1 αντικειμένων" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Διαγραφή φακέλου" +msgstr[1] "&Διαγραφή %1 φακέλων" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Συγχρονισμός φακέλου" +msgstr[1] "&Συγχρονισμός %1 φακέλων" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Διαγραφή πόρου" +msgstr[1] "&Διαγραφή %1 πόρων" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Συγχρονισμός πόρου" +msgstr[1] "&Συγχρονισμός %1 πόρων" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Αντιγραφή φακέλου" +msgstr[1] "Αντιγραφή %1 φακέλων" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Αντιγραφή αντικειμένου" +msgstr[1] "Αντιγραφή %1 αντικειμένων" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Αποκοπή αντικειμένου" +msgstr[1] "Αποκοπή %1 αντικειμένων" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Αποκοπή φακέλου" +msgstr[1] "Αποκοπή %1 φακέλων" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Διαγραφή αντικειμένου" +msgstr[1] "Διαγραφή %1 αντικειμένων" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Διαγραφή φακέλου" +msgstr[1] "Διαγραφή %1 φακέλων" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Συγχρονισμός φακέλου" +msgstr[1] "Συγχρονισμός %1 φακέλων" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Διαγραφή πόρου" +msgstr[1] "Διαγραφή %1 πόρων" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Συγχρονισμός πόρου" +msgstr[1] "Συγχρονισμός %1 πόρων" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Όνομα" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Θέλετε πράγματι να διαγραφεί αυτός ο φάκελος με τους υποφάκελους;" +msgstr[1] "Θέλετε πράγματι να διαγραφούν %1 φάκελοι με τους υποφάκελους;" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Διαγραφή φακέλου;" +msgstr[1] "Διαγραφή φακέλων;" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Αδύνατη η διαγραφή του φακέλου: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Η διαγραφή του φακέλου απέτυχε" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Ιδιότητες του φακέλου %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Θέλετε πράγματι να διαγραφεί το επιλεγμένο αντικείμενο;" +msgstr[1] "Θέλετε πράγματι να διαγραφούν %1 αντικείμενα;" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Διαγραφή αντικειμένου;" +msgstr[1] "Διαγραφή αντικειμένων;" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Αδυναμία διαγραφής του αντικειμένου: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Αποτυχία διαγραφής του αντικειμένου" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Μετονομασία προτιμώμενου" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Όνομα:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Νέος πόρος" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Αδυναμία δημιουργίας του πόρου: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Αποτυχία δημιουργίας του πόρου" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Θέλετε πραγματι να διαγραφεί αυτός ο πόρος;" +msgstr[1] "Θέλετε πράγματι τη διαγραφούν %1 πόροι;" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Διαγραφή πόρου;" +msgstr[1] "Διαγραφή πόρων;" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Αδύνατη η επικόλληση δεδομένων: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Αποτυχία επικόλλησης" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Αδυναμία προσθήκης \"/\" στο όνομα του φακέλου." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Σφάλμα κατά τη δημιουργία νέου φακέλου" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Αδυναμία προσθήκης \"/\" στην αρχή ή στο τέλος του ονόματος του φακέλου." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Πριν το συγχρονισμό του φακέλου «%1» είναι απαραίτητο να είναι ο πόρος " +"επιγρασμμικός. Θέλετε να τον κάνετε επιγραμμικό;" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Ο λογαριασμός «%1» είναι εκτός σύνδεσης" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Να γίνει επιγραμμικός" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Μετακίνηση σε αυτόν το φάκελο" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Αντιγραφή σε αυτόν το φάκελο" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Αποτυχία ενημέρωσης συνδρομής: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Σφάλμα συνδρομής" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Τοπικές συνδρομές" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Αναζήτηση:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Ε&γγεγραμμένοι μόνο" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Εγγ&ραφή" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Διαγραφή" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Αποτυχία δημιουργίας νέας ετικέτας" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Παρουσιάστηκε σφάλμα κατά τη δημιουργία νέας ετικέτας" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Θέλετε πράγματι να αφαιρέσετε την ετικέτα %1;" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Διαγραφή ετικέτας" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Διαγραφή ετικέτας" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Επιλογή ετικετών που πρέπει να εφαρμοστούν." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Δημιουργία νέας ετικέτας" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Διαχείριση ετικετών" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Επιλογή ετικετών..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Επιλογή ετικετών" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Καθαρισμός" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Κλικ για προσθήκη ετικετών" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Μετατροπέας Akonadi σε XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Μετατρέπει ένα υποδένδρο συλλογής Akonadi σε XML αρχείο." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Δεν φορτώθηκαν δεδομένα." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Δεν καθορίστηκε όνομα αρχείου" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Αδυναμία ανοίγματος αρχείου δεδομένων '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Το αρχείο %1 δεν υπάρχει." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Αδυναμία ανάλυσης αρχείου δεδομένων '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Αδυναμία φόρτωσης και ανάλυσης του ορισμού του συστήματος." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Αδυναμία δημιουργίας περιεχομένου για τον αναλυτή σχήματος." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Αδυναμία δημιουργίας σχήματος." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Αδυναμία δημιουργίας περιεχομένου επικύρωσης σχήματος." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Μη έγκυρος τύπος αρχείου." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Αδυναμία ανάλυσης αρχείου δεδομένων: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Αδυναμία εύρεσης συλλογής %1" + +#~ msgid "Locally cached parts:" +#~ msgstr "Τοπικά τμήματα στη λανθάνουσα" + +#~ msgid "Store emails locally:" +#~ msgstr "Αποθήκευση αλληλογραφίας τοπικά:" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Απομακρυσμένο Id" + +#~ msgid "MimeType" +#~ msgstr "Τύπος mime" + +#~ msgid "Form" +#~ msgstr "Φόρμα" + +#, fuzzy +#~| msgid "Delete?" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Διαγραφή;" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Ακύρωση" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Μη αναγνωσμένα" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Σύνολο" + +#, fuzzy +#~| msgid "Size:" +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Μέγεθος:" + +#, fuzzy +#~| msgctxt "@title, application name" +#~| msgid "Akonadi Resource" +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Πόρος του Akonadi" + +#, fuzzy +#~| msgctxt "@title:column, name of a thing" +#~| msgid "Name" +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Όνομα" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Βρέθηκε έκδοση πρωτοκόλλου %1, αναμένονταν τουλάχιστον %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Η έκδοση πρωτοκόλλου του εξυπηρετητή είναι επαρκής." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Η έκδοση πρωτοκόλλου του εξυπηρετητή είναι η %1, η οποία είναι ίση ή " +#~ "νεότερη της 
απαιτούμενης έκδοσης %2." + +#~ msgid "KDE Test Program" +#~ msgstr "Πρόγραμμα ελέγχου KDE" + +#, fuzzy +#~| msgid "No such collection." +#~ msgid "Cannot list root collection." +#~ msgstr "Δεν υπάρχει τέτοια συλλογή." + +#, fuzzy +#~| msgid "Akonadi server process registered at D-Bus." +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Η διεργασία εξυπηρετητή του Akonadi καταχωρήθηκε στο D-Bus." + +#, fuzzy +#~| msgid "" +#~| "The Akonadi server process is registered at D-Bus which typically " +#~| "indicates it is operational." +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Η διεργασία εξυπηρετητή του Akonadi καταχωρήθηκε στο D-Bus το οποίο " +#~ "υποδηλώνει και τη λειτουργικότητά του." + +#, fuzzy +#~| msgid "Akonadi server process not registered at D-Bus." +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Η διεργασία εξυπηρετητή του Akonadi δεν καταχωρήθηκε στο D-Bus." + +#, fuzzy +#~| msgid "" +#~| "The Akonadi server process is not registered at D-Bus which typically " +#~| "means it was not started or encountered a fatal error during startup." +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Η διεργασία εξυπηρετητή του Akonadi δεν καταχωρήθηκε στο D-Bus το οποίο " +#~ "υποδηλώνει ότι δεν εκκίνησε ή συνέβη ένα κρίσιμο σφάλμα κατά την εκκίνησή " +#~ "του." + +#, fuzzy +#~| msgid "Akonadi server process not registered at D-Bus." +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Η διεργασία εξυπηρετητή του Akonadi δεν καταχωρήθηκε στο D-Bus." + +#, fuzzy +#~| msgid "Akonadi server process not registered at D-Bus." +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Η διεργασία εξυπηρετητή του Akonadi δεν καταχωρήθηκε στο D-Bus." + +#, fuzzy +#~| msgid "Akonadi server process registered at D-Bus." +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Η διεργασία εξυπηρετητή του Akonadi καταχωρήθηκε στο D-Bus." + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Νέος φάκελος..." + +#, fuzzy +#~| msgid "Folder &Properties..." +#~ msgid "Resource Properties" +#~ msgstr "Ι&διότητες φακέλου..." + +#~ msgid "Cache" +#~ msgstr "Λανθάνουσα μνήμη" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Πολιτική λανθάνουσας μνήμης από γονέα" + +#~ msgid "Cache Policy" +#~ msgstr "Πολιτική λανθάνουσας μνήμης" + +#~ msgid "Interval check time:" +#~ msgstr "Χρονικό διάστημα ελέγχου:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Λήξη τοπικής λανθάνουσας μνήμης:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Συγχρονισμός κατ' απαίτηση" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "" +#~ "Επιλέξτε τους φακέλους που θέλετε να εμφανίζονται στο δέντρο φακέλων" + +#, fuzzy +#~| msgctxt "search folder" +#~| msgid "Search:" +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Αναζήτηση:" + +#~ msgid "Available Folders" +#~ msgstr "Διαθέσιμοι φάκελοι" + +#~ msgid "Current Changes" +#~ msgstr "Τρέχουσες τροποποιήσεις" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Διαγραφή από τον επιλεγμένο φάκελο" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "" +#~ "Ο εξυπηρετητής Akonadi δεν ανέφερε σφάλματα κατά την εκκίνησή του στο %1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Η διεργασία ελέγχου του Akonadi δεν ανέφερε σφάλματα κατά την εκκίνηση " +#~ "στο %1." + +#~ msgid "TODO" +#~ msgstr "TODO" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

To Akonadi δε λειτουργεί.
Λεπτομέρειες...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Πόρος του Akonadi" + +#, fuzzy +#~| msgid "no collection" +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "καμία συλλογή" +#~ msgstr[1] "καμία συλλογή" + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "&Αντιγραφή φακέλου" + +#~ msgid "TextLabel" +#~ msgstr "ΕτικέταΚειμένου" diff --git a/po/en_GB/akonadi_knut_resource.po b/po/en_GB/akonadi_knut_resource.po new file mode 100644 index 0000000..146840d --- /dev/null +++ b/po/en_GB/akonadi_knut_resource.po @@ -0,0 +1,89 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Andrew Coles , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-07-30 23:02+0100\n" +"Last-Translator: Andrew Coles \n" +"Language-Team: British English \n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "No data file selected." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "File '%1' loaded successfully." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Select Data File" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut Data File" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "No item found for remoteid %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Parent collection not found in DOM tree." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Unable to write collection." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Modified collection not found in DOM tree." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Deleted collection not found in DOM tree." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Parent collection '%1' not found in DOM tree." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Unable to write item." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Modified item not found in DOM tree." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Deleted item not found in DOM tree." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Path to the Knut data file." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Do not change the actual backend data." diff --git a/po/en_GB/libakonadi5.po b/po/en_GB/libakonadi5.po new file mode 100644 index 0000000..c85689e --- /dev/null +++ b/po/en_GB/libakonadi5.po @@ -0,0 +1,2660 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Steve Allewell , 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +# Jonathan Marten , 2017. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-04-03 16:01+0100\n" +"Last-Translator: Steve Allewell \n" +"Language-Team: British English \n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 20.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Steve Allewell" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "steve.allewell@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "There is currently no account configured." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Accounts integration is not supported" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Unable to register object at dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 of type %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agent identifier" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Ready" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Offline" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Syncing..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Error." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Not configured" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Resource identifier" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi Resource" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Invalid item retrieved" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Error while creating item: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Error while updating collection: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Updating local collection failed: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Updating local items failed: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Cannot fetch item in offline mode." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Syncing folder '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Failed to retrieve collection for sync." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Failed to retrieve collection for attribute sync." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "The requested item no longer exists" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Job cancelled." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "No such collection." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Found unresolved orphan collections" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Did not find other item for conflict handling" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Unable to access D-Bus interface of created agent." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Agent instance creation timed out." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Unable to obtain agent type '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Unable to create agent instance." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Invalid collection instance." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Invalid resource instance." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Unable to obtain D-Bus interface for resource '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Collection attributes synchronisation timed out." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Invalid collection to copy" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Invalid destination collection" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Invalid parent" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Failed to parse Collection from response" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Invalid collection" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Invalid collection given." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "No objects specified for moving" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "No valid destination specified" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Invalid collection." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Invalid parent collection" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Cannot connect to the Akonadi service." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "User cancelled operation." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Unknown error." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Unexpected response" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Failed to create relation." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Resource synchronisation timed out." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Could not fetch root collection of resource %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "No resource ID given." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Invalid resource identifier '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Failed to configure default resource via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Failed to fetch the resource collection." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Timeout trying to get lock." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Failed to create tag." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Move to wastebin collection failed, aborting delete operation" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Invalid items passed" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Invalid collection passed" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "No valid collection or empty itemlist" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Could not find restore collection and restore resource is not available" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Name" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Loading..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Error" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Name" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Could not copy item: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Could not copy collection: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Could not move item: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Could not move collection: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Could not link entity: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Error" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Favourite Folders" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Total Messages" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Unread Messages" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Storage Size" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Subfolder Storage Size" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Unread" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Size" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Tag" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Unable to fetch item for index" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index is no longer available" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Payload part '%1' is not available for this index" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "No session available for this index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "No item available for this index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Unnamed plugin" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "No description available" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi Self Test" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Checks and reports state of Akonadi server" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&New Agent Instance..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Delete Agent Instance" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configure Agent Instance" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "New Agent Instance" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Could not create agent instance: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Agent instance creation failed" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Delete Agent Instance?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Do you really want to delete the selected agent instance?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 Configuration" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 Handbook" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "About %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "The configuration dialogue has been opened in another window" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Configuration for %1 is already opened elsewhere." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Failed to register %1 configuration dialogue." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minute" +msgstr[1] "minutes" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Retrieval" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Use options from parent folder or account" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchronise when selecting this folder" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatically synchronise after:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Never" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutes" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Locally Cached Parts" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Retrieval Options" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Always retrieve full &messages" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Retrieve message bodies on demand" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Keep message bodies locally for:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Forever" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Search" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Use folder by default" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&New Subfolder..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Create a new subfolder under the currently selected folder" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "New Folder" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Name" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Could not create folder: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Folder creation failed" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "General" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "One object" +msgstr[1] "%1 objects" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Name:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Use custom icon:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "folder" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistics" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Content:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objects" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Size:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Remember that indexing can take some minutes." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Maintenance" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Error while retrieving indexed items count" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexed %1 item in this folder" +msgstr[1] "Indexed %1 items in this folder" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calculating indexed items..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Files" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Folder type:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "unknown" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Items" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Total items:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Unread items:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexing" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Enable fulltext indexing" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Retrieving indexed items count ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Reindex folder" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "No Folder" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Open collection dialogue" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Select a collection" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Move here" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copy here" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancel" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Modification Time" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flags" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribute: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Conflict Resolution" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Take my version" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Take their version" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Keep both versions" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Data" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Starting Akonadi server..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stopping Akonadi server..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Move Here" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copy Here" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Link Here" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancel" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Personal information management service is starting..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Personal information management service is shutting down..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Personal information management service is performing a database upgrade." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimise " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Start" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"The Akonadi personal information management service is not operational." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Details..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Do you want to remove account '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Remove account?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Incoming accounts (add at least one):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&dd..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modify..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "R&emove" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Restart" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Recent Folder" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Rename Favourite" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Name:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi Server Self-Test" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Save Report..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copy Report to Clipboard" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Database driver found." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Database driver not found." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL server executable not tested." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "The current configuration does not require an internal MySQL server." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL server not found." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL server not readable." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL server not executable." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL found with unexpected name." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL server found." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL server found: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL server is executable." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Executing the MySQL server failed." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL server error log not tested." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "No current MySQL error log found." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL error log not readable." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "A MySQL server error log file was found but is not readable: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL server log contains errors." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "The MySQL server error log file '%1' contains errors." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL server log contains warnings." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "The MySQL server log file '%1' contains warnings." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL server log contains no errors." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"The MySQL server log file '%1' does not contain any errors or warnings." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL server configuration not tested." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL server default configuration found." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"The default configuration for the MySQL server was found and is readable at " +"%1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL server default configuration not found." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL server custom configuration not available." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"The custom configuration for the MySQL server was not found but is optional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL server custom configuration found." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"The custom configuration for the MySQL server was found and is readable at %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL server custom configuration not readable." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL server configuration not found or not readable." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "The MySQL server configuration was not found or is not readable." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL server configuration is usable." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "The MySQL server configuration was found at %1 and is readable." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Cannot connect to PostgreSQL server." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL server found." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "The PostgreSQL server was found and connection is working." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl not found" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl found and usable" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl found but not usable" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi control process registered at D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi control process not registered at D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi server process registered at D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi server process not registered at D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protocol version check not possible." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Server protocol version is too old." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Server protocol version is too new." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Server protocol version matches." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "The current Protocol version is %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Resource agents found." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "At least one resource agent has been found." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "No resource agents found." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "No current Akonadi server error log found." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"The Akonadi server did not report any errors during its current startup." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Current Akonadi server error log found." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "No previous Akonadi server error log found." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"The Akonadi server did not report any errors during its previous startup." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Previous Akonadi server error log found." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "No current Akonadi control error log found." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"The Akonadi control process did not report any errors during its current " +"startup." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Current Akonadi control error log found." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "No previous Akonadi control error log found." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"The Akonadi control process did not report any errors during its previous " +"startup." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Previous Akonadi control error log found." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi was started as root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi is not running as root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Save Test Report" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Error" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Could not open file '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Details" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&New Folder..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "New" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Delete Folder" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Delete" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Synchronise Folder" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchronise" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Folder &Properties" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Properties" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Paste" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Paste" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Manage Local &Subscriptions..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Manage Local Subscriptions" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Add to Favourite Folders" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Add to Favourite" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Remove from Favourite Folders" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Remove from Favourite" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Rename Favourite..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Rename" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copy Folder To..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copy To" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copy Item To..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Move Item To..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Move To" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Move Folder To..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Cut Item" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Cut" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Cut Folder" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Create Resource" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Delete Resource" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Resource Properties" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synchronise Resource" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Work Offline" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synchronise Folder Recursively" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synchronise Recursively" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Move Folder To Wastebin" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Move Folder To Wastebin" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Move Item To Wastebin" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Move Item To Wastebin" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restore Folder From Wastebin" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restore Folder From Wastebin" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restore Item From Wastebin" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restore Item From Wastebin" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restore Collection From Wastebin" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restore Collection From Wastebin" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synchronise Favourite Folders" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synchronise Favourite Folders" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synchronise Folder Tree" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copy Folder" +msgstr[1] "&Copy %1 Folders" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copy Item" +msgstr[1] "&Copy %1 Items" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Cut Item" +msgstr[1] "&Cut %1 Items" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Cut Folder" +msgstr[1] "&Cut %1 Folders" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Delete Item" +msgstr[1] "&Delete %1 Items" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Delete Folder" +msgstr[1] "&Delete %1 Folders" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synchronise Folder" +msgstr[1] "&Synchronise %1 Folders" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Delete Resource" +msgstr[1] "&Delete %1 Resources" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synchronise Resource" +msgstr[1] "&Synchronise %1 Resources" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copy Folder" +msgstr[1] "Copy %1 Folders" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copy Item" +msgstr[1] "Copy %1 Items" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Cut Item" +msgstr[1] "Cut %1 Items" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Cut Folder" +msgstr[1] "Cut %1 Folders" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Delete Item" +msgstr[1] "Delete %1 Items" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Delete Folder" +msgstr[1] "Delete %1 Folders" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synchronise Folder" +msgstr[1] "Synchronise %1 Folders" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Delete Resource" +msgstr[1] "Delete %1 Resources" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synchronise Resource" +msgstr[1] "Synchronise %1 Resources" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Name" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Do you really want to delete this folder and all its sub-folders?" +msgstr[1] "Do you really want to delete %1 folders and all their sub-folders?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Delete folder?" +msgstr[1] "Delete folders?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Could not delete folder: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Folder deletion failed" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Properties of Folder %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Do you really want to delete the selected item?" +msgstr[1] "Do you really want to delete %1 items?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Delete item?" +msgstr[1] "Delete items?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Could not delete item: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Item deletion failed" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Rename Favourite" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Name:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "New Resource" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Could not create resource: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Resource creation failed" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Do you really want to delete this resource?" +msgstr[1] "Do you really want to delete %1 resources?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Delete Resource?" +msgstr[1] "Delete Resources?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Could not paste data: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Paste failed" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "We can not add \"/\" in folder name." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Create new folder error" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "We can not add \".\" at begin or end of folder name." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Account \"%1\" is offline" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Go Online" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Move to This Folder" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copy to This Folder" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Failed to update subscription: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Subscription Error" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Local Subscriptions" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Search:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Subscribed only" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Su&bscribe" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Unsubscribe" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Failed to create a new tag" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "An error occurred while creating a new tag" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Do you really want to remove the tag %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Delete tag" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Delete tag" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Select tags that should be applied." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Create new tag" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Manage Tags" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Select tags..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Select Tags" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Clear" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Click to add tags" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi To XML converter" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Converts an Akonadi collection subtree into a XML file." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "No data loaded." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "No filename specified" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Unable to open data file '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "File %1 does not exist." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Unable to parse data file '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Schema definition could not be loaded and parsed." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Unable to create schema parser context." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Unable to create schema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Unable to create schema validation context." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Invalid file format." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Unable to parse data file: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Unable to find collection %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "Remote ID" + +#~ msgid "MimeType" +#~ msgstr "MimeType" + +#~ msgid "Default Name" +#~ msgstr "Default Name" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Delete" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Cancel" + +#~ msgid "Take left one" +#~ msgstr "Take left one" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Unread" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Total" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Size" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi Resource" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Name" + +#~ msgid "Invalid collection specified" +#~ msgstr "Invalid collection specified" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Protocol version %1 found, expected at least %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Server protocol version is recent enough." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Inconsistent local collection tree detected." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE Test Program" diff --git a/po/eo/akonadi_knut_resource.po b/po/eo/akonadi_knut_resource.po new file mode 100644 index 0000000..2df126a --- /dev/null +++ b/po/eo/akonadi_knut_resource.po @@ -0,0 +1,83 @@ +# Translation of akonadi_knut_resource into esperanto. +# Axel Rousseau , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2009-11-15 12:06+0100\n" +"Last-Translator: Axel Rousseau \n" +"Language-Team: esperanto \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: pology\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "" diff --git a/po/eo/libakonadi5.po b/po/eo/libakonadi5.po new file mode 100644 index 0000000..fad1c1a --- /dev/null +++ b/po/eo/libakonadi5.po @@ -0,0 +1,2557 @@ +# Translation of libakonadi into esperanto. +# Axel Rousseau , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2009-11-15 12:06+0100\n" +"Last-Translator: Axel Rousseau \n" +"Language-Team: esperanto \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: pology\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Axel Rousseau" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "axel@esperanto-jeunes.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Akonadi Resource" +msgstr "&Forviŝu dosierujon" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +msgid "Invalid collection to copy" +msgstr "Ne eblis krei dosierujon %1" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +msgid "Invalid destination collection" +msgstr "Ne eblis krei dosierujon %1" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, fuzzy, kde-format +msgid "Invalid collection." +msgstr "Ne eblis krei dosierujon %1" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +msgid "Invalid parent collection" +msgstr "Ne eblis krei dosierujon %1" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Nekonata eraro." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, fuzzy, kde-format +#| msgid "&Name:" +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "&Nomo:" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Ne eblis malfermi dosieron '%1'" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Ne eblis krei dosierujon %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Ne eblis malfermi dosieron '%1'" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Ne eblis krei dosierujon %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, fuzzy, kde-format +msgid "Favorite Folders" +msgstr "&Kreu leterujon" + +#: core/models/statisticsproxymodel.cpp:86 +#, fuzzy, kde-format +#| msgid "Cut Messages" +msgid "Total Messages" +msgstr "Tondi mesaĝojn" + +#: core/models/statisticsproxymodel.cpp:88 +#, fuzzy, kde-format +#| msgid "Unread Message" +msgid "Unread Messages" +msgstr "Nelegita mesaĝo" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvoto" + +#: core/models/statisticsproxymodel.cpp:105 +#, fuzzy, kde-format +msgid "Storage Size" +msgstr "Elektu stilon" + +#: core/models/statisticsproxymodel.cpp:111 +#, fuzzy, kde-format +msgid "Subfolder Storage Size" +msgstr "Elektu stilon" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Sennoma kromprogrameto" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Neniu priskribo uzebla" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, fuzzy, kde-format +msgid "Could not create agent instance: %1" +msgstr "Ne eblis krei dosierujon %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "&Forviŝu dosierujon" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "" +msgstr[1] "" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize when selecting this folder" +msgstr "&Forviŝu dosierujon" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutoj" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "&Serĉu" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, fuzzy, kde-format +#| msgid "&New Folder..." +msgid "&New Subfolder..." +msgstr "&Nova dosierujo..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nova dosierujo" + +#: widgets/collectiondialog.cpp:264 +#, fuzzy, kde-format +#| msgid "&Name:" +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "&Nomo:" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, fuzzy, kde-format +msgid "Could not create folder: %1" +msgstr "Ne eblis krei dosierujon %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "" +msgstr[1] "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nomo:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "folder" +msgstr "Nova dosierujo" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistikoj" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Grandeco:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Cut Messages" +msgid "Total items:" +msgstr "Tondi mesaĝojn" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Message" +msgid "Unread items:" +msgstr "Nelegita mesaĝo" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Reindex folder" +msgstr "&Forviŝu dosierujon" + +#: widgets/collectionrequester.cpp:113 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "No Folder" +msgstr "Nova dosierujo" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Ne eblis krei dosierujon %1" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Rezigni" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Movi tien ĉi" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopii tien ĉi" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Ligi tien ĉi" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "Rezi&gnu" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, fuzzy, kde-format +#| msgid "Details" +msgid "Details..." +msgstr "Detaloj" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "" + +#: widgets/recentcollectionaction.cpp:43 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Recent Folder" +msgstr "&Forviŝu dosierujon" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgid "Rename Filter" +msgid "Rename Favorite" +msgstr "Alinomu filtrilon" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgid "&Name:" +msgid "Name:" +msgstr "&Nomo:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Ne eblis malfermi dosieron '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaloj" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nova dosierujo..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Forviŝu %1 dosierujon" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, fuzzy, kde-format +#| msgid "Properties of Folder %1" +msgid "Properties" +msgstr "Ecoj de leterujo %1" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "Al&glui" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Alglui" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "" + +#: widgets/standardactionmanager.cpp:107 +#, fuzzy, kde-format +msgid "Add to Favorite Folders" +msgstr "&Kaŝu grupoprogramarajn leterujojn" + +#: widgets/standardactionmanager.cpp:108 +#, fuzzy, kde-format +msgid "Add to Favorite" +msgstr "&Kaŝu grupoprogramarajn leterujojn" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, fuzzy, kde-format +#| msgid "Rename Filter" +msgid "Remove from Favorite" +msgstr "Alinomu filtrilon" + +#: widgets/standardactionmanager.cpp:123 +#, fuzzy, kde-format +#| msgid "Rename Filter" +msgid "Rename Favorite..." +msgstr "Alinomu filtrilon" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Copy To" +msgstr "Nova dosierujo" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:150 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Create Resource" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Resource" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize Resource" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:188 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Synchronize Folder Recursively" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:189 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize Recursively" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Synchronize Favorite Folders" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:247 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize Favorite Folders" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize Folder Tree" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Copy Here" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Kopii tien ĉi" +msgstr[1] "&Kopii tien ĉi" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "New Folder" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Nova dosierujo" +msgstr[1] "Nova dosierujo" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Forviŝu %1 dosierujon" +msgstr[1] "&Forviŝu %1 dosierujojn" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:347 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:348 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:350 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Nova dosierujo" +msgstr[1] "Nova dosierujo" + +#: widgets/standardactionmanager.cpp:351 +#, fuzzy, kde-format +#| msgid "&Copy Here" +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "&Kopii tien ĉi" +msgstr[1] "&Kopii tien ĉi" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:353 +#, fuzzy, kde-format +#| msgid "New Folder" +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Nova dosierujo" +msgstr[1] "Nova dosierujo" + +#: widgets/standardactionmanager.cpp:354 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:355 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:356 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgid "&Name:" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "&Nomo:" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:371 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Ecoj de leterujo %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:380 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:381 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgid "Could not delete item: %1" +msgstr "Ne eblis malfermi dosieron '%1'" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, fuzzy, kde-format +#| msgid "Rename Filter" +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Alinomu filtrilon" + +#: widgets/standardactionmanager.cpp:385 +#, fuzzy, kde-format +#| msgid "&Name:" +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "&Nomo:" + +#: widgets/standardactionmanager.cpp:387 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title:window" +msgid "New Resource" +msgstr "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:388 +#, fuzzy, kde-format +msgid "Could not create resource: %1" +msgstr "Ne eblis krei dosierujon %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:396 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "&Forviŝu dosierujon" +msgstr[1] "&Forviŝu dosierujon" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, fuzzy, kde-format +#| msgid "Copy to This Folder" +msgid "Move to This Folder" +msgstr "Kopii al tiu leterujo" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopii al tiu leterujo" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, fuzzy, kde-format +msgid "Search:" +msgstr "&Serĉu" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title" +msgid "Delete tag" +msgstr "&Forviŝu dosierujon" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@info" +msgid "Delete tag" +msgstr "&Forviŝu dosierujon" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "&Forviŝu dosierujon" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgid "Unable to open data file '%1'." +msgstr "Ne eblis malfermi dosieron '%1'" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +msgid "Invalid file format." +msgstr "Ne eblis krei dosierujon %1" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +msgid "Unable to find collection %1" +msgstr "Ne eblis krei dosierujon %1" + +#~ msgid "Id" +#~ msgstr "Id" + +#, fuzzy +#~| msgid "&Delete Folder" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "&Forviŝu dosierujon" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Rezigni" + +#, fuzzy +#~| msgid "&Name:" +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "&Nomo:" + +#~ msgid "KDE Test Program" +#~ msgstr "KDE Testa Programo" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Nova dosierujo..." + +#~ msgid "Cache" +#~ msgstr "Tenejo" diff --git a/po/es/akonadi_knut_resource.po b/po/es/akonadi_knut_resource.po new file mode 100644 index 0000000..15f001c --- /dev/null +++ b/po/es/akonadi_knut_resource.po @@ -0,0 +1,87 @@ +# translation of akonadi_knut_resource.po to Spanish +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Eloy Cuadra , 2009. +# Javier Vinal , 2010. +# Javier Viñal , 2010, 2013. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2013-11-25 14:08+0100\n" +"Last-Translator: Javier Vinal \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.5\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "No se ha seleccionado ningún archivo de datos." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Archivo «%1» cargado con éxito." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Seleccionar archivo de datos" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Archivo de datos Knut de Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "No se encontró ningún elemento para el id. remoto %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Colección padre no encontrada en el árbol DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "No es posible escribir la colección." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Colección modificada no encontrada en el árbol DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Colección borrada no encontrada en el árbol DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Colección padre «%1» no encontrada en el árbol DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "No es posible escribir el elemento." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Elemento modificado no encontrado en el árbol DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Elemento borrado no encontrado en el árbol DOM." diff --git a/po/es/libakonadi5.po b/po/es/libakonadi5.po new file mode 100644 index 0000000..a232bca --- /dev/null +++ b/po/es/libakonadi5.po @@ -0,0 +1,2632 @@ +# translation of libakonadi.po to Spanish +# Translation of libakonadi to Spanish +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Enrique Matias Sanchez (aka Quique) , 2007. +# Santi , 2008. +# Dario Andres Rodriguez , 2008, 2009. +# Eloy Cuadra , 2009, 2011, 2019, 2020, 2021, %Y. +# Cristina Yenyxe Gonzalez Garcia , 2009. +# Adrián Martínez , 2009, 2010. +# Cristina Yenyxe González García , 2010, 2011. +# Javier Vinal , 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 12:30+0100\n" +"Last-Translator: Eloy Cuadra \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"com>\n" +"X-Generator: Lokalize 20.12.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" +"Enrique Matías Sánchez (aka Quique),Santi,Dario Andrés Rodríguez,Adrián " +"Martínez,Javier Viñal" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" +"cronopios@gmail.com,santi@kde-es.org,andresbajotierra@gmail.com,sfxgt3@gmail." +"com,fjvinal@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "No existe ninguna cuenta configurada." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "La integración de cuentas no está permitida" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "No es posible registrar objeto en dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 de tipo %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificador del agente" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agente de Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Preparado" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Desconectado" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sincronizando..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Error." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "No configurado" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificador del recurso" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Recurso de Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Recuperado elemento no válido" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Error al crear el elemento: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Error al actualizar la colección: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Falló al actualizar la colección local: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Falló al actualizar los elementos locales: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "No es posible obtener el elemento en modo desconectado." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sincronizando carpeta «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Ha fallado al recuperar la colección para sincronizar." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Ha fallado al recuperar la colección para sincronización de atributos." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "El elemento solicitado no existe" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Trabajo cancelado." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "No existe esa colección." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Se han encontrado colecciones huérfanas sin resolver" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "No se encontró ningún otro elemento para gestionar el conflicto" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "No se pudo acceder a la interfaz D-Bus del agente creado." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Tiempo de espera excedido al crear la instancia del agente." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "No es posible obtener el tipo de agente «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "No es posible crear la instancia del agente." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Instancia de colección no válida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Instancia del recurso no válida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "No se pudo obtener una interfaz D-Bus para el recurso «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" +"Tiempo de espera excedido al sincronizar los atributos de la colección." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Colección no válida para copiar" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Colección de destino no válida" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Padre no válido" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Ha fallado al analizar la colección desde la respuesta" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Colección no válida" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Se ha indicado una colección no válida." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Ningún objeto especificado para moverse" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "El destino especificado no es válido" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Colección no válida." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Colección padre no válida" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "No es posible la conexión con el servicio de Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"La versión de protocolo del servidor de Akonadi es incompatible. Asegúrese " +"de tener una versión compatible instalada." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operación cancelada por el usuario." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Error desconocido." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Respuesta inesperada" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Ha fallado al crear la relación." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tiempo de espera excedido durante la sincronización del recurso." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "No se ha podido obtener la colección raíz del recurso %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "No se ha dado una identidad de recurso." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identificador de recurso «%1» no válido" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Ha fallado al configurar el recurso por defecto a través de D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Ha fallado al obtener la colección de recursos." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tiempo de espera agotado en el intento de bloqueo." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Ha fallado al crear etiqueta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Ha fallado al mover colección a la papelera, operación interrumpida" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Pasados elementos no válidos" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Pasada colección no válida" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Colección no válida o lista de elementos vacía" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"No se ha podido encontrar la colección a restaurar y el recurso de " +"restauración no está disponible" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nombre" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Cargando..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Error" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"La colección de destino «%1» ya contiene\n" +"una colección llamada «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nombre" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "No se ha podido copiar el elemento: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "No se ha podido copiar la colección: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "No se ha podido mover el elemento: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "No se ha podido mover la colección: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "No se ha podido vincular la entidad: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Error" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Carpetas favoritas" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Total de mensajes" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Mensajes no leídos" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Cuota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Tamaño de almacenamiento" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Tamaño de almacenamiento de la subcarpeta" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "No leído" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Tamaño" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiqueta" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "No es posible obtener el objeto a indexar" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "El índice ya no está disponible" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" +"El componente de carga útil «%1» no se encuentra disponible para este índice" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Sesión no disponible para este índice" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Ningún elemento disponible para este índice" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Complemento sin nombre" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "No hay descripción disponible" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"La versión de protocolo del servidor Akonadi difiere de la usada por esta " +"aplicación.\n" +"Si usted actualizó recientemente su sistema, por favor, inicie sesión y " +"vuelva a ingresar para asegurarse de que todas las aplicaciones usan la " +"versión de protocolo correcta." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"No hay agentes de Akonadi disponibles. Por favor, verifique su instalación " +"de KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Discordancia en la versión del protocolo. La versión del servidor es más " +"antigua (%1) que la nuestra (%2). Si usted actualizó su sistema " +"recientemente, por favor, reinicie el servidor Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Discordancia en la versión del protocolo. La versión del servidor es más " +"reciente (%1) que la nuestra (%2). Si usted actualizó su sistema " +"recientemente, por favor, reinicie todas las aplicaciones de KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Autoprueba de Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Comprueba e informa del estado del servidor Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nueva instancia de agente..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Borrar instancia de agente" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configurar instancia de agente" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nueva instancia de agente" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "No se ha podido crear la instancia del agente: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "La creación de la instancia del agente ha fallado" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "¿Borrar instancia de agente?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "¿Seguro qué desea borrar la instancia de agente seleccionada?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuración %1 " + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manual %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Acerca de %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "El diálogo de configuración se ha abierto en otra ventana" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "La configuración para %1 está abierta todavía en otro lugar" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Ha fallado al registrar el diálogo de configuración %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuto" +msgstr[1] "minutos" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Recuperación" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usar opciones de la carpeta padre o cuenta" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronizar al seleccionar esta carpeta" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronizar automáticamente después de" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nunca" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutos" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Componentes de la caché local" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opciones de recuperación" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Recuperar siempre mensajes co&mpletos" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Recuperar el cuerpo de los mensajes bajo demanda" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Mantener los cuerpos de los mensajes en local para" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Para siempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Buscar" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usar carpeta por omisión" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nueva subcarpeta..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Crear una nueva subcarpeta por debajo de la carpeta seleccionada" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nueva carpeta" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nombre" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "No se han podido crear la carpeta: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "La creación de la carpeta ha fallado" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "General" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un objeto" +msgstr[1] "%1 objetos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nombre:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Utilizar icono personalizado:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "carpeta" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estadísticas" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contenido:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objetos" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Tamaño:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bytes" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Recuerde que la indexación puede durar varios minutos." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Mantenimiento" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Error en la recuperación del contador de elementos indexados" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexado %1 elemento en esta carpeta" +msgstr[1] "Indexados %1 elementos en esta carpeta" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calculando elementos indexados..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Archivos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipo de carpeta:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "desconocido" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementos" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Total de elementos:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Elementos no leídos" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexado" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Activar indexado de texto completo" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Recuperando el contador de elementos indexados..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Volver a indexar la carpeta" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ninguna carpeta" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Diálogo de abrir colección" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Seleccione una colección" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mover aquí" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copiar aquí" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancelar" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Hora de modificación" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Indicadores" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atributo: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolución de conflictos" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Elegir mi versión" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Elegir su versión" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mantener ambas versiones" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Sus cambios entran en conflicto con los hechos por otra persona mientras " +"tanto.
Dado que solo puede aplicarse una versión, usted tendrá que " +"integrar esos cambios manualmente.
Pulse sobre «Abrir el editor de texto» para mantener una copia de los textos, a " +"continuación, seleccione que versión es la más correcta, reábrala y " +"modifíquela de nuevo para añadir lo perdido." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Datos" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Iniciando servidor Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Deteniendo servidor Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mover aquí" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copiar aquí" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "En&lazar aquí" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancelar" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"No es posible la conexión con el servicio de gestión de información " +"personal.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "El servicio de gestión de información personal se está iniciando..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "El servicio de gestión de información personal se está parando..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"El servicio de gestión de información personal está realizando una " +"actualización de base de datos." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"El servicio de gestión de información personal está realizando una " +"actualización de la base de datos.\n" +"Esto ocurre después de una actualización de software y es necesario para " +"optimizar el rendimiento.\n" +"Dependiendo de la cantidad de información personal, podría tardar algunos " +"minutos." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"El servicio de gestión de información personal de Akonadi no está " +"funcionando. Esta aplicación no puede usarse sin ello." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Iniciar" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"La infraestructura de gestión de información personal de Akonadi no está " +"funcionando.\n" +"Haga clic en «Detalles» para obtener información detallada de este problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"El servicio de gestión de información personal de Akonadi no está " +"funcionando." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalles..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "¿Quiere usted eliminar la cuenta «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "¿Eliminar la cuenta?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Cuentas entrantes (añada al menos una):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "Aña&dir..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modificar..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Eliminar" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reiniciar" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Carpeta reciente" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Cambiar nombre del favorito" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nombre:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Autoprueba de servidor de Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Guardar informe..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copiar informe al portapapeles" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"El controlador QtSQL «%1», necesario por su configuración actual del " +"servidor Akonadi, se encontró en su sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"El controlador QtSQL «%1» es necesario por su configuración actual del " +"servidor Akonadi.\n" +"Los siguientes controladores están instalados: %2.\n" +"Asegúrese de que el controlador necesario esté instalado." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Controlador de base de datos encontrado." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Controlador de base de datos no encontrado." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Ejecutable del servidor MySQL no probado." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "La actual configuración no necesita un servidor MySQL interno." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Actualmente tiene configurado Akonadi para usar el servidor MySQL «%1».\n" +"Asegúrese de tener el servidor MySQL instalado, la ruta configurada " +"correctamente y los permisos de lectura y ejecución necesarios sobre el " +"ejecutable del servidor. Dicho ejecutable suele llamare «mysqld», y su " +"ubicación varía dependiendo la distribución." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Servidor MySQL no encontrado." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Servidor MySQL no legible." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Servidor MySQL no ejecutable." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL encontrado con un nombre inesperado." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Servidor MySQL encontrado." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Servidor MySQL encontrado: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "El servidor MySQL es ejecutable." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"La ejecución del servidor MySQL «%1» falló con el siguiente mensaje de " +"error: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "La ejecución del servidor MySQL ha fallado." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "El registro de errores del servidor MySQL no fue probado." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "No se encontró un registro actual de errores MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"El servidor MySQL no informó de ningún error durante el inicio. Puede " +"encontrar el registro en «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "El registro de errores MySQL no es legible." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Se encontró un registro de errores del servidor MySQL, pero no es legible: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "El registro del servidor MySQL contiene errores." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" +"El archivo de registro de errores del servidor MySQL «%1» contiene errores." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "El registro del servidor MySQL contiene advertencias." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "El archivo del registro del servidor MySQL «%1» contiene advertencias." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "El registro del servidor MySQL no contiene errores." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"El archivo de registro del servidor MySQL «%1» no contiene errores ni " +"advertencias." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "La configuración del servidor MySQL no fue probada." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "La configuración predeterminada del servidor MySQL fue encontrada." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"La configuración predeterminada para el servidor MySQL fue encontrada y es " +"legible en %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "La configuración predeterminada del servidor MySQL no fue encontrada." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"La configuración predeterminada del servidor MySQL no fue encontrada o no " +"fue posible leerla. Compruebe que su instalación Akonadi esté completa y que " +"tenga todos los permisos de acceso necesarios." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "La configuración personalizada del servidor MySQL no está disponible." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"La configuración personalizada para el servidor MySQL no fue encontrada, " +"pero es opcional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "La configuración personalizada del servidor MySQL fue encontrada." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"La configuración personalizada para el servidor MySQL fue encontrada y es " +"legible en %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "La configuración personalizada del servidor MySQL no es legible." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"La configuración personalizada para el servidor MySQL fue encontrada en %1 " +"pero no es legible. Compruebe su permiso de acceso." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "La configuración del servidor MySQL no fue encontrada o no es legible." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "La configuración del servidor MySQL no fue encontrada o no es legible." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "La configuración del servidor MySQL es utilizable." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "La configuración del servidor MySQL fue encontrada en %1 y es legible." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "No es posible conectar con el servidor de PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Servidor PostgreSQL encontrado." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Servidor PostgreSQL encontrado y conexión funcionando." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl no fue encontrado" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"El programa «akonadictl» necesita estar disponible bajo $PATH. Asegúrese de " +"tener el servidor Akonadi instalado." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl se encontró y es utilizable" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"El programa «%1» para controlar el servidor Akonadi fue encontrado y puede " +"ser ejecutado satisfactoriamente.\n" +"Resultado:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl se encontró, pero no es utilizable" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"El programa «%1» para controlar el servidor Akonadi fue encontrado, pero no " +"puede ser ejecutado satisfactoriamente.\n" +"Resultado:\n" +"%2\n" +"Asegúrese de que el servidor Akonadi esté instalado correctamente." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "El proceso de control de Akonadi registrado en D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"El proceso de control de Akonadi está registrado en D-Bus lo que, " +"generalmente, indica que es funcional." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "El proceso de control de Akonadi no registrado en D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"El proceso de control de Akonadi no está registrado en D-Bus lo que, " +"generalmente significa que no fue iniciado o se encontró un error fatal " +"durante el inicio." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "El proceso del servidor de Akonadi registrado en D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"El proceso del servidor Akonadi está registrado en D-Bus lo que, " +"generalmente indica que es funcional." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "El proceso del servidor Akonadi no registrado en D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"El proceso del servidor de Akonadi no está registrado en D-Bus lo que, " +"generalmente significa que no fue iniciado o se encontró un error fatal " +"durante el inicio." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "La comprobación de la versión del servidor no es posible." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Sin una conexión con el servidor no es posible comprobar si la versión del " +"protocolo cumple los requisitos." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "La versión del protocolo del servidor es muy antigua." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"La versión del protocolo del servidor es %1, pero se necesita al menos la " +"versión %2. Si usted actualizó recientemente KDE PIM, por favor, asegúrese " +"de reiniciar tanto Akonadi como las aplicaciones de KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "La versión del protocolo del servidor es muy nueva." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "La versión del protocolo del servidor coincide." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "La versión actual del protocolo del servidor es %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Agentes de recursos encontrados." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Al menos un agente de recursos ha sido encontrado." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "No se encontraron agentes de recursos." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"No se encontraron agentes de recursos. Akonadi no es utilizable si no se " +"tiene al menos uno. Normalmente, esto significa que no hay agentes de " +"recursos instalados o que existe un problema en la configuración. Se ha " +"buscado en las siguientes rutas: «%1». La variable de entorno XDG_DATA_DIRS " +"está configurada como «%2»; asegúrese de que incluya todas las rutas donde " +"estén instalados los agentes Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "No se encontró un registro actual de errores del servidor Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"El servidor Akonadi no informó de ningún error durante su inicio actual." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Se encontró un registro actual de errores del servidor Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"El servidor Akonadi informó de errores durante su actual inicio. Puede " +"encontrar el registro en «%1»." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "No se encontró un registro anterior de errores del servidor Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"El servidor Akonadi no informó de ningún error durante su inicio anterior." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Se encontró un registro anterior de errores del servidor Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"El servidor Akonadi informó de errores durante su anterior inicio. Puede " +"encontrar el registro en %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "No se encontró un registro actual de errores del control de Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"El proceso de control de Akonadi no reportó ningún error durante su actual " +"inicio." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Se encontró un registro actual de errores del control de Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"El proceso de control de Akonadi informó de errores durante su actual " +"inicio. Puede encontrar el registro en %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "No se encontró un registro anterior de errores del control de Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"El proceso de control de Akonadi no informó de ningún error durante su " +"inicio anterior." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Se encontró un registro anterior de errores del control de Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"El proceso de control de Akonadi informó de errores durante su inicio " +"anterior. Puede encontrar el registro en %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi se ha ejecutado como root (superusuario)" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Ejecutar aplicaciones orientadas a Internet como root/administrador le " +"expone a riesgos de seguridad. MySQL, utilizado para esta instalación de " +"Akonadi, no se permitirá ejecutarse como root para protegerle de estos " +"riesgos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi no se está ejecutando como root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi no se está ejecutando como root/administrador, lo cual es la " +"configuración recomendada para la seguridad del sistema." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Guardar informe de prueba" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Error" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "No se ha podido abrir el archivo «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Un error ocurrió durante el inicio del servidor Akonadi. Los siguientes " +"autopruebas se proponen ayudar a encontrar y solucionar este problema. " +"Cuando pida apoyo técnico o comunique fallos, por favor, siempre incluya " +"este informe." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalles" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Para obtener más consejos para la resolución de problemas por favor " +"consulte userbase.kde.org/" +"Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nueva carpeta..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nuevo" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Borrar carpeta" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Borrar" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sincronizar carpeta" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronizar" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propiedades de la carpeta" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propiedades" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Pegar" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Pegar" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gestionar &suscripciones locales..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gestionar suscripciones locales" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Añadir a carpetas favoritas" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Añadir a favorito" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Eliminar de carpetas favoritas" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Eliminar de favorito" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Cambiar el nombre del favorito..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Cambiar de nombre" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copiar carpeta a..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copiar en" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copiar elemento a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mover elemento a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mover a" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mover carpeta a..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Cortar elemento" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Cortar" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Cortar carpeta" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Crear recurso" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Borrar recurso" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Propiedades del &recurso" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sincronizar recurso" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Trabajar sin conexión" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronizar carpeta recursivamente" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronizar recursivamente" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mover carpeta a la papelera" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mover carpeta a la papelera" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Mover elemento a la papelera" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Mover elemento a la papelera" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Recuperar carpeta de la papelera" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Recuperar carpeta de la papelera" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Recuperar elemento de la papelera" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Recuperar elemento de la papelera" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Recuperar colección de la papelera" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Recuperar colección de la papelera" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronizar carpetas favoritas" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronizar carpetas favoritas" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronizar árbol de carpetas" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copiar carpeta" +msgstr[1] "&Copiar %1 carpetas" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copiar elemento" +msgstr[1] "&Copiar %1 elementos" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Cortar elemento" +msgstr[1] "&Cortar %1 elementos" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Cortar carpeta" +msgstr[1] "&Cortar %1 carpetas" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Borrar elemento" +msgstr[1] "&Borrar %1 elementos" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Borrar carpeta" +msgstr[1] "&Borrar %1 carpetas" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronizar carpeta" +msgstr[1] "&Sincronizar %1 carpetas" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Borrar recurso" +msgstr[1] "&Borrar %1 recursos" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronizar recurso" +msgstr[1] "&Sincronizar %1 recursos" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copiar carpeta" +msgstr[1] "Copiar %1 carpetas" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copiar elemento" +msgstr[1] "Copiar %1 elementos" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Cortar elemento" +msgstr[1] "Cortar %1 elementos" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Cortar carpeta" +msgstr[1] "Cortar %1 carpetas" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Borrar elemento" +msgstr[1] "Borrar %1 elementos" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Borrar carpeta" +msgstr[1] "Borrar %1 carpetas" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronizar carpeta" +msgstr[1] "Sincronizar %1 carpetas" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Borrar recurso" +msgstr[1] "Borrar %1 recursos" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronizar recurso" +msgstr[1] "Sincronizar %1 recursos" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nombre" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "¿Seguro qué desea borrar esta carpeta y todas sus subcarpetas?" +msgstr[1] "¿Seguro qué desea borrar %1 carpetas y todas sus subcarpetas?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "¿Borrar carpeta?" +msgstr[1] "¿Borrar carpetas?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "No se ha podido borrar la carpeta: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "El borrado de la carpeta ha fallado" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propiedades de la carpeta %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "¿Seguro qué desea borrar el elemento seleccionado?" +msgstr[1] "¿Seguro qué desea borrar %1 elementos?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "¿Borrar elemento?" +msgstr[1] "¿Borrar elementos?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "No se ha podido borrar el elemento: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "El borrado del elemento ha fallado" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Cambiar nombre del favorito" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nombre:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nuevo recurso" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "No se ha podido crear el recurso: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "La creación del recurso ha fallado" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "¿Seguro qué desea borrar este recurso?" +msgstr[1] "¿Seguro qué desea borrar %1 recursos?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "¿Borrar recurso?" +msgstr[1] "¿Borrar recursos?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "No se han podido pegar los datos: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Pegado fallido" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "No podemos añadir «/» en el nombre de la carpeta." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Error al crear la nueva carpeta" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "No podemos añadir «.» al principio o final del nombre de la carpeta." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Antes de sincronizar la carpeta «%1» es necesario tener el recurso en línea. " +"¿Quiere usted ponerlo en línea?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "La cuenta «%1» está desconectada" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Ponerse en línea" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mover a esta carpeta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copiar en esta carpeta" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "No se ha podido actualizar la suscripción: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Error de suscripción" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Suscripciones locales" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Buscar:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Solo suscritos" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Suscri&bir" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Darse de baja" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Ha fallado al crear una nueva etiqueta" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Ha ocurrido un error al crear una nueva etiqueta" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "¿Seguro qué desea eliminar la etiqueta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Borrar etiqueta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Borrar etiqueta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Seleccione las etiquetas que se deben aplicar." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Crear nueva etiqueta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gestionar etiquetas" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Seleccionar etiquetas..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Seleccionar etiquetas" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Despejar" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Pulse para añadir etiquetas" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Conversor de Akonadi a XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Convierte un subárbol de colección Akonadi a un archivo XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "No se cargaron datos." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nombre de archivo no especificado" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "No es posible abrir el archivo de datos «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "El archivo %1 no existe." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "No es posible analizar el archivo de datos «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "La definición del esquema no se pudo cargar y analizar." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "No es posible crear el contexto del analizador de esquema." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "No es posible crear el esquema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "No es posible crear el contexto de validación del esquema." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Formato de archivo no válido." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "No es posible analizar el archivo de datos «%1»" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "No es posible encontrar la colección %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "ID remoto" + +#~ msgid "MimeType" +#~ msgstr "Tipo MIME" + +#~ msgid "Form" +#~ msgstr "Formulario" + +#~ msgid "Default Name" +#~ msgstr "Nombre predeterminado" diff --git a/po/et/akonadi_knut_resource.po b/po/et/akonadi_knut_resource.po new file mode 100644 index 0000000..cd2ece4 --- /dev/null +++ b/po/et/akonadi_knut_resource.po @@ -0,0 +1,91 @@ +# translation of akonadi_knut_resource.po to Estonian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Marek Laane , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-08-17 04:20+0300\n" +"Last-Translator: Marek Laane \n" +"Language-Team: Estonian \n" +"Language: et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.1\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Andmefaili pole valitud." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Fail \"%1\" laaditi edukalt." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Andmefaili valimine" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knuti andmefail" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Võrgu-ID %1 jaoks ei leitud ühtegi elementi" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM-puus ei leitud eellaskogu." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Kogu kirjutamine nurjus." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM-puus ei leitud muudetud kogu." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM-puus ei leitud kustutatud kogu." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM-puus ei leitud eellaskogu \"%1\"." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Elemendi kirjutamine nurjus." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM-puus ei leitud muudetud elementi." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM-puus ei leitud kustutatud elementi." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Knuti andmefaili asukoht." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Taustaprogrammi tegelikke andmeid ei muudeta." diff --git a/po/et/libakonadi5.po b/po/et/libakonadi5.po new file mode 100644 index 0000000..1fd8ba1 --- /dev/null +++ b/po/et/libakonadi5.po @@ -0,0 +1,2805 @@ +# translation of libakonadi.po to Estonian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Marek Laane , 2007-2009, 2010, 2011, 2012, 2014, 2016, 2019, 2020. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2020-05-07 13:23+0300\n" +"Last-Translator: Marek Laane \n" +"Language-Team: Estonian \n" +"Language: et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 19.12.3\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Marek Laane" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "qiilaq69@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Praegu ei ole ühtegi kontot seadistatud." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Kontode lõimimine ei ole toetatud" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Objekti registreerimine D-Busis nurjus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1, tüüp %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agendi identifikaator" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Valmis" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Pole võrgus" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sünkroonimine..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Viga." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Pole seadistatud" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Ressursi identifikaator" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi ressurss" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Hangiti vigane element" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Tõrge elemendi loomisel: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Tõrge kogu uuendamisel: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Kohaliku kogu uuendamine nurjus: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Kohalike elementide uuendamine nurjus: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Võrguta režiimis ei saa elementi tõmmata." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Kausta '%1' sünkroonimine" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Kogu hankimine sünkroonimiseks nurjus." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Kogu hankimine atribuutide sünkroonimiseks nurjus." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Soovitud elementi pole enam" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Töö katkestati" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Sellist kogu ei ole." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Leiti lahendamata orbude kogud" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Konflikti lahendamiseks ei leitud teist elementi" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Loodud agendi D-Busi liidese kasutamine nurjus." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Agendi isendi loomine aegus." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Agendi tüübi \"%1\" hankimine nurjus." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Agendi isendi loomine nurjus." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Vigane kogu isend." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Vigane ressursiisend." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Ressursi \"%1\" D-Busi liidese hankimine nurjus" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Kogu atribuutide sünkroonimisel tekkis ajaületus." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Vigane kogu kopeerimiseks" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Vigane sihtkogu" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Vigane eellane." + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Kogu parsimine vastusest nurjus" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Vigane kogu" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Määrati vigane kogu." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Liigutamiseks pole ühtegi objekti määratud" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Korrektset sihtkohta pole määratud" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Vigane kogu." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Vigane eellaskogu" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Ühenduse loomine Akonadi teenusega nurjus." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi serveri protokolli versioon ei ole ühilduv. Kontrolli, kas sul on " +"paigaldatud ühilduv versioon." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Kasutaja katkestas toimingu." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Tundmatu viga." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Ootamatu vastus" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Seose loomine nurjus." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Ressursi sünkroonimisel tekkis ajaületus." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Ressursi %1 juurkogu hankimine nurjus." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Ressursi ID-d pole määratud." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Vigane ressursi identifikaator '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Vaikeressursi seadistamine D-Busi vahendusel nurjus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Ressursikogu hankimine nurjus" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Ajaületus luku hankimisel." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Sildi loomine nurjus." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Kogu viskamine prügikasti nurjus, toimingust loobutakse" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Edastati vigased elemendid" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Edastati vigane kogu" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Korrektne kogu puudub või on elementide loend tühi" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Taastatud kogu ei leitud ja taastamisressurss pole kättesaadav" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nimi" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Laadimine ..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Tõrge" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Sihtkogu \"%1\" sisaldab juba\n" +"kogu nimega \"%2\"." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nimi" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Elemendi kopeerimine nurjus: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kogu kopeerimine nurjus: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Elemendi liigutamine nurjus: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Kogu liigutamine nurjus: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Olemi linkimine nurjus: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Tõrge" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Lemmikkaustad" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Kirju kokku" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Lugemata kirju" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvoot" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Salvesti suurus" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Alamkausta salvesti suurus" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Lugemata" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Kokku" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Suurus" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Silt" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Elementide tõmbamine indeksi jaoks nurjus" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indeks pole enam saadaval" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Indeksile pole saadaval ressursi komponenti '%1'" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Indeksile pole saadaval ühtegi seanssi" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Indeksile pole saadaval ühtegi elementi" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Nimetu plugin" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Kirjeldus puudub" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi serveri protokolli versioon erineb selle rakenduse kasutatavast " +"protokolli versioonist.\n" +"Kui uuendasid hiljuti oma süsteemi, siis logi palun välja ja uuesti sisse " +"tagamaks, et kõik rakendused kasutavad õiget protokolli versiooni." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Akonadi agente pole saadaval. Palun kontrolli oma KDE PIM-i paigaldust." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Protokolli versioonid ei sobi. Serveri versioon on vanem (%1) kui meie oma " +"(%2). Kui oled süsteemi hiljaaegu uuendanud, käivita palun Akonadi server " +"uuesti." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Protokolli versioonid ei sobi. Serveri versioon on uuem (%1) kui meie oma " +"(%2). Kui oled süsteemi hiljaaegu uuendanud, käivita palun kõik KDE PIM-i " +"rakendused uuesti." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi enesetest" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi serveri kontrollimine ja oleku teadaandmine" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008: Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Uus agendi isend..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Kustuta agendi isend" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Seadista agendi isendit" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Uus agendi isend" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Agendi isendi loomine nurjus: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Agendi isendi loomine nurjus" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Kas kustutada agendi isend?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Kas tõesti kustutada kõik valitud agendi isendid?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 seadistamine" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 käsiraamat" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "%1 teave" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Seadistustedialoog on avatud teises aknas" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "%1 seadistamine on mujal avatud." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "%1 seadistustedialoogi registreerimine nurjus." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "min" +msgstr[1] "min" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Tõmbamine" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Ülemkausta või konto valikute kasutamine" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sünkroonimine kausta valimisel" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automaatse sünkroonimise intervall:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "puhvrit ei kontrollita kunagi" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "min" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Kohalikult puhverdatud komponendid" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Tõmbamise valikud" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Alati tõ&mmatakse täielikud kirjad" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Ki&rja sisu tõmmatakse vajaduse korral" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Kirja sisu säilitatakse kohapeal:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Mitte kunagi" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Otsing" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Kausta kasutamine vaikimisi" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Uus alamkaust..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Uue alamkausta loomine parajasti valitud kaustas" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Uus kaust" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nimi" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Kausta loomine nurjus: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Kausta loomine nurjus" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Üldine" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Üks objekt" +msgstr[1] "%1 objekti" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nimi:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Kohandat&ud ikooni kasutamine:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "kaust" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Sisu:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objekti" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Suurus:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 baiti" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Arvesta, et indekseerimisele võib kuluda mitu minutit." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Hooldus" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Tõrge indekseeritud elementide arvu hankimisel" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indekseeriti %1 element selles kataloogis" +msgstr[1] "Indekseeriti %1 elementi selles kataloogis" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Indekseeritud elementide kokkulugemine ..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Failid" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Kausta tüüp:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "teadmata" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elemendid" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elemente kokku:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Lugemata elemente:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indekseerimine" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Täisteksti indekseerimise lubamine" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Indekseeritud elementide arvu hankimine ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indekseeri kaust uuesti" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Kaust puudub" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Kogu avamise dialoog" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Kogu valimine" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Liiguta siia" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopeeri siia" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Loobu" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Muutmise aeg" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Lipud" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribuut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Konflikti lahendamine" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Kasuta minu versiooni" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Kasuta nende versiooni" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Jäta mõlemad versioonid alles" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Sinu muudatused on vastuolus muudatustega, mida vahepeal on teinud keegi " +"teine.
Kui just ei soovita üht versiooni kõrvale heita, tuleb muudatused " +"käsitsi sisestada.
Klõpsa tekstide koopia säilitamiseks Ava tekstiredaktor, vali, milline versioon on kõige " +"õigem, siis ava see uuesti ja asu muutma, et lisada puuduvad osad." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Andmed" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi serveri käivitamine..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi serveri peatamine..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Liiguta siia" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopeeri siia" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Lingi siia" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Loobu" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Ühenduse loomine personaalse teabe haldamise teenusega nurjus.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Personaalse teabe halduse teenus käivitatakse..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Personaalse teabe halduse teenus lülitatakse välja..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Personaalse teabe halduse teenus uuendab andmebaasi." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Personaalse teabe haldamise teenus uuendab andmebaasi.\n" +"See juhtub pärast tarkvara uuendamist ja on vajalik jõudluse " +"optimeerimiseks.\n" +"Sõltuvalt personaalse teabe hulgast võib selleks kuluda mitu minutit." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi personaalse teabe halduse teenus ei tööta. Seda rakendust ei saa " +"ilma selleta kasutada." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Käivita" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi personaalse teabe halduse (PIM) raamistik ei tööta.\n" +"Täpsema teabe saamiseks klõpsa \"Üksikasjad...\"" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi personaalse teabe halduse teenus ei tööta." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Üksikasjad..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Kas tõesti eemaldada konto \"%1\"?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Kas eemaldada konto?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Sissetulevate kirjade kontod (lisa vähemalt üks):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "L&isa..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Muuda..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Eemalda" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Käivita uuesti" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Viimati kasutatud kaust" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Lemmikkausta nime muutmine" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nimi:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi serveri enesetest" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Salvesta aruanne..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopeeri aruanne lõikepuhvrisse" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Sinu praegune Akonadi serveri seadistus nõuab QtSQL draiverit '%1' ja see " +"leiti süsteemist." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Sinu praegune Akonadi serveri seadistus nõuab QtSQL draiverit '%1'.\n" +"Paigaldatud on järgmised draiverid. %2.\n" +"Kontrolli, kas vajalik draiver on paigaldatud." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Leiti andmebaasi draiver." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Andmebaasi draiverit ei leitud." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL-serveri käivitusfaili ei ole testitud." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Praegune seadistus ei nõua sisemist MySQL-serverit." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Akonadi on praegu seadistatud kasutama MySQL-serverit '%1'.\n" +"Kontrolli, kas MySQL-server on paigaldatud, määratud õige otsingutee ning " +"sul on serveri käivitusfailile vajalikud lugemis- ja käivitamisõigused. " +"serveri käivitusfail kannab tavaliselt nime 'mysqld', aga selle asukoht on " +"distributsiooniti erinev." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-serverit ei leitud." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-server ei ole loetav." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-server ei ole käivitatav." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL leiti ootamatu nime all." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Leiti MySQL-server." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Leiti MySQL-server: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-server on käivitatav." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "MySQL-serveri '%1' käivitamine nurjus järgmise veateatega: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL-serveri käivitamine nurjus." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL-serveri vealogi ei ole testitud." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "MySQL-serveri vealogi ei leitud." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL-server ei teatanud käivitamisel ajal mingitest vigadest. Logi võib " +"leida failis \"%1\"." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL vealogi ei ole loetav." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Leiti MySQL-serveri vealogi fail, aga see ei ole loetav: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-serveri logi sisaldab vigu." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL-serveri vealogi fail '%1' sisaldab vigu." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-serveri logi sisaldab hoiatusi." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL-serveri logifail '%1' sisaldab hoiatusi." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-serveri logi ei sisalda vigu." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL-serveri logifail '%1' ei sisalda vigu ega hoiatusi." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-serveri seadistust ei ole testitud." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Leiti MySQL-serveri vaikeseadistus." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Leiti MySQL-serveri vaikeseadistus, mis on loetav asukohas %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL-serveri vaikeseadistust ei leitud." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL-serveri vaikeseadistust ei leitud või ei olnud see loetab. Kontrolli, " +"kas Akonadi paigaldus on täielik ja sul on kõik vajalikud kasutamisõigused." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL-serveri kohandatud seadistus ei ole saadaval." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "MySQL-serveri kohandatud seadistust ei leitud, aga see on soovitatav." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Leiti MySQL-serveri kohandatud seadistus." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Leiti MySQL-serveri kohandatud seadistus, mis on loetav asukohas %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL-serveri kohandatud seadistus ei ole loetav." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL-serveri kohandatud seadistus leiti asukohas %1, aga see ei ole loetav. " +"Kontrolli oma kasutamisõigusi." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL-serveri seadistust ei leitud või ei olnud see loetav." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL-serveri seadistust ei leitud või ei olnud see loetav." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL-serveri seadistus on kasutatav." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL-serveri seadistus leiti asukohas %1 ja see on loetav." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Ühenduse loomine PostgreSQL-i serveriga nurjus." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Leiti PostgreSQL-i server." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Leiti PostgreSQL-i server ja ühendus töötab." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "Programmi akonadictl ei leitud" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Programm 'akonadictl' peab olema kättesaadav otsinguteel ($PATH). Kontrolli, " +"kas Akonadi server on ikka paigaldatud." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "Leiti akonadictl ja see on kasutatav" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Leiti Akonadi serverit juhtiv programm '%1' ja seda saab edukalt käivitada.\n" +"Tulemus:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "Leiti akonadictl, aga see ei ole kasutatav." + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Leiti Akonadi serverit juhtiv programm '%1', aga seda ei saa edukalt " +"käivitada.\n" +"Tulemus:\n" +"%2\n" +"Kontrolli, kas Akonadi server on korrektselt paigaldatud." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi juhtimise protsess on D-Busis registreeritud." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi juhtimise protsess on D-Busis registreeritud, mis tavaliselt " +"tähendab, et see töötab." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi juhtimise protsess ei ole D-Busis registreeritud." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi juhtimise protsess ei ole D-Busis registreeritud, mis tavaliselt " +"tähendab, et see ei ole käivitatud või tekkis käivitamisel saatuslik viga." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi serveri protsess on D-Busis registreeritud." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi serveri protsess on D-Busis registreeritud, mis tavaliselt tähendab, " +"et see töötab." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi serveri protsess ei ole D-Busis registreeritud." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi serveri protsess ei ole D-Busis registreeritud, mis tavaliselt " +"tähendab, et see ei ole käivitatud või tekkis käivitamisel saatuslik viga." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protokolli versiooni ei saa kontrollida." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Serveriga ühendust loomata ei saa kontrollida, kas protokolli versioon " +"vastab nõuetele." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Serveri protokolli versioon on liiga vana." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Serveri protokolli versioon on %1, aga klient nõuab on vähemalt %2. Kui oled " +"hiljaaegu uuendanud KDE PIM-i, palun käivita kindlasti uuesti nii Akonadi " +"kui ka KDE PIM-i rakendused." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Serveri protokolli versioon on liiga uus." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Serveri protokolli versioon ei sobi." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Aktiivne protokolli versioon on %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Leiti ressursi agendid." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Leiti vähemalt üks ressursi agent." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Ressursi agente ei leitud." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Ühtegi ressursi agenti ei leitud, kuid Akonadi vajab tööks vähemalt ühte. " +"See tähendab tavaliselt, et ressursi agente ei ole paigaldatud või on mingi " +"probleem häälestamisega. Otsiti läbi järgmised asukohad: '%1'. " +"Keskkonnamuutuja XDG_DATA_DIRS väärtuseks on määratud '%2', nii et " +"kontrolli, kas see hõlmab kõiki asukohti, kuhu on paigaldatud Akonadi " +"agendid." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Aktiivse Akonadi serveri vealogi ei leitud." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi server ei teatanud viimase käivitamise ajal ühestki veast." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Leiti aktiivse Akonadi serveri vealogi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi server teatas praeguse käivitamise ajal vigadest. Logi leiab " +"asukohast %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Varasemat Akonadi serveri vealogi ei leitud." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi server ei teatanud eelmise käivitamise ajal ühestki veast." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Leiti eelmine Akonadi serveri vealogi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi server teatas eelmise käivitamise ajal vigadest. Logi leiab " +"asukohast %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Aktiivse Akonadi juhtimise vealogi ei leitud." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadi juhtimise protsess ei teatanud viimase käivitamise ajal ühestki " +"veast." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Leiti aktiivne Akonadi juhtimise vealogi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi juhtimise protsess teatas praeguse käivitamise ajal vigadest. Logi " +"leiab asukohast %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Eelmist Akonadi juhtimise vealogi ei leitud." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi juhtimise protsess ei teatanud eekmise käivitamise ajal ühestki " +"veast." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Leiti eelmine Akonadi juhtimise vealogi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi juhtimise protsess teatas eelmise käivitamise ajal vigadest. Logi " +"leiab asukohast %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi käivitati administraatori õigustes" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Internetiga suhtlevate rakenduste käivitamine administraatori õigustes toob " +"kaasa hulgaliselt turberiske. Akonadi kasutatav MySQL ei luba nende ohtude " +"vältimiseks käivitada ennast administraatori õigustes." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi ei tööta administraatori õigustes" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi ei tööta administraatori õigustes, mis on turvalise süsteemi huvides " +"ka soovitatav." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Testi aruande salvestamine" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Tõrge" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Faili '%1' avamine nurjus" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi serveri käivitamisel tekkis viga. Järgmised enesetestid peaksid " +"aitama probleemi tuvastada ja lahendada. Abi paludes või veast teada andes " +"lisage alati ka see aruanne." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Üksikasjad" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Probleemide lahendamise kohta leiab rohkem nõuandeid leheküljelt userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Uus kaust..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Uus" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Kustuta kaust" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Kustuta" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Sünkrooni kaust" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sünkrooni" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Ka&usta omadused" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Omadused" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Aseta" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Aseta" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Halda kohalikke tellimu&si..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Halda kohalikke tellimusi" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Lisa lemmikkaustade hulka" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Lisa lemmikute hulka" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Eemalda lemmikkaustade seast" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Eemalda lemmikute seast" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Nimeta lemmik ümber..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Muuda nime" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopeeri kaust..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopeeri" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopeeri element..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Liiguta element..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Liiguta" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Liiguta kaust..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Lõika element" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Lõika" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Lõika kaust" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Loo ressurss" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Kustuta ressurss" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Ressursi omadused" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Sünkrooni ressurss" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Töötamine võrguta" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sünkrooni kaust rekursiivselt" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sünkrooni rekursiivselt" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Viska kaust prügikasti" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Viska kaust prügikasti" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Viska element prügikasti" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Viska element prügikasti" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Taasta kaust prügikastist" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Taasta kaust prügikastist" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Taasta element prügikastist" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Taasta element prügikastist" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Taasta kogu prügikastist" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Taasta kogu prügikastist" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sünkrooni lemmikkaustad" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sünkrooni lemmikkaustad" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sünkrooni kaustapuu" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopeeri kaust" +msgstr[1] "&Kopeeri %1 kausta" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopeeri element" +msgstr[1] "&Kopeeri %1 elementi" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Lõika element" +msgstr[1] "&Lõika %1 elementi" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Lõika kaust" +msgstr[1] "&Lõika %1 kausta" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Kustuta element" +msgstr[1] "&Kustuta %1 elementi" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Kustuta kaust" +msgstr[1] "&Kustuta %1 kausta" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sünkrooni kaust" +msgstr[1] "&Sünkrooni %1 kausta" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Kustuta ressurss" +msgstr[1] "&Kustuta %1 ressursi" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sünkrooni ressurss" +msgstr[1] "&Sünkrooni %1 ressurssi" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopeeri kaust" +msgstr[1] "Kopeeri %1 kausta" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopeeri element" +msgstr[1] "Kopeeri %1 elementi" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Lõika element" +msgstr[1] "Lõika %1 elementi" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Lõika kaust" +msgstr[1] "Lõika %1 kausta" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Kustuta element" +msgstr[1] "Kustuta %1 elementi" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Kustuta kaust" +msgstr[1] "Kustuta %1 kausta" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sünkrooni kaust" +msgstr[1] "Sünkrooni %1 kausta" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Kustuta ressurss" +msgstr[1] "Kustuta %1 ressursi" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sünkrooni ressurss" +msgstr[1] "Sünkrooni %1 ressurssi" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nimi" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Kas tõesti kustutada see kaust ja kõik selle alamkaustad?" +msgstr[1] "Kas tõesti kustutada %1 kausta ja kõik nende alamkaustad?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Kas kustutada kaust?" +msgstr[1] "Kas kustutada kaustad?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Kausta kustutamine nurjus: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Kausta kustutamine nurjus" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Kausta %1 omadused" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Kas tõesti kustutada valitud element?" +msgstr[1] "Kas tõesti kustutada %1 elementi?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Kas kustutada element?" +msgstr[1] "Kas kustutada elemendid?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Elemendi kustutamine nurjus: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Elemendi kustutamine nurjus" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Lemmikkausta nime muutmine" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nimi:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Uus ressurss" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Ressursi loomine nurjus: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Ressursi loomine nurjus" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Kas tõesti kustutada see ressurss?" +msgstr[1] "Kas tõesti kustutada %1 ressurssi?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Kas kustutada ressurss?" +msgstr[1] "Kas kustutada ressursid?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Andmete asetamine nurjus: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Asetamine nurjus" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Kausta nimesse ei saa lisada \"/\"." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Tõrge uue kausta loomisel" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Kausta nime algusse või lõppu ei saa lisada \".\"." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Enne kausta \"%1\" sünkroonimist peab ressurss olema võrgus. Kas minna võrku?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Konto \"%1\" ei ole võrgus" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Mine võrku" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Liiguta sellesse kausta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopeeri sellesse kausta" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Tellimuse uuendamine nurjus: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Tellimuse tõrge" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Kohalikud tellimused" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Otsing:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Ainult tellitud" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "T&elli" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Tü&hista tellimus" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Uue sildi loomine nurjus" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Tõrge uue sildi loomisel" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Kas tõesti eemaldada silt %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Sildi kustutamine" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Sildi kustutamine" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Rakendatavate siltide valimine." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Loo uus silt" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Siltide haldamine" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Siltide valimine ..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Siltide valimine" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Puhasta" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Klõpsa siltide lisamiseks" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi->XML teisendaja" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Akonadi kogu alampuu teisendamine XML-failiks." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009: Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Andmeid pole laaditud." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Failinime pole määratud" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Andmefaili \"%1\" avamine nurjus." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Faili %1 ei ole olemas." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Andmefaili \"%1\" parsimine nurjus." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Skeemi definitsiooni ei saa laadida ega parsida." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Skeemi parseri konteksti loomine nurjus." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Skeemi loomine nurjus." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Skeemi valideerimise konteksti loomine nurjus." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Vigane failivorming." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Andmefaili parsimine nurjus: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Kogu %1 ei leitud" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "Võrgu-ID" + +#~ msgid "MimeType" +#~ msgstr "MIME tüüp" + +#~ msgid "Form" +#~ msgstr "Vorm" + +#~ msgid "Default Name" +#~ msgstr "Vaikimisi nimi" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Kustuta" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Loobu" + +#~ msgid "Take left one" +#~ msgstr "Kasuta vasakpoolset" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Kaks uuendust on teineteisega konfliktis.Palun vali, millist " +#~ "uuendust rakendada." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Lugemata" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Kokku" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Suurus" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi ressurss" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nimi" + +#~ msgid "Invalid collection specified" +#~ msgstr "Määratud on vigane kogu" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Leiti protokolli versioon %1, oodati vähemalt %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Serveri protokoll on piisavalt uus." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Serveri protokolli versioon on %1, mis on sama või uuemgi kui nõutav %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Leiti ebaühtlane kohalike kogude puu." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Ette anti võrgukogu ilma juurest lähtuva eellaste ahelata, ressurss on " +#~ "vigane." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE testprogramm" + +#~ msgid "Cannot list root collection." +#~ msgstr "Juurkogu loendi loomine nurjus." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuki otsinguteenus on D-Busis registreeritud." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Nepomuki otsinguteenus on D-Busis registreeritud, mis tavaliselt " +#~ "tähendab, et see töötab." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuki otsinguteenus ei ole D-Busis registreeritud." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Nepomuki otsinguteenus ei ole D-Busis registreeritud, mis tavaliselt " +#~ "tähendab, et see ei ole käivitatud või tekkis käivitamisel saatuslik viga." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuki otsinguteenus kasutab sobimatut taustaprogrammi." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Nepomuki otsinguteenus kasutab \"%1\" taustaprogrammi, mille kasutamine " +#~ "koos Akonadiga ei ole soovitatav." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuki otsinguteenus kasutab sobivat taustaprogrammi." + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Nepomuki otsinguteenus kasutab mõnda soovitatavat taustaprogrammi." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Plugin \"%1\" ei ole ehitatud staatilisena, palun maini see ära ka " +#~ "veateates." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Plugin ei ole ehitatud staatilisena" + +#~ msgid "Fetch Job Error" +#~ msgstr "Töö hankimise tõrge" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Uus kaust..." + +#, fuzzy +#~| msgid "&Resource Properties" +#~ msgid "Resource Properties" +#~ msgstr "Ressursi omadused" + +#~ msgid "Cache" +#~ msgstr "Puhver" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Puhvri reeglid võetakse üle eellaselt" + +#~ msgid "Cache Policy" +#~ msgstr "Puhvri reeglid" + +#~ msgid "Interval check time:" +#~ msgstr "Kontrollimise intervall:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Kohalik puhvri aegumine:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Sünkroniseerimine vajaduse korral" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Määramine, milliseid kaustu näidata kaustapuus" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Otsing" + +#~ msgid "Available Folders" +#~ msgstr "Saadaolevad kaustad" + +#~ msgid "Current Changes" +#~ msgstr "Aktiivsed muudatused" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Lõpeta valitud kausta tellimine" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Akonadi server teatav käivitamisel vigadest faili %1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Akonadi juhtimise protsess teatas käivitamise ajal vigadest faili %1." + +#~ msgid "TODO" +#~ msgstr "TODO" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi ei tööta.
Üksikasjad...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi ressurss" + +#~ msgid "Nepomuk search service uses Sesame2 backend. " +#~ msgstr "Nepomuki otsinguteenus kasutab Sesame2 taustaprogrammi. " + +#, fuzzy +#~| msgid "no collection" +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "kogu puudub" +#~ msgstr[1] "kogu puudub" + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "&Kopeeri kaust" diff --git a/po/eu/akonadi_knut_resource.po b/po/eu/akonadi_knut_resource.po new file mode 100644 index 0000000..7532df8 --- /dev/null +++ b/po/eu/akonadi_knut_resource.po @@ -0,0 +1,87 @@ +# Translation for akonadi_knut_resource.po to Euskara/Basque (eu). +# Copyright (C) 2020, This file is copyright: +# This file is distributed under the same license as the akonadi package. +# KDE euskaratzako proiektuko arduraduna . +# +# Translators: +# Iñigo Salvador Azurmendi , 2020. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2020-09-20 09:36+0200\n" +"Last-Translator: Iñigo Salvador Azurmendi \n" +"Language-Team: Basque \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 20.08.1\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Ez da datu-fitxategirik hautatu." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "«%1» fitxategia ondo zamatu da." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Hautatu datu-fitxategia" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut datu-fitxategia" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Ez da elementurik aurkitu «%1» urruneko ID-rako" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Guraso-bilduma ez da aurkitu DOM zuhaitzean." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Ezin du bilduma idatzi." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Aldatutako bilduma ez da aurkitu DOM zuhaitzean." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Ezabatutako bilduma ez da aurkitu DOM zuhaitzean." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "«%1» guraso-bilduma ez da aurkitu DOM zuhaitzean." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Ezin du elementua idatzi." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Aldatutako elementua ez da aurkitu DOM zuhaitzean." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Ezabatutako elementua ez da aurkitu DOM zuhaitzean." diff --git a/po/eu/libakonadi5.po b/po/eu/libakonadi5.po new file mode 100644 index 0000000..94c2bd2 --- /dev/null +++ b/po/eu/libakonadi5.po @@ -0,0 +1,2601 @@ +# Translation for libakonadi5.po to Euskara/Basque (eu). +# Copyright (C) 2020-2021, This file is copyright: +# This file is distributed under the same license as the akonadi package. +# KDE euskaratzeko proiektuko arduraduna . +# +# Translators: +# Iñigo Salvador Azurmendi , 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-07-25 21:47+0200\n" +"Last-Translator: Iñigo Salvador Azurmendi \n" +"Language-Team: Basque \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 21.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Iñigo Salvador Azurmendi" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xalba@ni.eus" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Une honetan ez dago konfiguratutako konturik." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Kontuen integrazioa ez da onartzen" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Ezin da objektua dbus-en erregistratu: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%2 motako %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agentearen identifikatzailea" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadiren agentea" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Prest" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Lerroz kanpo" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sinkronizatzen..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Errorea." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Konfiguratu gabe" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Baliabide-identifikatzailea" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi baliabidea" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Baliogabeko elementu bat eskuratu da" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Errorea elementua sortzean: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Errorea bilduma eguneratzean: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Bilduma lokala eguneratzea huts egin du: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Elementu lokalak eguneratzea huts egin du: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Ezin duzu elementurik ekarri lerroz kanpoko moduan." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "«%1» karpeta sinkronizatzea" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Huts egin du bilduma sinkronizatzeko eskuratzeak." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Huts egin du atributuak sinkronizatzeko bilduma eskuratzeak." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Eskatutako elementua ez existitzen dagoeneko" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Lana bertan behera utzi da." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Ez dago halako bildumarik." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Konpondu gabeko bilduma umezurtzak aurkitu dira" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Ez da aurkitu gatazkak tratatzeko beste elementurik" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Ezin da sortutako agentearen D-Bus interfazera sartu." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Agentearen instantzia sortzeko denbora-muga gainditu da." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Ezin da «%1» agente mota lortu." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Ezin da agentearen instantzia sortu." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Bilduma-instantzia baliogabea." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Baliabide-instantzia baliogabea." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Ezin izan da «%1» baliabidearen D-Bus interfazea lortu" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Bildumako atributuak sinkronizatzea denbora-muga gainditu du." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Kopiatzeko bilduma baliogabea" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Jomugako bilduma baliogabea" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Guraso baliogabea" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Huts egin du erantzunetik bilduma sintaktikoki aztertzeak" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Bilduma baliogabea" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Emandako bilduma baliogabea da." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Ez da mugitu beharreko objekturik zehaztu" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Ez da jomuga baliodunik zehaztu" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Bilduma baliogabea." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Guraso bilduma baliogabea" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Ezin da Akonadi zerbitzura konektatu." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi zerbitzariaren protokoloaren bertsioa bateraezina da. Ziurtatu " +"bertsio bateragarri bat instalatuta duzula." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Erabiltzaileak eragiketa bertan behera utzi du." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Errore ezezaguna." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Ustekabeko erantzuna" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Huts egin du harremana sortzeak." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Baliabidea sinkronizatzeko denbora-muga gainditu da." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Ezin izan da «%1» baliabidearen erro-bilduma ekarri." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Ez da baliabide IDrik eman." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "«%1» baliabide-identifikatzaile baliogabea" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Huts egin du baliabide lehenetsia D-Bus bidez konfiguratzeak." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Huts egin du baliabide-bilduma ekartzeak." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Denbora-muga gainditu da giltzatzeko saiakeran." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Huts egin du etiketa sortzeak." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Zakarrontzi bildumara mugitzea huts egin du, zakarrontziko eragiketa " +"galarazten" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Elementu baliogabeak pasatu dira" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Bilduma baliogabea pasatu da" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Bilduma baliogabea edo elementu-zerrenda hutsik" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Ezin izan da aurkitu bilduma lehengoratzea eta baliabidea lehengoratzea ez " +"dago erabilgarri" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Izena" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Zamatzen..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Errorea" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Jomugako «%1» bildumak badu dagoeneko\n" +"«%2» izeneko bilduma bat." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Izena" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Ezin izan da kopiatu: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Ezin izan da bilduma kopiatu: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Ezin izan da elementua mugitu: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Ezin izan da bilduma mugitu: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Ezin izan da entitatea estekatu: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Errorea" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Gogoko karpetak" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Mezuak guztira" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Irakurri gabeko mezuak" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kuota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Biltegiratze-neurria" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Azpikarpetaren biltegiratze-neurria" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Irakurri gabea" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Guztira" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Neurria" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiketa" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Ezin izan da elementua indexatzeko ekarri" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indizea jada ez dago erabilgarri" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Zama erabilgarriaren «%1» zatia ez dago indize honetarako erabilgarri" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Indize honetarako ez dago saiorik erabilgarri" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Indize honetarako ez dago elementurik erabilgarri" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Izengabeko plugina" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Ez dago azalpen erabilgarririk" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi zerbitzariaren protokoloaren bertsioa ez dator bat aplikazio honek " +"erabiltzen duenarekin.\n" +"Zure sistema berriki eguneratu baduzu, saio-itxi eta atzera saio-hasi " +"aplikazio guztiek bertsio zuzena erabiltzen dutela ziurtatzeko." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Ez dago Akonadi-ko agenterik erabilgarri. Egiaztatu zure KDE PIMen " +"instalazioa." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Protokoloaren bertsioak ez datoz bat. Zerbitzariaren bertsioa zaharragoa da " +"(%1) gurea baino (%2). Zure sistema azkenaldian eguneratu baduzu, " +"berrabiarazi Akonadi-ren zerbitzaria." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Protokoloen bertsioak ez datoz bat. Zerbitzariaren bertsioa berriagoa da " +"(%1) gurea baino (%2). Zure sistema azkenaldian eguneratu baduzu, " +"berrabiarazi zure KDE PIMeko aplikazio guztiak." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi-ren auto-proba" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi-ren zerbitzariaren egoera egiaztatu eta jakinarazten du" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Agentearen instantzia &berria..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "E&zabatu agentearen instantzia" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Konfiguratu Agentearen instantzia" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Agentearen instantzia berria" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Ezin izan da agentearen instantzia berria sortu: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Agentearen instantzia sortzea huts egin du" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Agentearen instantzia ezabatu?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Benetan ezabatu nahi duzu hautatutako agentearen instantzia?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 konfigurazioa" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 eskuliburua" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "%1-(r)i buruz" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Konfiguratzeko elkarrizketa-koadroa beste leiho batean ireki da" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "«%1» konfiguratzekoa jada beste nonbait irekita dago." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Huts egin du «%1» konfiguratzeko elkarrizketa-koadroa erregistratzeak." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minutu" +msgstr[1] "minutu" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Berreskuratzea" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Erabili guraso karpetako edo kontuko aukerak" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sinkronizatu karpeta hau hautatzean" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatikoki sinkronizatu hau igarotakoan:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Inoiz ez" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutu" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokalean cacheratutako Atalak" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Berreskuratzeko aukerak" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Berreskuratu beti &mezu osoa" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Be&rreskuratu mezuen gorputzak eskatu ahala" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Mezuen gorputzak lokalki mantendu:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Betirako" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Bilatu" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Erabili lehenetsitako karpeta" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Azpikarpeta &berria..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Sortu azpikarpeta berri bat hautatuta dagoen karpetaren barruan" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Karpeta berria" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Izena" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Ezin du karpeta sortu: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Karpeta sortzea huts egin du" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Orokorra" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Objektu bat" +msgstr[1] "%1 objektu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "Ize&na:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Era&bili norberak finkatutako ikonoa:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "karpeta" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estatistikak" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Edukia:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objektu" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Neurria:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Gogoratu indexatzeak minutu batzuk beharko dituela." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Mantentzea" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Errorea indexatutako elementuen kopurua berreskuratzean" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Kapeta honetan elementu %1 indexatu da" +msgstr[1] "Karpeta honetan %1 elementu indexatu dira" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Indexatutako elementuak kalkulatzen..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fitxategiak" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Karpeta-mota:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "ezezaguna" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementuak" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elementuak guztira:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Irakurri gabeko elementuak:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexatzea" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Gaitu testu osoa indexatzea" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Indexatutako elementu kopurua berreskuratzen..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Berriz indexatu karpeta" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Karpetarik ez" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Ireki bilduma elkarrizketa-koadroa" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Hautatu bilduma bat" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mugitu hona" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopiatu hona" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Utzi" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Aldatze ordua" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Banderak" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atributua: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Gatazkak ebaztea" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Hartu nire bertsioa" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Hartu haien bertsioa" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mantendu bi bertsioak" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Zure aldaketek gatazka dute beste norbaitek bitartean egindako " +"aldaketekin.
Bertsioetako bat bota ezean, biak eskuz bateratu beharko " +"dituzu.
Egin klik «Ireki testu editorea» " +"aukeran testuen kopia bat gordetzeko, ondoren hautatu zein bertsio den " +"zuzenagoa, hura berriz ireki eta berriz aldatu falta dena gehitzeko." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Datuak" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi zerbitzaria abiatzen..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Gelditu Akonadi zerbitzaria..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mugitu hona" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopiatu hemen" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Es&tekatu hemen" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Utzi" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Ezin da Informazio pertsonala kudeatzeko zerbitzura konektatu.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Informazio pertsonala kudeatzeko zerbitzua abiatzen ari da..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Informazio pertsonala kudeatzeko zerbitzua itzaltzen ari da..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Informazio pertsonala kudeatzeko zerbitzua datu-basearen bertsio-berritze " +"bat gauzatzen ari da." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Informazio pertsonala kudeatzeko zerbitzua datu-basearen bertsio-berritze " +"bat gauzatzen ari da.\n" +"Hori software eguneratze baten ondoren gertatu ohi da, eta errendimendua " +"optimizatzeko beharrezkoa da.\n" +"Informazio pertsonal kopuruaren arabera, baliteke minutu batzuk behar izatea." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi, informazio pertsonala kudeatzeko zerbitzua ez dago martxan. " +"Aplikazio hau ezin da erabili hura gabe." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Hasi" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi, informazio pertsonala kudeatzeko azpiegitura ez dabil.\n" +"Egin klik «Xehetasunak...» aukeran, arazo honi buruzko informazio zehatza " +"eskuratzeko." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi, informazio pertsonala kudeatzeko zerbitzua ez dabil." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "zehaztasunak..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "«%1» kontua kendu nahi duzu?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Kendu kontua?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Sarrerako kontuak (gehitu bat gutxienez):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Gehitu..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Al&datu..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Kendu" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Berrabiarazi" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Azkenaldiko karpeta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Berrizendatu gogokoa" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Izena:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi Zerbitzariaren auto-proba" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Gorde txostena..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopiatu txostena arbelera" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Zure Akonadi zerbitzariaren uneko konfigurazioak «%1» QtSQL gidaria behar du " +"eta sisteman aurkitu da." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Zure Akonadi zerbitzariaren uneko konfigurazioak «%1» QtSQL gidaria behar " +"du.\n" +"Ondoko gidariak instalatuta daude: %2.\n" +" Ziurtatu beharrezkoa den gidaria instalatuta dagoela." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Datu-base gidaria aurkitu da." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Datu-base gidaria ez da aurkitu." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL zerbitzariaren exekutagarria ez da probatu." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Uneko konfigurazioak ez du behar barneko MySQL zerbitzari bat." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Une honetan konfiguratuta duzu Akonadik «%1» MySQL zerbitzaria erabiltzea.\n" +"Ziurta ezazu MySQL zerbitzaria instalatuta duzula, ezarri dagokion bide-" +"izena eta ziurtatu zerbitzariko exekutagarrian beharrezko irakurtzeko eta " +"exekutatzeko baimenak dituzula." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL zerbitzaria ez da aurkitu." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL zerbitzaria ezin da irakurri." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL zerbitzaria ezin da exekutatu." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL aurkitu da ustekabeko izenarekin." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL zerbitzaria aurkitu da." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL zerbitzaria aurkitu da: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL zerbitzaria exekutagarria da." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"«%1» MySQL zerbitzaria exekutatzea ondoko errore mezuarekin huts egin du: " +"«%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL zerbitzaria exekutatzea huts egin du." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL zerbitzariaren erroreen egunkaria ez da probatu." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Ez da aurkitu MySQL erroreen egunkaririk." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL zerbitzariak abio honetan ez du erroreen berri eman. Egunkaria hemen " +"aurkitu dezakezu, «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL erroreen egunkaria ezin da irakurri." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"MySQL zerbitzariaren erroreen egunkari-fitxategi bat aurkitu da, baina ezin " +"da irakurri: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL zerbitzariaren egunkariak erroreak ditu." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL zerbitzariaren «%1» erroreen egunkari fitxategiak erroreak ditu." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL zerbitzariaren egunkariak abisuak ditu." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL zerbitzariaren «%1» egunkari-fitxategiak abisuak ditu." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL zerbitzariaren egunkariak ez du errorerik." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"MySQL zerbitzariaren «%1» egunkari-fitxategiak ez du errorerik ez eta " +"abisurik ere." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL zerbitzariaren konfigurazioa ez da probatu." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL zerbitzariaren konfigurazio lehenetsia aurkitu da." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"MySQL zerbitzariaren konfigurazioa aurkitu da eta irakurgai dago hemen, «%1»." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL zerbitzariaren konfigurazio lehenetsia ez da aurkitu." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL zerbitzariaren konfigurazio lehenetsia ez da aurkitu edo ezin da " +"irakurri. Egiaztatu zure Akonadi-ren instalazioa osorik dagoela eta " +"huraatzitzeko behar diren baimen guztiak dituzula." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL zerbitzariaren konfigurazio pertsonalizatua ez dago erabilgarri." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Ez da aurkitu MySQL zerbitzariaren konfigurazio pertsonalizaturik, hautazkoa " +"da ordea." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL zerbitzariaren konfigurazio pertsonalizatua aurkitu da." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"MySQL zerbitzariaren konfigurazio pertsonalizatua aurkitu da eta hemen " +"irakur daiteke, %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL zerbitzariaren konfigurazio pertsonalizatua ezin da irakurri." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL zerbitzariaren konfigurazio pertsonalizatua «%1»(e)n aurkitu da, baina " +"ezin da irakurri. Egiaztatu hura atzitzeko dituzun baimenak." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL zerbitzariaren konfigurazioa ez da aurkitu edo ezin da irakurri." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL zerbitzariaren konfigurazioa ez da aurkitu edo ezin da irakurri." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL zerbitzariaren konfigurazioa erabil daiteke." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"MySQL zerbitzariaren konfigurazioa «%1»(e)an aurkitu da eta irakur daiteke." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Ezin da konektatu PostgreSQL zerbitzarira." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL zerbitzaria aurkitu da." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL zerbitzaria aurkitu da eta konexioa badabil." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl ez da aurkitu" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"«akonadictl» programa zure $PATH bidez eskuragarri egon behar da. Ziurtatu " +"ezazu Akonadi zerbitzaria instalatuta duzula." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl aurkitu da eta erabilgarria da" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi zerbitzaria kontrolatzeko «%1» programa aurkitu da eta arazorik gabe " +"exekutatu ahal izan da.\n" +"Emaitza:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl aurkitu da baino ez da erabilgarria" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi zerbitzaria kontrolatzeko «%1» programa aurkitu da baina ezin izan " +"da behar bezala exekutatu.\n" +"Emaitza:\n" +"%2\n" +"Ziurtatu Akonadi zerbitzaria behar bezala instalatuta dagoela." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi kontrolatzeko prozesua D-Bus-en erregistratuta dago." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi kontrolatzeko prozesua D-Bus-en erregistratuta dago, horrek " +"badabilela adierazi ohi du." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi kontrolatzeko prozesua ez dago D-Bus-en erregistratuta." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi kontrolatzeko prozesua ez dago D-Bus-en erregistratuta, horrek " +"adierazi ohi du ez dela abiatu edo abiatzean errore guztiz kaltegarri bat " +"izan duela." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi zerbitzariaren prozesua D-Bus-en erregistratuta dago." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi prozesua D-Bus-en erregistratuta dago, horrek badabilela adierazi " +"ohi du." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi zerbitzariaren prozesua ez dago D-Bus-en erregistratuta." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi zerbitzariaren prozesua ez dago D-Bus-en erregistratuta, horrek " +"adierazi ohi du ez dela abiatu edo abiatzean errore guztiz kaltegarri bat " +"izan duela." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protokoloaren bertsioa ezin da egiaztatu." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Zerbitzarirako konexiorik gabe ezin da egiaztatu protokoloaren bertsioak " +"eskakizunak betetzen dituen." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Zerbitzariaren protokoloaren bertsioa zaharregia da." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Zerbitzariaren protokoloaren bertsioa %1 da, baina bezeroak %2 bertsioa " +"eskatzen du. Berriki KDE PIM eguneratu baduzu, ziurtatu biak, Akonadi eta " +"KDE PIM aplikazioak, berrabiarazten dituzula." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Zerbitzariaren protokoloaren bertsioa berriegia da." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Zerbitzariaren protokoloaren bertsioa bat dator." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Protokoloaren uneko bertsioa %1 da." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Baliabide agentea aurkitu dira." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Gutxienez baliabide-agente bat bilatu da." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Ez da baliabide agenterik aurkitu." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Ez da aurkitu baliabide-agenterik, Akonadik erabilgarria izateko gutxienez " +"bat behar du. Honek adierazi ohi du ez dagoela instalatutako baliabide-" +"agenterik edo ezarpen arazo bat dagoela. Ondoko bide-izenetan bilatu da: " +"«%1». XDG_DATA_DIRS ingurune aldagaia «%2» ezarrita dago; ziurta ezazu " +"Akonadi-agenteak instalatuta dauden bide-izen guztiak barnean dituela." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" +"Ez da aurkitu Akonadi zerbitzariaren uneko erroreen egunkari-sarrerarik." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi zerbitzariak ez du abio honetan errorerik jakinarazi." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Akonadi zerbitzariaren uneko erroreen egunkari-sarrerak aurkitu dira." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi zerbitzariak erroreak jakinarazi ditu abio honetan. Egunkaria hemen " +"auki daiteke, «%1»." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" +"Ez da aurkitu Akonadi zerbitzariaren aurreko erroreen egunkari-sarrerarik." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi zerbitzariak ez zuen errorerik jakinarazi aurreko abioan." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" +"Akonadi zerbitzariaren aurreko erroreen egunkari-sarrerak aurkitu dira." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi zerbitzariak aurreko abioan erroreak jakinarazi zituen. Egunkaria " +"hemen aurki daiteke, «%1»." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" +"Ez da aurkitu Akonadi kontrolaren uneko erroreen egunkari-sarrerarik aurkitu." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadi kontrolatzeko prozesuak ez du uneko abioan errorerik jakinarazi." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Akonadi kontrolatzeko uneko erroreen egunkari-sarrerak aurkitu dira." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi kontrolatzeko prozesuak erroreak jakinarazi ditu uneko abioan. " +"Egunkaria hemen aurki daiteke, «%1»." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" +"Ez da aurkitu Akonadi kontrolatzeko aurreko erroreen egunkari-sarrerarik." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi kontrolatzeko prozesuak ez du aurreko abioan errorerik jakinarazi." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Akonadi kontrolatzeko aurreko erroreen egunkari-sarrerak aurkitu dira." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi kontrolatzeko prozesuak erroreak jakinarazi ditu aurreko abioan. " +"Egunkaria hemen aurki daiteke, «%1»." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi «root» gisa abiarazi da" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Internetera begira dauden aplikazioak «root»/administratzaile gisa ibiltzea " +"segurtasun arrisku ugariren aurrean babes gabe uzten zaitu. MySQL, Akonadi-" +"ren instalazio honek erabiltzen duena, ez du onartuko «root» gisa ibiltzea, " +"zu arrisku hauetatik babeste aldera." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi ez da «root» gisa ibiltzen ari" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi ez da «root»/administratzaile erabiltzaile gisa ibiltzen ari, " +"sistema seguru baterako ezarpen gomendatua." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Gorde probaren txostena" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Errorea" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Ezin du «%1» fitxategia ireki" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Errore bat gertatu da Akonadi zerbitzaria abiatzean. Ondoko auto-probek " +"arazo honen zantzuak bilatu eta konpontzen lagundu beharko lukete. Euskarria " +"eskatzean edo akatsen berri ematean, mesedez, erantsi txosten hau." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Zehaztasunak" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Arazoak konpontzeko argibide gehiago hemen, userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Karpeta &berria..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Berria" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Eza&batu karpeta" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Ezabatu" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sinkronizatu karpeta" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sinkronizatzea" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Karpetaren &propietateak" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propietateak" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Itsatsi" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Itsatsi" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Kudeatu &harpidetza lokalak..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Kudeatu harpidetza lokalak" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Gehitu gogoko karpetetara" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Gehitu gogokoetara" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Kendu gogoko kapetetatik" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Kendu gogokoetatik" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Berrizendatu gogokoa..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Berrizendatu" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopiatu karpeta hona..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopiatu hona" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopiatu elementua hona..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mugitu elementua hona..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mugitu hona" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mugitu karpeta hona..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Eba&ki elementua" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Ebaki" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Eba&ki karpeta" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Sortu baliabidea" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Ezabatu baliabidea" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Baliabidearen propietateak" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sinkronizatu baliabidea" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Lerroz kanpo lan egitea" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sinkronizatu karpeta era errekurtsiboan" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sinkronizatze errekurtsiboa" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mugitu karpeta zakarrontzira" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mugitu karpeta zakarrontzira" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Bota elementua zakarrontzira" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Bota elementua zakarrontzira" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Lehengoratu karpeta zakarrontzitik" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Lehengoratu karpeta zakarrontzitik" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Lehengoratu elementua zakarrontzitik" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Lehengoratu elementua zakarrontzitik" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Lehengoratu bilduma zakarrontzitik" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Lehengoratu bilduma zakarrontzitik" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sinkronizatu gogoko karpetak" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sinkronizatu gogoko karpetak" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sinkronizatu karpeta-zuhaitza" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopiatu karpeta" +msgstr[1] "&Kopiatu %1 karpeta" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopiatu elementua" +msgstr[1] "&Kopiatu %1 elementu" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Eba&ki elementua" +msgstr[1] "Eba&ki %1 elementu" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Eba&ki karpeta" +msgstr[1] "Eba&ki %1 karpeta" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Ezabatu elementua" +msgstr[1] "&Ezabatu %1 elementu" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Eza&batu karpeta" +msgstr[1] "Eza&batu %1 karpeta" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sinkronizatu karpeta" +msgstr[1] "&Sinkronizatu %1 karpeta" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Ezabatu baliabidea" +msgstr[1] "&Ezabatu %1 baliabide" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sinkronizatu baliabidea" +msgstr[1] "&Sinkronizatu %1 baliabide" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopiatu karpeta" +msgstr[1] "Kopiatu %1 karpeta" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopiatu elementua" +msgstr[1] "Kopiatu %1 elementu" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Ebaki elementua" +msgstr[1] "Ebaki %1 elementu" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Ebaki karpeta" +msgstr[1] "Ebaki %1 karpeta" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Ezabatu elementua" +msgstr[1] "Ezabatu %1 elementu" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Ezabatu karpeta" +msgstr[1] "Ezabatu %1 karpeta" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sinkronizatu karpeta" +msgstr[1] "Sinkronizatu %1 karpeta" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Ezabatu baliabidea" +msgstr[1] "Ezabatu %1 baliabide" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sinkronizatu baliabidea" +msgstr[1] "Sinkronizatu %1 baliabide" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Izena" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Benetan ezabatu nahi duzu karpeta hau eta haren azpikarpeta guztiak?" +msgstr[1] "" +"Benetan ezabatu nahi dituzu %1 karpeta eta haren azpikarpeta guztiak?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Karpeta ezabatu?" +msgstr[1] "Karpetak ezabatu?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Ezin izan da karpeta ezabatu: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Karpeta ezabatzea huts egin du" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "«%1» karpetaren propietateak" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Benetan ezabatu nahi duzu hautatutako elementua?" +msgstr[1] "Benetan ezabatu nahi dituzu %1 elementu?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Elementua ezabatu?" +msgstr[1] "Elementuak ezabatu?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Ezin izan du elementua ezabatu: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Elementua ezabatzea huts egin du" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Berrizendatu gogokoa" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Izena:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Baliabide berria" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Ezin izan da baliabidea sortu: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Baliabide sorrerak huts egin du" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Benetan ezabatu nahi duzu baliabide hau?" +msgstr[1] "Benetan ezabatu nahi dituzu %1 baliabide?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Baliabidea ezabatu?" +msgstr[1] "Baliabideak ezabatu?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Ezin izan da daturik itsatsi: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Itsastea huts egin du" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Ezin da «/» gehitu karpeta baten izenean." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Karpeta berria sortzean errorea" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Ezin da «.» gehitu karpeta-izen baten hasieran edo bukaeran." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"«%1» karpeta sinkronizatu aurretik, baliabidea lerron egon behar da. Lerroan " +"ipini nahi duzu?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "«%1» kontua lerroz kanpo dago" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Konektatu" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mugitu karpeta honetara" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopiatu karpeta honetara" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Huts egin du harpidetza eguneratzeak: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Harpidetza errorea" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Harpidetza lokalak" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Bilatu:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Harpidetuta soilik" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Harpidetzea" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Harpidetza &kentzea" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Huts egin du etiketa berri bat sortzeak" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Errore bat gertatu da etiketa berri bat sortzean" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Benetan kendu nahi duzu %1 etiketa?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Ezabatu etiketa" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Ezabatu etiketa" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Hautatu aplikatu beharreko etiketak." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Sortu etiketa berria" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Kudeatu etiketak" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Hautatu etiketak..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Hautatu etiketak" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Garbitu" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Egin klik etiketak gehitzeko" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi-tik XMLra bihurtzailea" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Akonadi bilduma azpi-zuhaitz bat XML fitxategi bihurtzen du." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Ez da daturik zamatu." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Ez da fitxategi-izenik zehaztu." + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Ezin izan du «%1» datu-fitxategia ireki." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "%1 fitxategia ez da existitzen." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Ezin izan da «%1» datu-fitxategiaren sintaxia aztertu." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" +"Eskemaren definizioa ezin izan da zamatu eta sintaxi-azterketa ez du " +"gainditu." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Ezin izan da sintaxi-azterketarako testuingurua sortu." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Ezin izan da eskema sortu." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Ezin izan da eskema balioztatzeko testuingurua sortu." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Fitxategi-formatu baliogabea." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Ezin da datu-fitxategiaren sintaxia aztertu: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Ezin izan da «%1» bilduma aurkitu" diff --git a/po/fi/akonadi_knut_resource.po b/po/fi/akonadi_knut_resource.po new file mode 100644 index 0000000..9d1a1d8 --- /dev/null +++ b/po/fi/akonadi_knut_resource.po @@ -0,0 +1,93 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Tommi Nieminen , 2010, 2011. +# Lasse Liehu , 2014. +# +# KDE Finnish translation sprint participants: +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2014-02-24 08:54+0200\n" +"Last-Translator: Lasse Liehu \n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-POT-Import-Date: 2012-12-01 22:24:47+0000\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Datatiedostoa ei ole valittu." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Tiedosto ”%1” ladattu onnistuneesti." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Datatiedoston valinta" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut -datatiedosto" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Etätunnisteelle %1 ei löytynyt kohdetta" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Emokokoelmaa ei löytynyt DOM-puusta." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Kokoelmaa ei voida kirjoittaa." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Muutettua kokoelmaa ei löytynyt DOM-puusta." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Poistettua kokoelmaa ei löytynyt DOM-puusta." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Emokokoelmaa ”%1” ei löytynyt DOM-puusta." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Kohdetta ei voi kirjoittaa." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Muutettua kohdetta ei löytynyt DOM-puusta." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Poistettua kohdetta ei löytynyt DOM-puusta." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Knut-datatiedoston sijainti." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Älä muuta tietoja taustaosassa." diff --git a/po/fi/libakonadi5.po b/po/fi/libakonadi5.po new file mode 100644 index 0000000..7d07b83 --- /dev/null +++ b/po/fi/libakonadi5.po @@ -0,0 +1,2662 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Tommi Nieminen , 2010, 2011, 2013, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +# Lasse Liehu , 2011, 2012, 2013, 2014, 2015, 2016, 2018. +# +# KDE Finnish translation sprint participants: +# Author: Artnay +# Author: Lliehu +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-05-06 17:12+0300\n" +"Last-Translator: Tommi Nieminen \n" +"Language-Team: Finnish \n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-POT-Import-Date: 2012-12-01 22:25:18+0000\n" +"X-Generator: Lokalize 20.04.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Tommi Nieminen" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "translator@legisign.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Tilejä ei ole asetettu." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Tilien yhdistämistä ei tueta" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Ei saatu rekisteröidyksi oliota DBusiin: %1" + +# Esim. ”IMAP-sähköpostipalvelin [käyttäjän antama nimi]” +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%2 %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agentin tunniste" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-agentti" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Valmis" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Ei verkossa" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synkronoidaan…" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Virhe." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Ei määritetty" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Resurssin tunniste" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-resurssi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Saatiin virheellinen tietue" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Virhe luotaessa tietuetta: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Virhe virkistettäessä kokoelmaa: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Paikallisen kokoelman virkistäminen epäonnistui: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Paikallisten tietueiden päivittäminen epäonnistui: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Tietuetta ei voi noutaa yhteydettömässä tilassa." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synkronoidaan kansiota ”%1”" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Kokoelmaa ei saatu noudetuksi synkronointia varten." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Kokoelmaa ei saatu noudetuksi määritteen synkronointia varten." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Pyydettyä kohdetta ei ole enää olemassa" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Työ peruttiin." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Kokoelmaa ei löydy." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Löytyi ratkaisemattomia orpokokoelmia" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Ei löytynyt muita kohtia ristiriidan käsittelyyn" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Luodun agentin DBus-liittymään ei saada yhteyttä." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Agentti-instanssin luomisen aikakatkaisu." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Agentin tyyppiä ”%1” ei voi saada." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Agentti-instanssi ei voitu luoda." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Virheellinen kokoelmainstanssi." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Virheellinen resurssin instanssi." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "D-Bus-liitäntää ei saatu resurssille ”%1”" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Kokoelman määritteiden synkronoinnin aikakatkaisu." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Virheellinen kopioitava kokoelma" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Virheellinen kohdekokoelma" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Virheellinen emo" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Vastauksesta saadun kokoelman jäsentäminen epäonnistui" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Virheellinen kokoelma" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Annettu virheellinen kokoelma." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Siirrettäviä objekteja ei määritetty" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Kelvollista kohdetta ei määritetty" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Virheellinen kokoelma." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Virheellinen emokokoelma" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Akonadi-palveluun ei saada yhteyttä." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi-palvelimen yhteyskäytäntöversio on yhteensopimaton. Varmista, että " +"sinulla on yhteensopiva versio asennettuna." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Käyttäjä perui toiminnon." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Tuntematon virhe." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Odottamaton vastaus" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Suhteen luonti epäonnistui." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Resurssin synkronoinnin aikakatkaisu" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Resurssin %1 juurikokoelmaa ei saatu noudetuksi." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Resurssin tunnistetta ei annettu." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Virheellinen resurssin tunniste ”%1”" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Oletusresurssia ei voitu määrittää D-Busin kautta." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Resurssikokoelmaa ei saatu noudetuksi." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Aikakatkaisu yritettäessä lukitusta." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Luokituksen luonti epäonnistui." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Roskakorikokoelmaan siirtäminen epäonnistui, keskeytetään roskakoriin siirto" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Annettiin virheellisiä tietueita" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Annettiin virheellinen kokoelma" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Ei kelvollista kokoelmaa tai tietueluettelo on tyhjä" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Palautuskokoelmaa ei löydetty ja palautusresurssi ei ole saatavilla" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nimi" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Ladataan…" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Virhe" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Kohdekokoelma ”%1” sisältää jo kokoelman,\n" +"jonka nimi on ”%2”." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nimi" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Kohdetta ei voitu kopioida: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kokoelmaa ei voitu kopioida: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Kohdetta ei voitu siirtää: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Kokoelmaa ei voitu siirtää: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Entiteettiä ei voitu linkittää: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Virhe" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Suosikkikansiot" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Viestejä kaikkiaan" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Viestejä lukematta" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Tilarajoitus" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Tallennuskoko" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Alikansioiden tallennuskoko" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Lukematta" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Kaikkiaan" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Koko" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Luokitus" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Tietuetta ei saada noudettua indeksiksi" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indeksiä ei ole enää saatavilla" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Tälle indeksille ei ole saatavilla sisältöosaa ”%1”" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Tässä indeksissä ei ole saatavilla istuntoa" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Tässä indeksissä ei ole saatavilla tietueita" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Nimetön liitännäinen" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Ei kuvausta saatavilla" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi-palvelimen yhteyskäytäntöversio eroaa sovelluksen käyttämästä " +"versiosta.\n" +"Jos olet vastikään päivittänyt järjestelmän, kirjaudu ulos ja takaisin " +"sisään varmistaaksesi, että kaikki sovellukset käyttävät oikeaa " +"yhteyskäytäntöversiota." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "Akonadi-agentteja ei ole saatavilla. Tarkista KDE PIM -asennuksesi." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Yhteyskäytäntöversiot eivät täsmää. Palvelimen versio on vanhempi (%1) kuin " +"ohjelman (%2). Jos olet vastikään päivittänyt järjestelmän, käynnistä " +"Akonadi-palvelin uudelleen." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Yhteyskäytäntöversiot eivät täsmää. Palvelimen versio on uudempi (%1) kuin " +"ohjelman (%2). Jos olet vastikään päivittänyt järjestelmän, käynnistä kaikki " +"KDE:n PIM-sovellukset uudelleen." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadin itsetestaus" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Tarkastaa ja ilmoittaa Akonadi-palvelimen tilan" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Uusi agentti-instanssi…" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Poista agentti-instanssi" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Muokkaa agentti-instanssia" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Uusi agentti-instanssi" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Agentti-instanssia ei voitu luoda: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Agentti-instanssin luominen epäonnistui" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Poistetaanko agentti-instanssi?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Haluatko varmasti poistaa valitun agentti-instanssin?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Asetukset: %1|/|$[gen %1] asetukset" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1-käyttöohje|/|$[gen %1] käyttöohje" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Tietoa: %1|/|Tietoa $[yleisnimi_pienellä $[elat %1] ]" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Toiseen ikkunaan on avattu asetusikkuna" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" +"%1-asetukset on avattu jo toisaalla.|/|$[gen %1] asetukset on avattu jo " +"toisaalla." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" +"%1-asetusikkunan rekisteröinti epäonnistui.|/|$[gen %1] asetusikkunan " +"rekisteröinti epäonnistui." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuutti" +msgstr[1] "minuuttia" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Nouto" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Käytä ylemmän kansion tai tilin asetuksia" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synkronoi valittaessa tämä kansio" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synkronoi automaattisesti ajassa:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Ei koskaan" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuuttia" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Välimuistissa olevat osat" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Noutoasetukset" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Nouda aina koko &viestit" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Nouda viesti&rungot pyydettäessä" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Säilytä viestirungot paikallisesti:" + +# Oletus: liittyy edeltävään id:hen ”Keep message bodies locally for:” +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Aina" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Etsi" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Käytä kansiota oletusarvoisesti" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Uusi alikansio…" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Luo uusi alikansio valitun kansion alle" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Uusi kansio" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nimi" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Kansiota ei voitu luoda: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Kansion luominen epäonnistui" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Yleistä" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Yksi objekti" +msgstr[1] "%1 objektia" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nimi:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Käytä &omaa kuvaketta:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "kansio" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Tilastot" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Sisältö:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objektia" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Koko:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 tavua" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Huomaa, että indeksointi kestää joitakin minuutteja." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Ylläpito" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Virhe noudettaessa indeksoitujen tietueiden määrää" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indeksoi tästä kansiosta %1 tietue" +msgstr[1] "Indeksoi tästä kansiosta %1 tietuetta" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Lasketaan indeksoituja tietueita…" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Tiedostot" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Kansion tyyppi:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "tuntematon" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Tietueita" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Tietueita kaikkiaan:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Tietueita lukematta:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indeksoidaan" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Salli koko tekstin indeksointi" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Noudetaan indeksoitujen tietueiden määrää…" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indeksoi kansio uudelleen" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ei kansioita" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Avaa kokoelmaikkuna" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Valitse kokoelma" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Siirrä tähän" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopioi tähän" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Peru" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Muutosaika" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Liput" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Määrite: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Ristiriidan ratkaisu" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Käytä minun versiotani" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Käytä heidän versiotaan" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Säilytä kumpikin versio" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Muutoksesi ovat ristiriidassa samaan aikaan toisaalla tehtyjen kanssa." +"
Ellei toista versiota hylätä, muutokset täytyy yhdistää käsin." +"
Säilytä teksteistä kopio napsauttamalla Avaa " +"tekstimuokkain, valitse oikea versio, avaa se uudelleen ja muokkaa sitä " +"lisätäksesi puuttuvat tiedot." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Data" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Käynnistetään Akonadi-palvelinta…" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Pysäytetään Akonadi-palvelinta…" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Siirrä tähän" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopioi tähän" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Linkitä tähän" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "Per&u" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Ei saada yhteyttä PIM-palveluun.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Henkilökohtaisten tietojen hallintapalvelu käynnistyy…" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Henkilökohtaisten tietojen hallintapalvelu sulkeutuu…" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Henkilökohtaisten tietojen hallintapalvelu päivittää tietokantaa." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Henkilökohtaisten tietojen hallintapalvelu päivittää tietokantaa. Näin käy\n" +"ohjelmistopäivityksen jälkeen, ja se on tarpeen suorituskyvyn " +"optimoimiseksi.\n" +"Päivityksen nopeus riippuu henkilökohtaisten tietojen määrästä." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi, henkilökohtaisten tietojen hallintapalvelu, ei ole käynnissä.\n" +"Tätä sovellusta ei voi käyttää ilman sitä." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Käynnistä" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi, henkilökohtaisten tietojen hallintakehys, ei ole toiminnassa.\n" +"Saat ongelmasta lisätietoa napsauttamalla ”Yksityiskohdat”." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi, henkilökohtaisten tietojen hallintakehys, ei ole toiminnassa." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Yksityiskohdat…" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Haluatko poistaa tilin ”%1”?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Poistetaanko tili?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Saapuvien viestien tilit (lisää ainakin yksi):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Lisää…" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Muuta…" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Poista" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Käynnistä uudelleen" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Viimeaikainen kansio" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Muuta suosikin nimeä" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nimi:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi-palvelimen itsetestaus" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Tallenna raportti…" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopioi raportti leikepöydälle" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Nykyinen Akonadi-palvelinmäärityksesi vaatii QtSQL-ajuria ”%1”, jota ei " +"löydy järjestelmäsi." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Nykyinen Akonadi-palvelinmäärityksesi vaatii QtSQL-ajuria ”%1”.\n" +"Seuraavat ajurit on asennettu: %2.\n" +"Varmista, että vaadittu ajuri on asennettu." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Tietokanta-ajuri löytynyt." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Tietokanta-ajuria ei löytynyt." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL-palvelinohjelmaa ei testattu." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Nykyinen määritys ei vaadi sisäistä MySQL-palvelinta." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Olet määrittänyt Akonadin käyttämään MySQL-palvelinta ”%1”.\n" +"Varmista, että MySQL-palvelin on asennettu, sijainti on asetettu oikein ja " +"että sinulla on tarvittavat luku- ja suoritusoikeudet palvelinohjelmaan. " +"Palvelinohjelman nimi on tavallisesti ”mysqld”, ja sen sijainti vaihtelee " +"jakeluversioittain." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-palvelinta ei löytynyt." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-palvelinta ei voi lukea." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-palvelin ei ole ohjelma." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Odottamattoman niminen MySQL löytyi." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL-palvelin löytyi." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL-palvelin löytyi: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-palvelin on ohjelma." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"MySQL-palvelimen ”%1” suoritus päättyi epäonnistuneesti seuraavaan " +"virheilmoitukseen: ”%2”" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL-palvelimen suorittaminen epäonnistui." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL-palvelimen virhelokia ei testattu." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Nykyistä MySQL-virhelokia ei löytynyt." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL-palvelin ei raportoinut virheistä tässä käynnistyksessä. Loki löytyy " +"sijainnista ”%1”." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-virheloki ei ole luettavissa." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "MySQL-palvelimen virheloki löytyi mutta ei ole luettavissa: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-palvelinloki sisältää virheitä." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL-palvelimen virheloki ”%1” sisältää virheitä." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-palvelinloki sisältää varoituksia." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL-palvelinloki ”%1” sisältää varoituksia." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-palvelinloki ei sisällä virheitä." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL-palvelinloki ”%1” ei sisällä virheitä tai varoituksia." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-palvelimen määritystä ei testattu." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL-palvelimen oletusmääritys löytyi." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "MySQL-palvelimen oletusmääritys löytyi ja on luettavissa (%1)." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL-palvelimen oletusmääritystä ei löytynyt." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL-palvelimen oletusmääritystä ei löytynyt tai se ei ole luettavissa. " +"Tarkista, että Akonadi-asennus on täydellinen ja että sinulla on tarvittavat " +"käyttöoikeudet." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL-palvelimen mukautettu määritys ei ole saatavilla." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "MySQL-palvelimen valinnaista mukautettua määritystä ei löytynyt." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL-palvelimen mukautettu määritys löytyi." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "MySQL-palvelimen mukautettu määritys löytyi (%1) ja on luettavissa." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL-palvelimen mukautettu määritys ei ole luettavissa." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL-palvelimen mukautettu määritys löytyi (%1), mutta se ei ole " +"luettavissa. Tarkista käyttöoikeutesi." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL-palvelimen määritystä ei löytynyt tai se ei ole luettavissa." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL-palvelimen määritystä ei löytynyt tai se ei ole luettavissa." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL-palvelimen määritys on käyttökelpoinen." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL-palvelimen määritys löytyi (%1) ja on luettavissa." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL-palvelimeen ei saada yhteyttä." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL-palvelin löytyi." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL-palvelin löytyi ja yhteys toimii." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl:ää ei löytynyt" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Ohjelman ”akonadictl” tulee löytyä hakupolun ($PATH) varrelta. Tarkista, " +"onko Akonadi-palvelin asennettu." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl löytyi ja on käytettävissä" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi-palvelinta hallitseva ohjelma ”%1” löytyi ja voitiin suorittaa.\n" +"Tulos:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl löytyi muttei ole käytettävissä" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi-palvelinta hallitseva ohjelma ”%1” löytyi, mutta sitä ei onnistuttu " +"suorittamaan.\n" +"Tulos:\n" +"%2\n" +"Varmista, että Akonadi-palvelin on asennettu oikein." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi-hallintaprosessi rekisteröity D-Busiin." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-hallintaprosessi on rekisteröity D-Busiin, mikä yleensä tarkoittaa " +"sen olevan toiminnassa." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadin hallintaprosessia ei ole rekisteröity D-Busiin." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadin hallintaprosessia ei ole rekisteröity D-Busiin, mikä tavallisesti " +"tarkoittaa, ettei sitä ole käynnistetty tai että se on käynnistettäessä " +"kohdannut vakavan virheen." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadin palvelinprosessi on rekisteröity D-Busiin." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadin palvelinprosessi on rekisteröity D-Busiin, mikä tavallisesti " +"osoittaa sen olevan toiminnassa." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadin palvelinprosessia ei ole rekisteröity D-Busiin." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadin palvelinprosessia ei ole rekisteröity D-Busiin, mikä tavallisesti " +"tarkoittaa, ettei sitä ole käynnistetty tai että se on käynnistettäessä " +"kohdannut vakavan virheen." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Yhteyskäytäntöversion tarkistus ei mahdollista." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Yhteydettä palvelimeen ei ole mahdollista tarkistaa, vastaako " +"yhteyskäytäntöversio vaatimuksia." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Palvelimen yhteyskäytäntöversio on liian vanha." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Palvelimen yhteyskäytäntöversio on %1, mutta asiakasohjelma vaatii vähintään " +"version %2. Jos olet äskettäin päivittänyt KDE PIMin, käynnistä uudelleen " +"sekä Akonadi että KDE:n PIM-sovellukset." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Palvelimen yhteyskäytäntöversio on liian uusi." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Palvelimen yhteyskäytäntöversio täsmää." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Palvelimen yhteyskäytäntöversio on %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Resurssiagentteja löytynyt." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Ainakin yksi resurssiagentti on löytynyt." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Resurssiagentteja ei löytynyt." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Resurssiagentteja ei löytynyt eikä Akonadi ole käytettävissä ilman vähintään " +"yhtä. Yleensä tämä tarkoittaa, ettei resurssiagentteja ole asennettu tai " +"asennuksessa on ollut ongelmia. Etsittiin seuraavista sijainneista: ”%1”. " +"XDG_DATA_DIRS-ympäristömuuttujan arvo on ”%2”: varmista, että tämä sisältää " +"kaikki kansiot, joihin Akonadi-agentteja on asennettu." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Nykyistä Akonadi-palvelimen virhelokia ei löytynyt." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi-palvelin ei ilmoittanut virheistä viimeksi käynnistettäessä." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Nykyinen Akonadi-palvelimen virheloki löytyi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-palvelin ilmoitti virheistä tässä käynnistyksessä. Loki löytyy " +"sijainnista %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Edellistä Akonadi-palvelimen virhelokia ei löytynyt." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi-palvelin ei ilmoittanut virheistä edellisessä käynnistyksessä." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Edellinen Akonadi-palvelimen virheloki löytyi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-palvelin raportoi virheistä edellisessä käynnistyksessään. Loki " +"löytyy sijainnista %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Nykyistä Akonadin hallintavirhelokia ei löytynyt." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadin hallintaprosessi ei raportoinut virheistä viimeksi käynnistettäessä." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Nykyinen Akonadin hallintavirheloki löytyi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadin hallintaprosessi ei raportoinut virheistä viimeksi " +"käynnistettäessä. Loki löytyy sijainnista %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Edellistä Akonadin hallintavirhelokia ei löytynyt." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadin hallintaprosessi ei raportoinut virheistä edellisen kerran " +"käynnistettäessä." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Edellinen Akonadi-hallintavirheloki löytyi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi-hallintaprosessi raportoi virheistä edellisessä käynnistyksessään. " +"Loki löytyy sijainnista %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi käynnistettiin ylläpitäjänä" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Internetiin yhteydessä olevien ohjelmien ajaminen ylläpitäjäoikeuksin " +"altistaa sinut turvariskeille. Akonadi-asennuksen käyttämä MySQL ei salli " +"itseään käynnistettävän ylläpitäjänä, jotta olisit suojassa niiltä." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadin omistaja ei ole root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadin omistaja ei ole root (ylläpitäjätunnus), mikä on turvallisen " +"järjestelmän suositusasetus." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Tallenna testiraportti" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Virhe" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Tiedostoa ”%1” ei voitu avata" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi-palvelinta käynnistettäessä sattui virhe. Seuraavien itsetestausten " +"on tarkoitus helpottaa ongelman jäljittämisessä ja ratkaisemisessa. " +"Sisällytä tämä raportti pyytäessäsi tukea tai ilmoittaessasi " +"ohjelmavirheistä." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Yksityiskohdat" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Lisää vianmääritysvihjeitä löytää osoitteesta userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Uusi kansio…" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Uusi" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Poista kansio" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Poista" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "S&ynkronoi kansio" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synkronoi" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Kansion &ominaisuudet" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Ominaisuudet" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Liitä" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Liitä" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Hallitse paikallisia &tilauksia…" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Hallitse paikallisia tilauksia" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Lisää suosikkikansioihin" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Lisää suosikkeihin" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Poista suosikkikansioista" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Poista suosikeista" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Muuta suosikin nimeä…" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Muuta nimeä" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopioi kansio kohteeseen…" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopioi kohteeseen" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopioi tietue kohteeseen…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Siirrä tietue kohteeseen…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Siirrä kohteeseen" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Siirrä kansio kohteeseen…" + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "L&eikkaa tietue" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Leikkaa" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "L&eikkaa kansio" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Luo resurssi" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Poista resurssi" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Resurssin ominaisuudet" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synkronoi resurssi" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Työskentele verkotta" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synkronoi kansio alikansioineen" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synkronoi myös alikansiot" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Siirrä kansio roskakoriin" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Siirrä kansio roskakoriin" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Siirrä tietue roskakoriin" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Siirrä tietue roskakoriin" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Palauta kansio roskakorista" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Palauta kansio roskakorista" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Palauta tietue roskakorista" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Palauta tietue roskakorista" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Palauta kokoelma roskakorista" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Palauta kokoelma roskakorista" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Synkronoi suosikkikansio&t" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synkronoi suosikkikansiot" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synkronoi kansiopuu" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopioi kansio" +msgstr[1] "&Kopioi %1 kansiota" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopioi tietue" +msgstr[1] "Kopioi %1 tietuetta" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "L&eikkaa tietue" +msgstr[1] "L&eikkaa %1 tietuetta" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "L&eikkaa kansio" +msgstr[1] "L&eikkaa %1 kansiota" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Poista tietue" +msgstr[1] "&Poista %1 tietuetta" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Poista kansio" +msgstr[1] "&Poista %1 kansiota" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synkronoi kansio" +msgstr[1] "&Synkronoi %1 kansiota" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Poista resurssi" +msgstr[1] "&Poista %1 resurssia" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synkronoi resurssi" +msgstr[1] "&Synkronoi %1 resurssia" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopioi kansio" +msgstr[1] "&Kopioi %1 kansiota" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopioi tietue" +msgstr[1] "Kopioi %1 tietuetta" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Leikkaa tietue" +msgstr[1] "Leikkaa %1 tietuetta" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Leikkaa kansio" +msgstr[1] "Leikkaa %1 kansiota" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Poista tietue" +msgstr[1] "Poista %1 tietuetta" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Poista kansio" +msgstr[1] "Poista %1 kansiota" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synkronoi kansio" +msgstr[1] "Synkronoi %1 kansiota" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Poista resurssi" +msgstr[1] "Poista %1 resurssia" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synkronoi resurssi" +msgstr[1] "Synkronoi %1 resurssia" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nimi" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Haluatko varmasti poistaa tämän kansion alikansioineen?" +msgstr[1] "Haluatko varmasti poistaa %1 kansiota kaikkine alikansioineen?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Poistetaanko kansio?" +msgstr[1] "Poistetaanko kansiot?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Kansiota ei voitu poistaa: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Kansion poistaminen epäonnistui" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Kansion %1 ominaisuudet" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Haluatko varmasti poistaa valitun tietueen?" +msgstr[1] "Haluatko varmasti poistaa %1 tietuetta?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Poista tietue?" +msgstr[1] "Poista tietueet?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Tietuetta ei voitu poistaa: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Tietueen poistaminen epäonnistui" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Muuta suosikin nimeä" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nimi:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Uusi resurssi" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Resurssia ei saatu luoduksi: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Resurssin luominen epäonnistui" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Haluatko varmasti poistaa tämän resurssin?" +msgstr[1] "Haluatko varmasti poistaa %1 resurssia?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Poistetaanko resurssi?" +msgstr[1] "Poistetaanko resurssit?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Tietoa ei voitu liittää: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Liittäminen epäonnistui" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Kansion nimessä ei voi olla ”/”." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Uuden kansion luontivirhe" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Kansion nimen alussa tai lopussa ei voi olla pistettä (”.”)." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Ennen kansion ”%1” synkronointia resurssiin täytyy olla yhteys. Haluatko " +"ottaa siihen yhteyden?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Tiliin ”%1” ei ole yhteyttä" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Yhdistä verkkoon" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Siirrä tähän kansioon" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopioi tähän kansioon" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Tilauksen päivitys epäonnistui: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Tilausvirhe" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Paikalliset tilaukset" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Etsi:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Vain tilatut" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Tilaa" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Peru tilaus" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Uuden luokituksen luonti epäonnistui" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Uutta luokitusta luotaessa tapahtui virhe" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Haluatko varmasti poistaa luokituksen %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Poista luokitus" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Poista luokitus" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Valitse käytettävät luokitukset." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Luo uusi luokitus" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Hallitse luokituksia" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Valitse luokitukset…" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Valitse luokitukset" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Tyhjennä" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Lisää luokituksia napsauttamalla" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "…" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi->XML-muunnin" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Muuntaa Akonadi-kokoelma-alipuun XML-tiedostoksi." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Ei ladattua tietoa." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Tiedostonimeä ei ole annettu" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Datatiedostoa ”%1” ei saada avatuksi." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Tiedostoa %1 ei ole olemassa." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Datatiedostoa ”%1” ei saada jäsennetyksi." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Ei saatu ladattua ja jäsennettyä skeeman määritelmää." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Ei saatu luotua skeeman jäsennyskontekstia." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Ei voitu luoda skeemaa." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Ei saatu luotua skeeman validointikontekstia." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Virheellinen tiedostomuoto." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Datatiedostoa ei saada jäsennetyksi: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Kokoelmaa %1 ei löydy" + +#~ msgid "Id" +#~ msgstr "Tunniste" + +#~ msgid "Remote Id" +#~ msgstr "Etätunniste" + +#~ msgid "MimeType" +#~ msgstr "MIME-tyyppi" + +#~ msgid "Default Name" +#~ msgstr "Oletusnimi" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Poista" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Peru" + +#~ msgid "Take left one" +#~ msgstr "Säilytä vasemmainen" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Kaksi päivitystä on ristiriidassa keskenään.Valitse, mitkä " +#~ "päivitykset otetaan käyttöön." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Lukemattomia viestejä" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Kaikkiaan" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Koko" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-resurssi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nimi" + +#~ msgid "Invalid collection specified" +#~ msgstr "Määritetty virheellinen kokoelma" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Protokollan versio %1 löytyi, odotettiin vähintään %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Palvelimen protokollaversio on tarpeeksi uusi." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Palvelimen protokollaversio on %1, mikä on sama tai uudempi kuin vaadittu " +#~ "versio %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Havaittu ristiriitainen paikallinen kokoelmapuu." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Etäkokoelmalta puuttuu juureen päättyvä periytymisketju: resurssi on " +#~ "rikki." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE-testiohjelma" diff --git a/po/fr/akonadi_knut_resource.po b/po/fr/akonadi_knut_resource.po new file mode 100644 index 0000000..e9529af --- /dev/null +++ b/po/fr/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# translation of akonadi_knut_resource.po to Français +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Joëlle Cornavin , 2009. +# Joëlle Cornavin , 2010. +# xavier , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2013-05-19 14:02+0200\n" +"Last-Translator: xavier \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 1.5\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Aucun fichier de données n'a été sélectionné." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Le fichier « %1 » a été chargé avec succès." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Sélectionner un fichier de données" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Fichier de données « Knut » pour Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Aucun élément n'a été trouvé pour l'ID distant %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Collection parente introuvable dans l'arborescence DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Impossible d'écrire la collection." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Collection modifiée introuvable dans l'arborescence DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Collection supprimée introuvable dans l'arborescence DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Collection parente « %1 » introuvable dans l'arborescence DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Impossible d'écrire l'élément." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Élément modifié introuvable dans l'arborescence DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Élément supprimé introuvable dans l'arborescence DOM." diff --git a/po/fr/libakonadi5.po b/po/fr/libakonadi5.po new file mode 100644 index 0000000..23a4333 --- /dev/null +++ b/po/fr/libakonadi5.po @@ -0,0 +1,2855 @@ +# translation of libakonadi.po to Francais +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Mickael Sibelle , 2008, 2009, 2010, 2011. +# Mickaël Sibelle , 2010, 2012. +# Sébastien Renard , 2012, 2013, 2014. +# xavier , 2012, 2013, 2020, 2021. +# Vincent Pinon , 2017, 2018. +# Simon Depiets , 2018, 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-06 10:43+0100\n" +"Last-Translator: Xavier Besnard \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 21.07.80\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Vincent Pinon, Mickaël Sibelle, Sébastien Renard, Simon Depiets" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "vpinon@kde.org, kimael@gmail.com, renard@kde.org, sdepiets@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Aucun compte n'est configuré actuellement." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "L'intégration des comptes n'est pas prise en charge." + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Impossible d'enregistrer l'objet sur D-Bus : %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "la ressource « %1 » de type %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identifiant de l'agent" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agent Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Prêt" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Déconnectée" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synchronisation..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Erreur." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Non configuré" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identifiant de ressource" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Ressource Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Élément reçu non valable" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Erreur lors de la création de l'élément : %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Erreur lors de la mise à jour de la collection : %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "La mise à jour de la collection locale a échoué : %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "La mise à jour des éléments locaux a échoué : %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Impossible de recevoir les éléments en mode déconnecté" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synchronisation du dossier « %1 »" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Échec de la réception de la collection pour synchronisation." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Échec de la réception de la collection pour synchronisation des attributs." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "L'élément demandé n'existe plus" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Tâche annulée." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Aucune collection de ce genre." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Collections d'orphelins non-résolus trouvées" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" +"Impossible de trouver l'autre élément pour la prise en charge du conflit" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Impossible d'accéder à l'interface D-Bus de l'agent créé." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Le temps maximum de création de l'instance de l'agent est dépassé." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Impossible d'obtenir le type d'agent « %1 »." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Impossible de créer l'instance de l'agent." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Instance de collection non valable." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Instance de ressource non valable." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Impossible d'obtenir une interface D-Bus pour la ressource « %1 »" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" +"Le temps maximum de synchronisation des attributs de la collection est " +"dépassé." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Collection à copier non valable" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Collection de destination non valable" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Parent non valable" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Échec de l'analyse de la collection à partir de la réponse." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Collection non valable" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Collection fournie non valable." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Aucun objet à déplacer spécifié" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Aucune destination valable spécifiée" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Collection non valable." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Collection parente non valable" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Impossible de se connecter au service Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"La version du protocole du serveur Akonadi est incompatible. Veuillez vous " +"assurer que la version dont vous disposez est compatible." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "L'utilisateur a annulé l'opération." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Erreur inconnue." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Réponse inattendue" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Impossible de créer la relation." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Le temps maximum de synchronisation avec la ressource est dépassé." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Impossible de recevoir la collection racine de la ressource %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Aucune identifiant de ressource fourni." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identifiant de ressource « %1 » non valable" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Échec de la configuration de la ressource par défaut via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Échec de la réception pour la collection de la ressource." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Le temps maximum d'attente d'obtention d'un verrou est dépassé." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Impossible de créer l'étiquette" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Le déplacement de la collection vers la corbeille a échoué, annulation de " +"l'opération" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Éléments fournis non valables" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Collection fournie non valable" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Aucune collection valable ou liste d'éléments vide" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Impossible de trouver la collection de restauration et la ressource de " +"restauration n'est pas disponible" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nom" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Chargement..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Erreur" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"La collection cible « %1 » contient déjà\n" +"une collection portant le nom « %2 »." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nom" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Impossible de copier un élément : %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Impossible de copier une collection : %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Impossible déplacer un élément : %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Impossible de déplacer une collection : %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Impossible de lier une entité : %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Erreur" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Dossiers favoris" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Nombre total de messages" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Messages non lus" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Taille de stockage" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Taille du sous-dossier" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Non lus" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Taille" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Étiquette" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Impossible de recevoir un élément pour l'index" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "L'index n'est plus disponible" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" +"La partie « %1 » de la charge utile n'est pas disponible pour cet index" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Aucune session disponible pour cet index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Aucun élément disponible pour cet index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Module externe sans nom" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Aucune description disponible" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"La version de protocole du serveur Akonadi est différente de la version du " +"protocole utilisé par cette application.\n" +"Si vous avez récemment mis à jour votre système, veuillez fermer votre " +"session et vous reconnecter pour vous assurer que toutes les applications " +"utilisent la version correcte du protocole." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Aucun Agent Akonadi disponible. Veuillez vérifier votre installation de KDE " +"PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Versions de protocole non cohérentes. La version du protocole du serveur est " +"plus ancienne (%1) que la nôtre (%2). Si vous avez mis à jour récemment " +"votre système, veuillez redémarrer le serveur Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Versions de protocole non cohérentes. La version du protocole du serveur est " +"plus récente (%1) que la nôtre (%2). Si vous avez mis à jour récemment votre " +"système, veuillez redémarrer toutes les applications KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Auto-test Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Vérifie et rapporte l'état du serveur Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nouvelle instance d'agent..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Supprimer l'instance &d'agent" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configurer l'instance d'agent" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nouvelle instance d'agent" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Impossible de créer l'instance d'agent : %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "La création de l'instance d'agent a échoué" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Supprimer l'instance d'agent ?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Voulez-vous vraiment supprimer l'instance d'agent sélectionnée ?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuration de %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manuel de %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "A propos de %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" +"La boîte de dialogue de configuration est ouverte dans une autre fenêtre" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "La configuration de %1 est déjà ouverte ailleurs." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Impossible de déclarer la boîte de dialogue de configuration de %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minute" +msgstr[1] "minutes" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Réception" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Utiliser les options du dossier ou du compte parent" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchroniser lors de sélection de ce dossier" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synchroniser automatiquement après :" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Jamais" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutes" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Parties en cache local" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Options de réception" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Toujours recevoir les &messages complets" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Recevoir les corps des messages à la demande" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Conserver les corps des messages localement pour :" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Toujours" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Rechercher" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Utiliser le dossier par défaut" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nouveau sous-dossier..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Créer un nouveau sous-dossier sous le dossier actuellement sélectionné" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nouveau dossier" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nom" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Impossible de créer le dossier : %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "La création du dossier a échoué" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Général" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un objet" +msgstr[1] "%1 objets" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nom :" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Utiliser une icône personnalisée :" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "dossier" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistiques" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contenu :" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objet" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Taille :" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 octet" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Rappelez vous que l'indexation peut prendre plusieurs minutes." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Maintenance" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Erreur lors de la réception du nombre d'éléments indexés" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 élément indexé dans ce dossier" +msgstr[1] "%1 éléments indexés dans ce dossier" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calcul des éléments indexés..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fichiers" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Type de dossier :" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "inconnu" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Éléments" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Nombre total d'éléments" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Éléments non lus :" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexation" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Activer l'indexation du texte complet" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Réception du nombre d'éléments indexés..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Ré-indexer le dossier" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Aucun dossier" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Ouvrir la boîte de dialogue de collection" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Sélectionner une collection" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Déplacer ici" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copier ici" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Annuler" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Heure de modification" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Drapeaux" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribut : %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Résolution de conflit" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Prendre ma version" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Prendre leur version" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Conserver les deux versions" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Vos changements entrent en conflit avec ceux effectués par quelqu'un " +"d'autre entre temps.
À moins qu'une des version puisse être simplement " +"abandonnée, vous devez intégrer ces changements manuellement.
Veuillez " +"cliquer sur « Ouvrir l'éditeur de texte » " +"pour conserver une copie des textes, et alors sélectionner quelle version " +"est la plus pertinente, puis la ré-ouvrir et la modifier à nouveau pour " +"ajouter ce qu'il manque." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Données" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Démarrage du serveur Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Arrêt du serveur Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Déplacer ici" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copier ici" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Lier ici" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Annuler" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Impossible de se connecter au service de gestion des informations " +"personnelles.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" +"Le service de gestion des informations personnelles est en cours de " +"démarrage..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" +"Le service de gestion des informations personnelles est en cours de " +"fermeture..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Le service de gestion des informations personnelles réalise actuellement une " +"mise à jour de la base de données." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Le service de gestion des informations personnelles réalise actuellement une " +"mise à jour de la base de données.\n" +"Ceci fait suite à une mise à jour du logiciel et est nécessaire pour " +"optimiser les performances.\n" +"Selon le volume de données personnelles, ceci peut prendre plusieurs minutes." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Le service de gestion des informations personnelles n'est pas opérationnel. " +"Il est impossible d'utiliser cette application sans ce service." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Démarrer" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"L'environnement Akonadi de gestion des informations personnelles n'est pas " +"opérationnel.\n" +"Veuillez cliquer sur « Détails... » pour obtenir des informations détaillées " +"sur ce problème." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Le service de gestion des informations personnelles n'est pas opérationnel." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Détails..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Voulez-vous supprimer le compte « %1 » ?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Supprimer le compte ?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Comptes de réception (ajoutez au moins un compte) :" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Ajouter…" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modifier..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "Supprim&er" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Redémarrer" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Dossier récent" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Renommer un favori" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nom :" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Auto-test du serveur Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Enregistrer le rapport..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copier le rapport dans le presse-papier" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Le pilote « QtSQL » « %1 » est nécessaire dans votre configuration actuelle " +"du serveur Akonadi et a bien été trouvé sur votre système." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Le pilote SQL « %1 » est nécessaire dans votre configuration actuelle du " +"serveur Akonadi.\n" +"Les pilotes suivants sont installés : %2.\n" +"Veuillez vous assurer de la bonne installation du pilote requis." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Pilote de base de donnée trouvé." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Impossible de trouver un pilote de base de donnée." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Exécutable du serveur MySQL non testé." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "La configuration actuelle ne nécessite aucun serveur MySQL interne." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Vous avez configuré Akonadi pour qu'il utilise le serveur MySQL « %1 ».\n" +"Veuillez vous assurer d'avoir installé un serveur MySQL, indiquer son " +"emplacement correct et vous assurer que vous disposez des droits nécessaires " +"en lecture et exécution sur l'exécutable du serveur. L'exécutable du serveur " +"est le plus souvent nommé « mysqld » ; son emplacement varie selon les " +"distributions." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Serveur MySQL introuvable." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Serveur MySQL non accessible en lecture." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Serveur MySQL non exécutable." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL trouvé avec un nom inattendu." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Serveur MySQL trouvé." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Serveur MySQL trouvé : %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Le serveur MySQL est exécutable." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"L'exécution du serveur MySQL « %1 » a échoué avec le message d'erreur " +"suivant : « %2 »" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "L'exécution du serveur MySQL a échoué." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Journal d'erreur du serveur MySQL non testé." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Journal d'erreur MySQL introuvable." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Le serveur MySQL n'a rapporté aucune erreur au cours de ce démarrage. Le " +"journal se trouve dans « %1 »." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Journal des erreurs MySQL inaccessible en lecture." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Un fichier journal du serveur MySQL a bien été trouvé mais n'est pas " +"accessible en lecture : %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Le journal d'erreurs du serveur MySQL contient des erreurs." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" +"Le fichier journal d'erreurs « %1 » du serveur MySQL contient des erreurs." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Le journal du serveur MySQL contient des alertes." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Le fichier journal « %1 » du serveur MySQL contient des alertes." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Le journal du serveur MySQL ne contient aucune erreur." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Le fichier journal « %1 » du serveur MySQL ne contient aucune erreur ou " +"alerte." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "La configuration du serveur MySQL n'est pas testée." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "La configuration par défaut du serveur MySQL a bien été trouvée." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"La configuration par défaut du serveur MySQL a bien été trouvée et est " +"lisible à l'emplacement %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Configuration par défaut du serveur MySQL introuvable." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"La configuration par défaut pour le serveur MySQL est introuvable ou n'est " +"pas lisible. Veuillez vérifier que votre installation d'Akonadi est complète " +"et que vous disposez des droits d'accès nécessaires." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Configuration personnalisée du serveur MySQL non disponible." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"La configuration personnalisée du serveur MySQL est introuvable mais " +"optionnelle." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Configuration personnalisée du serveur MySQL trouvée." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"La configuration personnalisée du serveur MySQL a bien été trouvée et est " +"lisible à l'emplacement %1." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Configuration personnalisée du serveur MySQL non lisible." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"La configuration personnalisée du serveur MySQL a bien été trouvée à " +"l'emplacement %1 mais n'est pas lisible. Veuillez vérifier vos droits " +"d'accès." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Configuration du serveur MySQL introuvable ou non lisible." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"La configuration du serveur MySQL n'a pu être trouvée ou n'est pas lisible." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "La configuration du serveur MySQL est utilisable." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"La configuration du serveur MySQL a bien été trouvée à l'emplacement %1 et " +"est lisible." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Impossible de se connecter au serveur PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Serveur PostgreSQL trouvé." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Le serveur PostgreSQL a été trouvé et la connexion fonctionne." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "« akonadictl » introuvable" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Le programme « akonadictl » doit être accessible via la variable " +"d'environnement « $PATH ». Veuillez vous assurer que vous disposez bien d'un " +"serveur Akonadi installé." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "« akonadictl » trouvé et utilisable" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Le programme « %1 » permettant de contrôler le serveur Akonadi a bien été " +"trouvé et a été exécuté sans problème.\n" +"Résultat :\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "« akonadictl » trouvé mais inutilisable" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Le programme « %1 » permettant de contrôler le serveur Akonadi a bien été " +"trouvé mais n'a pu être exécuté.\n" +"Résultat :\n" +"%2\n" +"Veuillez vous assurer que le serveur Akonadi est installé correctement." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Processus de contrôle d'Akonadi enregistré dans D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Le processus de contrôle d'Akonadi est bien enregistré dans D-Bus ce qui " +"indique qu'il est bien opérationnel." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Processus de contrôle d'Akonadi non enregistré dans D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Le processus de contrôle d'Akonadi n'a pas pu être enregistré dans D-Bus ce " +"qui indique qu'il n'a pas été démarré ou a rencontré une erreur fatale au " +"démarrage." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Processus du serveur Akonadi enregistré dans D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Le processus serveur Akonadi est bien enregistré dans D-Bus ce qui indique " +"qu'il est bien opérationnel." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Processus du serveur Akonadi non enregistré dans D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Le processus du serveur Akonadi n'a pas pu être enregistré dans D-Bus ce qui " +"indique qu'il n'a pas été démarré ou a rencontré une erreur fatale au " +"démarrage." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Vérification de la version du protocole impossible." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Sans une connexion au serveur, il est impossible de vérifier si la version " +"du protocole correspond aux exigences." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Version du protocole du serveur trop ancienne." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"La version du protocole du serveur est %1, mais la version %2 est requise " +"par le client. Si vous avez récemment mis à jour KDE PIM, veuillez vous " +"assurer de redémarrer à la fois Akonadi et les applications KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Version du protocole du serveur trop récente." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Version du protocole du serveur correspond." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "La version du protocole courant est %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Agents de ressource trouvés." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Au moins un agent de ressource a été trouvé." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Aucun agent de ressource n'a été trouvé." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Aucun agent de ressource n'a été trouvé, Akonadi n'est pas utilisable sans " +"au moins un tel agent. Cela signifie d'habitude qu'aucun agent de ressource " +"n'est installé ou qu'il y a un problème de configuration. Les emplacements " +"suivants ont été parcourus : « %1 ». La variable d'environnement " +"« XDG_DATA_DIRS » est fixée à « %2 » ; veuillez vous assurer qu'elle " +"contient bien tous les emplacements où sont installés des agents Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Aucun journal d'erreurs de serveur Akonadi n'a été trouvé." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Le serveur Akonadi n'a rapporté aucune erreur au court de ce démarrage." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Un journal d'erreurs de serveur Akonadi a été trouvé." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Le serveur Akonadi a rapporté des erreurs au cours du démarrage actuel. Le " +"journal se trouve dans « %1 »." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Aucun journal d'erreurs du précédent serveur Akonadi n'a été trouvé." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Le serveur Akonadi n'a rapporté aucune erreur au cours de son démarrage " +"précédent." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Un journal d'erreurs du précédent serveur Akonadi a été trouvé." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Le serveur Akonadi a rapporté des erreurs au cours de son précédent " +"démarrage. Le journal se trouve dans « %1 »." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Aucun journal d'erreurs du contrôleur Akonadi n'a été trouvé." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Le processus du contrôleur Akonadi n'a renvoyé aucune erreur au cours de ce " +"démarrage." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Un journal d'erreurs du contrôleur Akonadi a été trouvé." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Le processus du contrôleur Akonadi a rapporté des erreurs au cours de ce " +"démarrage. Le journal se trouve dans « %1 »." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" +"Aucun journal d'erreurs du contrôleur Akonadi précédent n'a été trouvé." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Le processus du contrôleur Akonadi n'a rapporté aucune erreur au cours de " +"son démarrage précédent." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Un journal d'erreurs du contrôleur Akonadi précédent a été trouvé." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Le processus du contrôleur Akonadi a rapporté des erreurs au cours du " +"précédent démarrage. Le journal se trouve dans « %1 »." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi a été lancé en tant que superutilisateur" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"L'exécution en tant qu'administrateur (root) des applications s'appuyant sur " +"Internet vous expose à de nombreux risques de sécurité. MySQL, utilisé par " +"cette installation d'Akonadi, s'interdira toute exécution en tant " +"qu'administrateur (root), ceci pour vous protéger de ces risques." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi ne s'exécute pas en tant que superutilisateur" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi ne s'exécute pas en tant que superutilisateur (ou utilisateur " +"administrateur), ce qui est la configuration recommandée pour un système " +"sécurisé." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Enregistrer le rapport de test." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Erreur" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Impossible d'ouvrir le fichier « %1 » " + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Une erreur s'est produite lors du démarrage du serveur Akonadi. Les auto-" +"tests suivants sont là pour vous aider à trouver la cause du problème et le " +"résoudre. Si vous demandez de l'aide ou envoyez des rapports de bogue, " +"veuillez toujours joindre ce rapport." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Détails" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Pour plus d'astuces sur la résolution des problèmes, veuillez vous " +"référer à userbase.kde.org/" +"Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nouveau dossier..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nouveau" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Supprimer un &dossier" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Supprimer" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Synchroniser un dossier" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchroniser" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propriétés d'un dossier" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propriétés" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Coller" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Coller" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gérer les abonnement&s locaux" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gérer les abonnements locaux" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Ajouter aux dossiers favoris" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Ajouter aux favoris" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Supprimer des dossiers favoris" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Supprimer des favoris" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Renommer un favori..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Renommer" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copier un dossier vers..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copier vers" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copier un élément vers..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Déplace un élément vers..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Déplacer vers" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Déplacer un dossier vers..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Couper un élément" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Couper" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Couper un dossier" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Créer une ressource" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Supprimer une ressource" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Propriétés d'une ressource" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synchroniser une ressource" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Travailler hors connexion" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synchroniser récursivement un dossier" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synchroniser récursivement" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Déplacer un dossier dans la corbeille" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Déplacer un dossier dans la corbeille" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Déplacer un élément dans la corbeille" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Déplacer un élément dans la corbeille" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restaurer un dossier depuis la corbeille" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restaurer un dossier depuis la corbeille" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restaurer un dossier depuis la corbeille" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restaurer un dossier depuis la corbeille" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restaurer un dossier depuis la corbeille" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restaurer une collection depuis la corbeille" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synchroniser les dossiers préférés" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synchroniser les dossiers préférés" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synchroniser l'arborescence des dossiers" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copier un dossier" +msgstr[1] "&Copier %1 dossiers" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copier un élément" +msgstr[1] "&Copier %1 éléments" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Couper un élément" +msgstr[1] "&Couper %1 éléments" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Couper un dossier" +msgstr[1] "Couper %1 dossiers" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Supprimer un élément" +msgstr[1] "&Supprimer %1 éléments" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Supprimer un &dossier" +msgstr[1] "Supprimer %1 &dossiers" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synchroniser un dossier" +msgstr[1] "&Synchroniser %1 dossiers" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Supprimer une ressource" +msgstr[1] "&Supprimer %1 ressources" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synchroniser une ressource" +msgstr[1] "&Synchroniser %1 ressources" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copier un dossier" +msgstr[1] "Copier %1 dossiers" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copier un élément" +msgstr[1] "Copier %1 éléments" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Couper un élément" +msgstr[1] "Couper %1 éléments" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Couper un dossier" +msgstr[1] "Couper %1 dossiers" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Supprimer un élément" +msgstr[1] "Supprimer %1 éléments" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Supprimer un dossier" +msgstr[1] "Supprimer %1 dossiers" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synchroniser un dossier" +msgstr[1] "Synchroniser %1 dossiers" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Supprimer une ressource" +msgstr[1] "Supprimer %1 ressources" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synchroniser une ressource" +msgstr[1] "Synchroniser %1 ressources" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nom" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Voulez-vous vraiment supprimer ce dossier et tous ses sous-dossiers ?" +msgstr[1] "" +"Voulez-vous vraiment supprimer %1 dossiers et tous leurs sous-dossiers ?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Supprimer le dossier ?" +msgstr[1] "Supprimer les dossiers ?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Impossible de supprimer un dossier : %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "La suppression du dossier a échoué" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propriétés du dossier %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Voulez-vous vraiment supprimer l'élément sélectionné ?" +msgstr[1] "Voulez-vous vraiment supprimer %1 éléments ?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Supprimer l'élément ?" +msgstr[1] "Supprimer les éléments ?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Impossible de supprimer l'élément : %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "La suppression de l'élément a échoué" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Renommer un favori" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nom :" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nouvelle ressource" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Impossible de créer une ressource : %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "La création de la ressource a échoué" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Voulez-vous vraiment supprimer cette ressource ?" +msgstr[1] "Voulez-vous vraiment supprimer %1 ressources ?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Supprimer la ressource ?" +msgstr[1] "Supprimer les ressources ?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Impossible de coller les données : %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Le collage a échoué" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Impossible d'ajouter « / » dans le nom de dossier." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Erreur à la création d'un nouveau dossier" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Impossible d'ajouter « . » au début ou à la fin du nom d'un dossier." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Avant de pouvoir synchroniser le dossier « %1 », il est nécessaire que la " +"ressource soit en ligne. Voulez-vous la mettre en ligne ?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Le compte « %1 » est hors ligne" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Se connecter" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Déplacer vers ce dossier" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copier vers ce dossier" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Impossible de mettre à jour l'abonnement : %1." + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Erreur d'abonnement" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Abonnements locaux" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Rechercher :" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Uniquement le&s abonnements" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "S'a&bonner" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Se &désabonner" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Impossible de créer une nouvelle étiquette" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Erreur lors de la création d'une nouvelle étiquette" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Voulez-vous vraiment supprimer l'étiquette %1 ?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Supprimer l'étiquette" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Supprimer l'étiquette" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Sélectionner les étiquettes qui devraient être appliquées." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Créez une nouvelle étiquette" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gérer les étiquettes" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Supprimer les étiquettes..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Sélectionner les étiquettes" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Effacer" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Cliquez pour ajouter des étiquettes" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Convertisseur Akonadi vers XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Convertit l'arborescence d'une collection Akonadi vers un fichier XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Aucune donnée chargée." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Aucun nom de fichier spécifié" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Impossible d'ouvrir le fichier de données « %1 »." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Le fichier « %1 » n'existe pas." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Impossible d'analyser le fichier de données « %1 »." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Impossible de charger et d'analyser la définition du schéma." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Impossible de créer le contexte d'analyse du schéma." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Impossible de créer le schéma." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Impossible de créer le contexte de validation du schéma." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Format de fichier non valable." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Impossible d'analyser le fichier de données : %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Impossible de trouver la collection %1" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Id distant" + +#~ msgid "MimeType" +#~ msgstr "Type MIME" + +#~ msgid "Default Name" +#~ msgstr "Nom par défaut" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Supprimer" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Annuler" + +#~ msgid "Take left one" +#~ msgstr "Prendre celui de gauche" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Deux mises à jour sont en conflit.Veuillez choisir laquelle des " +#~ "deux doit être appliquée." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Non lus" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Total" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Taille" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Ressource Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nom" + +#~ msgid "Invalid collection specified" +#~ msgstr "Collection spécifiée non valable" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "" +#~ "Version du protocole %1 identifiée, la version %2 au moins est attendue" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Version du protocole du serveur suffisamment récente." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "La version du protocole du serveur est %1, ce qui est aussi récent ou " +#~ "plus récent que la version %2 requise." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Incohérence détectée dans l'arbre de la collection locale." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Collection distante sans fourniture d'une chaîne d'ancêtre se terminant " +#~ "par une racine fournie. La ressource est cassée." + +#~ msgid "KDE Test Program" +#~ msgstr "Programme de test pour KDE" + +#~ msgid "Cannot list root collection." +#~ msgstr "Impossible de lister la collection racine." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Service de recherche Nepomuk enregistré dans D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Le service de la recherche Nepomuk est enregistrée dans D-Bus ce qui " +#~ "indique qu'il est opérationnel." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Service de recherche Nepomuk non enregistré dans D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Le service de recherche Nepomuk n'a pas pu être enregistré dans D-Bus ce " +#~ "qui indique qu'il n'a pas été démarré ou a rencontré une erreur fatale au " +#~ "démarrage." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Le service de recherche Nepomuk utilise un moteur inapproprié." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Le service de recherche Nepomuk utilise le moteur « %1 », ce qui n'est " +#~ "pas recommandé pour utilisation avec Akonadi." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Le service de recherche Nepomuk utilise un moteur inapproprié." + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Le service de recherche Nepomuk utilise l'un des moteurs recommandés." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Le module externe « %1 » n'est pas compilé statiquement à l'intérieur de " +#~ "ce logiciel, veuillez insérer cette information dans le rapport de bogue." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Module externe non compilé statiquement" + +#~ msgid "Fetch Job Error" +#~ msgstr "Erreur de réception de la tâche" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Nouveau dossier..." + +#, fuzzy +#~| msgid "Folder &Properties" +#~ msgid "Resource Properties" +#~ msgstr "Propriétés du dossier" + +#~ msgid "Cache" +#~ msgstr "Cache" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Hériter de la politique de cache du parent" + +#~ msgid "Cache Policy" +#~ msgstr "Politique de cache" + +#~ msgid "Interval check time:" +#~ msgstr "Délai entre deux vérifications :" + +#~ msgid "Local cache timeout:" +#~ msgstr "Délai avant l'obsolescence du cache local :" + +#~ msgid "Synchronize on demand" +#~ msgstr "Synchroniser à la demande" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "" +#~ "Indiquer quels dossiers vous voulez voir apparaître dans l'arborescence " +#~ "de dossiers" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Rechercher" + +#~ msgid "Available Folders" +#~ msgstr "Dossiers disponibles" + +#~ msgid "Current Changes" +#~ msgstr "Modifications actuelles" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Se désinscrire des dossiers sélectionnés" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "" +#~ "Le serveur Akonadi a rapporté des erreurs au court de ce démarrage dans " +#~ "%1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Le processus du contrôleur Akonadi a rapporté des erreurs au court de ce " +#~ "démarrage dans %1." + +#~ msgid "TODO" +#~ msgstr "TODO" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi n'est pas opérationel.
Détails...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Ressource Akonadi" + +#, fuzzy +#~| msgid "No such collection." +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "Aucune collection de ce genre." +#~ msgstr[1] "Aucune collection de ce genre." + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "&Copier le dossier" + +#~ msgid "TextLabel" +#~ msgstr "TextLabel" diff --git a/po/ga/akonadi_knut_resource.po b/po/ga/akonadi_knut_resource.po new file mode 100644 index 0000000..582db97 --- /dev/null +++ b/po/ga/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# Irish translation of akonadi_knut_resource +# Copyright (C) 2009 This_file_is_part_of_KDE +# This file is distributed under the same license as the akonadi_knut_resource package. +# Kevin Scannell , 2009. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2009-04-06 08:33-0500\n" +"Last-Translator: Kevin Scannell \n" +"Language-Team: Irish \n" +"Language: ga\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n < 11 ? " +"3 : 4\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Níl aon chomhad sonraí roghnaithe." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "D'éirigh le luchtú chomhad '%1'." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Roghnaigh Comhad Sonraí" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Comhad Sonraí Knut Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "" + +#~ msgid "Path to the Knut data file." +#~ msgstr "Conair go dtí an comhad sonraí Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Ná hathraigh na sonraí féin." diff --git a/po/ga/libakonadi5.po b/po/ga/libakonadi5.po new file mode 100644 index 0000000..b4a61d1 --- /dev/null +++ b/po/ga/libakonadi5.po @@ -0,0 +1,2785 @@ +# Irish translation of libakonadi +# Copyright (C) 2009 This_file_is_part_of_KDE +# This file is distributed under the same license as the libakonadi package. +# Kevin Scannell , 2009. +msgid "" +msgstr "" +"Project-Id-Version: kdepim/libakonadi.po\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2007-07-11 08:14-0500\n" +"Last-Translator: Kevin Scannell \n" +"Language-Team: Irish \n" +"Language: ga\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n < 11 ? " +"3 : 4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Kevin Scannell" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kscanne@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Aitheantóir gníomhaire" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Gníomhaire Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Réidh" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "As Líne" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Á Shioncrónú..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Earráid." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Aitheantóir acmhainne" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Acmhainn Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Fuarthas mír neamhbhailí" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Earráid agus mír á cruthú: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Earráid agus bailiúchán á nuashonrú: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Níorbh fhéidir an bailiúchán logánta a nuashonrú: %1." + +#: agentbase/resourcebase.cpp:718 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Níorbh fhéidir an bailiúchán logánta a nuashonrú: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Ní féidir mír a fháil sa mhód as líne." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Fillteán '%1' á shioncrónú" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Níl an mhír iarrtha ann a thuilleadh" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Níl a leithéid de bhailiúchán ann." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Aimsíodh bailiúcháin dílleachtaí gan réiteach" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Ní féidir comhéadan D-Bus an ghníomhaire cruthaithe a rochtain." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Cruthú an ghníomhaire thar am." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Níorbh fhéidir cineál gníomhaire '%1' a fháil." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Bailiúchán neamhbhailí." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Acmhainn neamhbhailí." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Ní féidir comhéadan D-Bus a fháil le haghaidh acmhainn '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Bailiúchán neamhbhailí" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Bailiúchán neamhbhailí" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Máthair neamhbhailí" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Unable to fetch collection in replay mode." +msgid "Failed to parse Collection from response" +msgstr "Ní féidir bailiúchán a fháil sa mhód athdhéanta." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Bailiúchán neamhbhailí" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Tugadh bailiúchán neamhbhailí" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Bailiúchán neamhbhailí." + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Bailiúchán neamhbhailí" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Ní féidir ceangal leis an tseirbhís Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Níl leagan phrótacal an fhreastalaí Akonadi comhoiriúnach. Bí cinnte go " +"bhfuil leagan comhoiriúnach suiteáilte agat." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Cealaithe ag an úsáideoir." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Earráid anaithnid." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Sioncrónú acmhainne thar am." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Aitheantóir neamhbhailí acmhainne '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Thar am ag iarraidh glas a fháil." + +#: core/jobs/tagcreatejob.cpp:49 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create tag." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Bailiúchán neamhbhailí seolta" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Ainm" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Á Luchtú..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Earráid." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Ainm" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Níorbh fhéidir an mhír a chóipeáil:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Níorbh fhéidir an bailiúchán a chóipeáil:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Níorbh fhéidir an mhír a bhogadh:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Níorbh fhéidir an bailiúchán a bhogadh:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Níorbh fhéidir aonán a nascadh:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Earráid." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Fillteáin Is Ansa Leat" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Teachtaireachtaí Iomlána" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Teachtaireachtaí Gan Léamh" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Cuóta" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Méid Stórála" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Méid Stórála san Fhofhillteán" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Gan léamh" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Iomlán" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Méid" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Ní féidir mír a fháil don innéacs" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Níl an t-innéacs ar fáil a thuilleadh" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Níl aon seisiún ar fáil don innéacs seo" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Níl aon mhír ar fáil don innéacs seo" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Breiseán gan ainm" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Níl cur síos ar fáil" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "Féintástáil Fhreastalaí Akonadi" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "Ní féidir ceangal leis an tseirbhís Akonadi." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Gníomhaire &Nua..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Scrios Gníomhaire" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Cumraigh Gníomhaire" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Gníomhaire Nua" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Níorbh fhéidir gníomhaire a chruthú: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "nóiméad" +msgstr[1] "nóiméad" +msgstr[2] "nóiméad" +msgstr[3] "nóiméad" +msgstr[4] "nóiméad" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize when selecting this folder" +msgstr "Sioncrónaigh Fillteán" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Ná seiceáil riamh" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "nóiméad" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Páirteanna i dTaisce Logánta" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Go deo" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "" +#| "@info/plain Displayed grayed-out inside the textbox, verb to search" +#| msgid "Search" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Cuardaigh" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Fofhillteán &Nua..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Fillteán Nua" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Ainm" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Níorbh fhéidir an fillteán a chruthú: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Theip ar chruthú an fhillteáin" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Ginearálta" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Réad amháin" +msgstr[1] "%1 réad" +msgstr[2] "%1 réad" +msgstr[3] "%1 réad" +msgstr[4] "%1 réad" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Ainm:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Ú&sáid deilbhín saincheaptha:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "fillteán" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Staitisticí" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Inneachar:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 réad" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Méid:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Beart" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "Earráid agus mír á cruthú: %1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" +msgstr[4] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "&Airíonna an Fhillteáin" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "Gearr Mír" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Teachtaireachtaí Iomlána" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Teachtaireachtaí Gan Léamh" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "Fillteán Le Déanaí" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Gan Fillteán" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Oscail dialóg bhailiúcháin" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Roghnaigh bailiúchán" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Bog anseo" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Cóipeáil anseo" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cealaigh" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Bratacha" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Sonraí" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Freastalaí Akonadi á thosú..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Freastalaí Akonadi á stopadh..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Bog Anseo" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Cóipeáil Anseo" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Nasc Anseo" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "Ce&alaigh" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Ní féidir ceangal leis an tseirbhís Akonadi." + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Bainisteoir faisnéise phearsanta á thosú..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Bainisteoir faisnéise phearsanta á mhúchadh..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Tosaigh" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, fuzzy, kde-format +#| msgid "Personal information management service is starting..." +msgid "The Akonadi personal information management service is not operational." +msgstr "Bainisteoir faisnéise phearsanta á thosú..." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Mionsonraí..." + +#: widgets/manageaccountwidget.cpp:199 +#, fuzzy, kde-format +#| msgid "Do you really want to delete the search view '%1'?" +msgid "Do you want to remove account '%1'?" +msgstr "An bhfuil tú cinnte gur mian leat amharc cuardaigh '%1' a scriosadh?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "Tosaigh" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Fillteán Le Déanaí" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Ainm:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Féintástáil Fhreastalaí Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Sábháil an Tuairisc..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Cóipeáil Tuairisc go dtí an Ghearrthaisce" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Aimsíodh tiománaí bunachair sonraí." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Tiománaí bunachair sonraí gan aimsiú." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Freastalaí inrite MySQL gan tástáil." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Freastalaí MySQL gan aimsiú." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Freastalaí MySQL neamh-inléite." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Freastalaí MySQL neamh-inrite." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Aimsíodh MySQL ach ainm aisteach air." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Aimsíodh freastalaí MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Aimsíodh freastalaí MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Tá an freastalaí MySQL inrite." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Níl an logchomhad earráide MySQL inléite." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Tá earráidí i logchomhad an fhreastalaí MySQL." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Tá rabhaidh i logchomhad an fhreastalaí MySQL." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Níl aon earráidí i logchomhad an fhreastalaí MySQL." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Aimsíodh cumraíocht réamhshocraithe an fhreastalaí MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Níor aimsíodh cumraíocht réamhshocraithe ar an bhfreastalaí MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Níl cumraíocht shaincheaptha ar an bhfreastalaí MySQL ar fáil." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Aimsíodh cumraíocht shaincheaptha ar an bhfreastalaí MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Níl cumraíocht shaincheaptha an fhreastalaí MySQL inléite." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Níor aimsíodh cumraíocht an fhreastalaí MySQL nó níl sí inléite." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Tá cumraíocht an fhreastalaí MySQL inúsáidte." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Ní féidir ceangal leis an bhfreastalaí PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Aimsíodh freastalaí PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl gan aimsiú" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "Aimsíodh akonadictl agus tá sé inúsáidte" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "Aimsíodh akonadictl ach níl sé inúsáidte" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Cláraíodh an próiseas rialaithe Akonadi le D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Cláraíodh an próiseas rialaithe Akonadi le D-Bus, rud a chiallaíonn go " +"bhfuil sé i bhfeidhm de ghnáth." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Níl an próiseas rialaithe Akonadi cláraithe le D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Cláraíodh próiseas freastalaí Akonadi le D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Cláraíodh próiseas freastalaí Akonadi le D-Bus, rud a chiallaíonn go bhfuil " +"sé i bhfeidhm de ghnáth." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Níl próiseas freastalaí Akonadi cláraithe le D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Aimsíodh gníomhairí acmhainne." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Aimsíodh gníomhaire acmhainne amháin ar a laghad." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Níor aimsíodh aon ghníomhairí acmhainne." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Earráid." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Níorbh fhéidir comhad '%1' a oscailt" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Mionsonraí" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Fillteán &Nua..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nua" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Scrios Fillteán" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Scrios" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Sioncrónaigh Fillteán" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sioncrónaigh" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Airíonna an Fhillteáin" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Airíonna" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Greamaigh" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Greamaigh" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Bainistigh &Síntiúis Logánta..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Bainistigh Síntiúis Logánta" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Athainmnigh" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Cóipeáil Fillteán Go..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Cóipeáil Go" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Cóipeáil Mír Go..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Bog Mír Go..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Bog Go" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Bog Fillteán Go..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Gearr Mír" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Gearr" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Gearr Fillteán" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Cruthaigh Acmhainn" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Scrios Acmhainn" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Airíonna na hAcmhainne" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Sioncrónaigh Acmhainn" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Oibrigh As Líne" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sioncrónaigh Fillteán go hAthchúrsach" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sioncrónaigh go hAthchúrsach" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize Folder Tree" +msgstr "Sioncrónaigh Fillteán" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Cóipeáil Fillteán" +msgstr[1] "&Cóipeáil %1 Fhillteán" +msgstr[2] "&Cóipeáil %1 Fhillteán" +msgstr[3] "&Cóipeáil %1 bhFillteán" +msgstr[4] "&Cóipeáil %1 Fillteán" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Cóipeáil Mír" +msgstr[1] "&Cóipeáil %1 Mhír" +msgstr[2] "&Cóipeáil %1 Mhír" +msgstr[3] "&Cóipeáil %1 Mír" +msgstr[4] "&Cóipeáil %1 Mír" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Gearr Mír" +msgstr[1] "&Gearr %1 Mhír" +msgstr[2] "&Gearr %1 Mhír" +msgstr[3] "&Gearr %1 Mír" +msgstr[4] "&Gearr %1 Mír" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Gearr Fillteán" +msgstr[1] "&Gearr %1 Fhillteán" +msgstr[2] "&Gearr %1 Fhillteán" +msgstr[3] "&Gearr %1 bhFillteán" +msgstr[4] "&Gearr %1 Fillteán" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Scrios Mír" +msgstr[1] "&Scrios %1 Mhír" +msgstr[2] "&Scrios %1 Mhír" +msgstr[3] "&Scrios %1 Mír" +msgstr[4] "&Scrios %1 Mír" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Scrios Fillteán" +msgstr[1] "&Scrios %1 Fhillteán" +msgstr[2] "&Scrios %1 Fhillteán" +msgstr[3] "&Scrios %1 bhFillteán" +msgstr[4] "&Scrios %1 Fillteán" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sioncrónaigh Fillteán" +msgstr[1] "&Sioncrónaigh %1 Fhillteán" +msgstr[2] "&Sioncrónaigh %1 Fhillteán" +msgstr[3] "&Sioncrónaigh %1 bhFillteán" +msgstr[4] "&Sioncrónaigh %1 Fillteán" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Scrios Acmhainn" +msgstr[1] "&Scrios %1 Acmhainn" +msgstr[2] "&Scrios %1 Acmhainn" +msgstr[3] "&Scrios %1 nAcmhainn" +msgstr[4] "&Scrios %1 Acmhainn" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sioncrónaigh Acmhainn" +msgstr[1] "&Sioncrónaigh %1 Acmhainn" +msgstr[2] "&Sioncrónaigh %1 Acmhainn" +msgstr[3] "&Sioncrónaigh %1 nAcmhainn" +msgstr[4] "&Sioncrónaigh %1 Acmhainn" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Cóipeáil Fillteán" +msgstr[1] "Cóipeáil %1 Fhillteán" +msgstr[2] "Cóipeáil %1 Fhillteán" +msgstr[3] "Cóipeáil %1 bhFillteán" +msgstr[4] "Cóipeáil %1 Fillteán" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Cóipeáil Mír" +msgstr[1] "Cóipeáil %1 Mhír" +msgstr[2] "Cóipeáil %1 Mhír" +msgstr[3] "Cóipeáil %1 Mír" +msgstr[4] "Cóipeáil %1 Mír" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Gearr Mír" +msgstr[1] "Gearr %1 Mhír" +msgstr[2] "Gearr %1 Mhír" +msgstr[3] "Gearr %1 Mír" +msgstr[4] "Gearr %1 Mír" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Gearr Fillteán" +msgstr[1] "Gearr %1 Fhillteán" +msgstr[2] "Gearr %1 Fhillteán" +msgstr[3] "Gearr %1 bhFillteán" +msgstr[4] "Gearr %1 Fillteán" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Scrios Mír" +msgstr[1] "Scrios %1 Mhír" +msgstr[2] "Scrios %1 Mhír" +msgstr[3] "Scrios %1 Mír" +msgstr[4] "Scrios %1 Mír" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Scrios Fillteán" +msgstr[1] "Scrios %1 Fhillteán" +msgstr[2] "Scrios %1 Fhillteán" +msgstr[3] "Scrios %1 bhFillteán" +msgstr[4] "Scrios %1 Fillteán" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sioncrónaigh Fillteán" +msgstr[1] "Sioncrónaigh %1 Fhillteán" +msgstr[2] "Sioncrónaigh %1 Fhillteán" +msgstr[3] "Sioncrónaigh %1 bhFillteán" +msgstr[4] "Sioncrónaigh %1 Fillteán" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Scrios Acmhainn" +msgstr[1] "Scrios %1 Acmhainn" +msgstr[2] "Scrios %1 Acmhainn" +msgstr[3] "Scrios %1 nAcmhainn" +msgstr[4] "Scrios %1 Acmhainn" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sioncrónaigh Acmhainn" +msgstr[1] "Sioncrónaigh %1 Acmhainn" +msgstr[2] "Sioncrónaigh %1 Acmhainn" +msgstr[3] "Sioncrónaigh %1 nAcmhainn" +msgstr[4] "Sioncrónaigh %1 Acmhainn" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Ainm" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"An bhfuil tú cinnte gur mian leat an fillteán seo a scriosadh in éineacht le " +"gach fofhillteán atá ann?" +msgstr[1] "" +"An bhfuil tú cinnte gur mian leat an %1 fhillteán a scriosadh in éineacht le " +"gach fofhillteán atá iontu?" +msgstr[2] "" +"An bhfuil tú cinnte gur mian leat na %1 fhillteán a scriosadh in éineacht le " +"gach fofhillteán atá iontu?" +msgstr[3] "" +"An bhfuil tú cinnte gur mian leat na %1 bhfillteán a scriosadh in éineacht " +"le gach fofhillteán atá iontu?" +msgstr[4] "" +"An bhfuil tú cinnte gur mian leat na %1 fillteán a scriosadh in éineacht le " +"gach fofhillteán atá iontu?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Scrios an fillteán?" +msgstr[1] "Scrios na fillteáin?" +msgstr[2] "Scrios na fillteáin?" +msgstr[3] "Scrios na fillteáin?" +msgstr[4] "Scrios na fillteáin?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Níorbh fhéidir an fillteán a scriosadh: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Theip ar scriosadh an fhillteáin" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Airíonna Fhillteán %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "An bhfuil tú cinnte gur mian leat an mhír roghnaithe a scriosadh?" +msgstr[1] "" +"An bhfuil tú cinnte gur mian leat na %1 mhír roghnaithe a scriosadh?" +msgstr[2] "" +"An bhfuil tú cinnte gur mian leat na %1 mhír roghnaithe a scriosadh?" +msgstr[3] "An bhfuil tú cinnte gur mian leat na %1 mír roghnaithe a scriosadh?" +msgstr[4] "An bhfuil tú cinnte gur mian leat na %1 mír roghnaithe a scriosadh?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "&Scrios an mhír" +msgstr[1] "&Scrios na míreanna" +msgstr[2] "&Scrios na míreanna" +msgstr[3] "&Scrios na míreanna" +msgstr[4] "&Scrios na míreanna" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Níorbh fhéidir an mhír a scriosadh: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Theip ar scriosadh" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Ainm:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Acmhainn Nua" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Níorbh fhéidir an acmhainn a chruthú: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Theip ar chruthú" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "An bhfuil tú cinnte gur mian leat an acmhainn seo a scriosadh?" +msgstr[1] "An bhfuil tú cinnte gur mian leat an %1 acmhainn seo a scriosadh?" +msgstr[2] "An bhfuil tú cinnte gur mian leat na %1 acmhainn seo a scriosadh?" +msgstr[3] "An bhfuil tú cinnte gur mian leat na %1 n-acmhainn seo a scriosadh?" +msgstr[4] "An bhfuil tú cinnte gur mian leat na %1 acmhainn seo a scriosadh?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Scrios an Acmhainn?" +msgstr[1] "Scrios na hAcmhainní?" +msgstr[2] "Scrios na hAcmhainní?" +msgstr[3] "Scrios na hAcmhainní?" +msgstr[4] "Scrios na hAcmhainní?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Níorbh fhéidir na sonraí a ghreamú: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Theip ar ghreamú" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, fuzzy, kde-format +#| msgid "Work Offline" +msgctxt "@action:button" +msgid "Go Online" +msgstr "Oibrigh As Líne" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Bog go dtí an Fillteán Seo" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Cóipeáil an Fillteán Seo" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to update subscription: %1" +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Síntiúis Logánta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Síntiúis Logánta" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Cuardaigh:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Liostáilte amháin" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Liostáil" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Díliostáil" + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create a new tag" +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "An error occurred while creating a new tag" +msgstr "Earráid agus mír á cruthú: %1" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete this resource?" +#| msgid_plural "Do you really want to delete %1 resources?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "An bhfuil tú cinnte gur mian leat an acmhainn seo a scriosadh?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "Scrios Mír" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "Scrios Mír" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Create new tag" +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Scrios Mír" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to open data file '%1'." +msgstr "Níorbh fhéidir cineál gníomhaire '%1' a fháil." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to parse data file '%1'." +msgstr "Níorbh fhéidir cineál gníomhaire '%1' a fháil." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "Níorbh fhéidir gníomhaire a chruthú." + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Invalid item retrieved" +msgid "Invalid file format." +msgstr "Fuarthas mír neamhbhailí" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "Níorbh fhéidir na sonraí a ghreamú: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "Bailiúchán neamhbhailí" + +#~ msgid "Id" +#~ msgstr "Aitheantas" + +#~ msgid "Remote Id" +#~ msgstr "Aitheantas Cianda" + +#~ msgid "MimeType" +#~ msgstr "Cineál MIME" + +#~ msgid "Form" +#~ msgstr "Foirm" + +#~ msgid "Default Name" +#~ msgstr "Ainm Réamhshocraithe" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Scrios" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Cealaigh" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Gan léamh" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Iomlán" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Méid" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Acmhainn Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Ainm" + +#~ msgid "Invalid collection specified" +#~ msgstr "Sonraíodh bailiúchán neamhbhailí" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "" +#~ "Aimsíodh leagan %1 an phrótacail, bhíothas ag súil le %2 ar a laghad" + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Braitheadh crann bailiúcháin logánta atá neamhréireach." + +#~ msgid "KDE Test Program" +#~ msgstr "Ríomhchlár Tástála KDE" + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Cláraíodh seirbhís chuardaigh Nepomuk le D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Cláraíodh seirbhís chuardaigh Nepomuk le D-Bus, rud a chiallaíonn go " +#~ "bhfuil sé i bhfeidhm de ghnáth." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Níl seirbhís chuardaigh Nepomuk cláraithe le D-Bus." + +#~ msgid "Cache" +#~ msgstr "Taisce" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Faigh polasaí taisce le hoidhreacht" + +#~ msgid "Cache Policy" +#~ msgstr "Polasaí Taisce" + +#~ msgid "Interval check time:" +#~ msgstr "Eatramh idir sheiceáil:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Teorainn ama an taisce logánta:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Sioncrónaigh ar éileamh" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Bainistigh cé acu fillteáin is mian leat a fheiceáil sa chrann" + +#~ msgid "Available Folders" +#~ msgstr "Fillteáin atá ar fáil" + +#~ msgid "Current Changes" +#~ msgstr "Athruithe Reatha" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Díliostáil ón fhillteán roghnaithe" + +#~ msgid "TODO" +#~ msgstr "LE DÉANAMH" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Acmhainn Akonadi" + +#~ msgctxt "@info, purpose of application" +#~ msgid "Akonadi Resource" +#~ msgstr "Acmhainn Akonadi" diff --git a/po/gl/akonadi_knut_resource.po b/po/gl/akonadi_knut_resource.po new file mode 100644 index 0000000..6ea737e --- /dev/null +++ b/po/gl/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Xosé , 2009, 2010. +# Adrián Chaves (Gallaecio) , 2018, 2019. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2019-01-01 14:02+0100\n" +"Last-Translator: Adrián Chaves (Gallaecio) \n" +"Language-Team: Galician \n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Non se seleccionou ningún ficheiro de datos." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Cargouse o ficheiro «%1» satisfactoriamente." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Seleccionar o ficheiro de datos" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Ficheiro de datos Knut de Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Non se atopou ningún elemento para o identificador remoto %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Non se atopou a colección pai na árbore DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Non se pode escribir a colección." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Non se atopou a colección modificada na árbore DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Non se atopou a colección eliminada na árbore DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Non se atopou a colección pai «%1» na árbore DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Non se pode escribir o elemento." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Non se atopou o elemento modificado na árbore DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Non se atopou o elemento eliminado na árbore DOM." diff --git a/po/gl/libakonadi5.po b/po/gl/libakonadi5.po new file mode 100644 index 0000000..e594fe6 --- /dev/null +++ b/po/gl/libakonadi5.po @@ -0,0 +1,2790 @@ +# translation of libakonadi.po to galician +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# mvillarino , 2008, 2009. +# marce villarino , 2009. +# Xosé , 2009, 2010. +# Marce Villarino , 2009. +# Xosé , 2009, 2011, 2013. +# Marce Villarino , 2012, 2013, 2014. +# Adrian Chaves Fernandez , 2013, 2015, 2016, 2017. +# Adrián Chaves (Gallaecio) , 2017, 2018, 2019. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2019-11-01 17:00+0100\n" +"Last-Translator: Adrián Chaves (Gallaecio) \n" +"Language-Team: Galician \n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 19.08.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Marce Villarino, Xosé Calvo, Adrian Chaves" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" +"mvillarino@users.sourceforge.net, xosecalvo@gmail.com, adrian@chaves.io" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Actualmente non hai ningunha conta configurada." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Non se permite a integración de contas" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Non se pode rexistrar o obxecto en D-Bus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 de tipo %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificador do axente" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Axente de Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Preparado" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Desconectado" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Estase a sincronizar…" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Erro." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Non configurado" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificador do recurso" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Recurso de Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Obtívose un elemento incorrecto." + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Produciuse un erro ao crear o elemento: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Produciuse un erro ao actualizar a colección: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Fallou a actualización da colección local: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "A actualización dos elementos locais fallou: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Non se pode obter o elemento no modo sen conexión." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sincronizando o cartafol «%1»." + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Non se puido obter a colección para sincronización." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Non se puido obter a colección para a sincronización de atributos." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "O elemento solicitado xa non existe." + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Cancelouse a tarefa." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Non hai tal colección." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Atopáronse coleccións orfas sen resolver" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Non se atopou outro elemento para xestionar un conflito" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Non se pode acceder á interface D-Bus do axente creado." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "A creación da instancia do axente esgotou o tempo límite." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Non se pode obter o tipo de axente «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Non se pode crear a instancia do axente." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Deuse unha instancia de colección que é incorrecta." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "A instancia do recurso é incorrecta." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Non se pode obter a interface D-Bus para o recurso «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "A sincronización dos atributos da colección esgotou o tempo límite." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "A colección para copiar é incorrecta." + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "A colección de destino é incorrecta." + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "O pai é incorrecto" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Non se puido analizar a colección da resposta." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "A colección é incorrecta" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Deuse unha colección que é incorrecta." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Non se indicou que obxectos mover" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Non se especificou ningún destino correcto" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "A colección é incorrecta." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "A colección nai é incorrecta." + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Non se pode conectar co servizo Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"A versión do protocolo do servizo Akonadi non é compatíbel. Verifique que " +"ten instalada unha versión compatíbel." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "O usuario cancelou a operación." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Erro descoñecido." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Resposta inesperada" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Non se puido crear a relación." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "A sincronización do recurso esgotou o tempo límite." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Non se puido obter a colección raíz do recurso %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Non se forneceu ningún identificador de recurso." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "O identificador de recurso «%1» é incorrecto" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Non se puido configurar o recurso predeterminado mediante D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Non se puido obter a colección de recursos." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Esgotouse o tempo de espera para obter o bloqueo." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Non se puido crear a etiqueta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Botar a colección no lixo fallou, interrompeuse a operación." + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Fornecéronse elementos incorrectos." + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Forneceuse unha colección incorrecta." + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" +"Non hai ningunha colección correcta, ou a lista de elementos está baleira." + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Non se puido atopar a funcionalidade de restaurar a colección, e a de " +"restaurar o recurso non está dispoñíbel." + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Cargando…" + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@window:title" +msgid "Error" +msgstr "Erro" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"A colección de destino «%1» xa contén\n" +"unha colección chamada «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Non se puido copiar o elemento:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Non se puido copiar a colección:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Non se puido mover o elemento:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Non se puido mover a colección:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Non se puido ligar a entidade:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@title:window" +msgid "Error" +msgstr "Erro" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Cartafoles favoritos" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Mensaxes totais" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Mensaxes sen ler" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Cota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Tamaño do almacenamento" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Tamaño do almacenamento do subcartafol" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Sen ler" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Tamaño" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiqueta" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Non se pode obter un elemento para o índice" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "O índice non está dispoñíbel máis" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "A parte de carga útil «%1» non está dispoñíbel para este índice" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Non hai sesión dispoñíbel para este índice" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Non hai elemento dispoñíbel para este índice" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Complemento sen nome" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Non se dispón de descrición" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"A versión do protocolo do servidor de Akonadi é distinta da versión do " +"protocolo que usa esta aplicación.\n" +"Se actualizou o sistema recentemente, saia e volva entrar para asegurarse de " +"que todas as aplicacións usan a versión correcta do protocolo." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Non hai axentes de Akonadi dispoñíbeis. Verifique a súa instalación de KDE " +"PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"As versións do protocolo non coinciden. A versión do servidor (%1) é máis " +"vella que a versión local (%2). Se actualizou o sistema recentemente " +"reinicie o servidor de Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"As versións do protocolo non coinciden. A versión do servidor (%1) é máis " +"nova que a versión local (%2). Se actualizou o sistema recentemente reinicie " +"todas as aplicacións de KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Proba de Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Comproba e notifica o estado do servidor de Akonadi." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nova instancia do axente…" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Elimina&r esta instancia do axente" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configurar a instancia do axente" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nova instancia do axente" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Non se puido crear a instancia do axente: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Fallou a creación da instancia do axente" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Eliminar esta instancia do axente?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Seguro que quere eliminar todas as instancias do axente escollidas?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuración de %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manual de %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Sobre %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "O diálogo de configuración abriuse noutra xanela" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "A configuración de %1 xa se abriu noutra parte." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Non se puido rexistrar o diálogo de configuración de %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuto" +msgstr[1] "minutos" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Obtención" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Empregar accións para o cartafol ou conta pai" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronizar ao escoller este cartafol" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronización automaticamente despois de:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nunca" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutos" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Partes na caché local" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opcións de obtención" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Obter sempre as &mensaxes completas." + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Obte&r o corpo das mensaxes cando se soliciten." + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Man ter os corpos das mensaxes localmente:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Para sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Buscar" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usar o cartafol predeterminado" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Novo subcartafol…" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Crear un subcartafol novo no cartafol escollido" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Novo cartafol" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nome" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Non se puido crear o cartafol: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Fallou a creación do cartafol" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Xeral" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un obxecto" +msgstr[1] "%1 obxectos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nome:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usar unha icona personalizada:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "cartafol" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estatísticas" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contido:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 obxectos" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Tamaño:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bytes" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Lembre que indexar pode tardar uns minutos." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Mantemento" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Erro ao obter a lista de elementos indexados." + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexouse %1 elemento neste cartafol." +msgstr[1] "Indexáronse %1 elementos neste cartafol." + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calculando os elementos indexados…" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Ficheiros" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipo de cartafol:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "descoñecido" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementos" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elementos totais:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Elementos sen ler:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexación" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Activar a indexación de texto completo." + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Obtendo o número de elementos indexados…" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indexar de novo o cartafol" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ningún cartafol" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Diálogo para abrir coleccións" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Escolla unha colección" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mover para aquí" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copiar aquí" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancelar" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Hora de modificación" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Bandeiras" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atributo: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolución de conflitos" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Usar a miña versión" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Usar a súa versión" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Manter ambas as dúas versións" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Os seus cambios están en conflito cos cambios feitos ao mesmo tempo por " +"outra persoa.
A menos que unha das dúas versións sexa prescindíbel, terá " +"que integrar os cambios manualmente.
Prema «Abrir un editor de texto» para manter unha copia dos textos, e a " +"continuación seleccione a versión que sexa máis correcta, logo ábraa de novo " +"e modifíquea para engadir o que falte." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Datos" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Estase a iniciar o servidor Akonadi…" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Estase a deter o servidor Akonadi…" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mover para aquí" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copiar aquí" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Ligar aquí" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancelar" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Non se pode conectar co servizo de xestión de información persoal.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "O servizo de xestión de información persoal estase a iniciar…" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "O servizo de xestión de información persoal estase a apagar…" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"O servizo de xestión de información persoal está a realizar unha anovación " +"da base de datos." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"O servizo de xestión de información persoal está a anovar a base de datos.\n" +"Isto ocorre despois de actualizar o software, e é necesario para optimizar o " +"rendemento.\n" +"Dependendo da cantidade de información persoal, podería levar algúns minutos." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"O servizo de xestión de información persoal Akonadi non está operativo. Esta " +"aplicación non se pode empregar sen el." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Iniciar" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"A infraestrutura de xestión de información persoal Akonadi non está " +"operativa.\n" +"Prema «Detalles» para obter información detallada sobre este problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"O servizo de xestión de información persoal Akonadi non está operativo." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalles…" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Quere retirar a conta «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Retirar esta conta?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Contas de recepción (engada polo menos unha):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Engadir…" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modificar…" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Retirar" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reiniciar" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Cartafol recente" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Renomear o favorito" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Nome:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Probas internas do servidor Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Gardar o informe…" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copiar o informe no portapapeis" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"O configuración actual do servidor Akonadi require o controlador de QtSQL " +"«%1», que se atopou no sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"A configuración actual do servidor Akonadi require o controlador de QtSQL " +"«%1».\n" +"Están instalados estes controladores: %2.\n" +"Verifique que estea instalado o controlador requirido." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Atopouse o controlador da base de datos." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Non se atopou o controlador da base de datos." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "O executábel do servidor de MySQL non está probado." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "A configuración actual non require dun servidor de MySQL interno." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Ten Akonadi configurado para empregar o servidor de MySQL «%1».\n" +"Verifique que ten o servidor de MySQL instalado, configurada a ruta correcta " +"e que ten os permisos correctos de lectura e execución do executábel do " +"servidor. Normalmente, o executábel do servidor chámase «mysqld» e onde " +"estea depende da distribución." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Non se atopou o servidor de MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Non é posíbel ler o servidor de MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Non é posíbel executar o servidor de MySQL." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Atopouse MySQL cun nome que non se agardaba." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Atopouse o servidor de MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Atopouse o servidor de MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "O servidor de MySQL é executábel." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Fallou a execución do servidor de MySQL «%1» coa mensaxe de erro seguinte: " +"«%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "A execución do servidor de MySQL fallou." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "O rexistro de erros do servidor de MySQL non está probado." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Non se atopou ningún rexistro actual de erros de MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"O servidor de MySQL non informou en %1 de erros durante o arranque. Pódese " +"atopar o rexistro en «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Non é posíbel ler o rexistro de erros de MySQL." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Atopouse un ficheiro de rexistro de erros do servidor de MySQL pero non é " +"lexíbel: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "O rexistro do servidor de MySQL contén erros." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" +"O ficheiro «%1» de rexistro de erros do servidor de MySQL contén erros." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "O rexistro do servidor de MySQL contén avisos." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "O ficheiro «%1» de rexistro do servidor de MySQL contén avisos." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "O rexistro do servidor de MySQL non contén ningún erro." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"O ficheiro «%1» de rexistro do servidor MySQL non contén ningún erro nin " +"aviso." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "A configuración do servidor MySQL non se probou." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Atopouse a configuración predeterminada do servidor MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Atopouse en %1 a configuración predeterminada do servidor MySQL e é lexíbel." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Non se atopou a configuración predeterminada do servidor MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Non se atopou a configuración predeterminada do servidor MySQL ou esta non " +"era lexíbel. Comprobe que a instalación de Akonadi estea completa e que ten " +"todos os dereitos de acceso requiridos." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" +"Non está dispoñíbel a configuración personalizada do servidor de MySQL." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Non se atopou a configuración personalizada do servidor MySQL, pero é " +"opcional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Atopouse a configuración personalizada do servidor MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Atopouse en %1 a configuración personalizada do servidor MySQL e é lexíbel." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "A configuración personalizada do servidor MySQL non é lexíbel." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Atopouse en %1 a configuración personalizada do servidor MySQL, pero non é " +"lexíbel. Comprobe os permisos." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Non se atopou a configuración do servidor MySQL ou non é lexíbel." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Non se atopou a configuración do servidor MySQL ou non é lexíbel." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "A configuración do servidor MySQL é utilizábel." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Atopouse en %1 a configuración do servidor MySQL, e é lexíbel." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Non se pode conectar co servidor de PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Atopouse o servidor de PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Atopouse o servidor de PostgreSQL e a conexión está a funcionar." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "non se atopou akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"O programa «akonadictl» debe estar accesíbel mediante $PATH. Verifique que " +"ten o servidor Akonadi instalado." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "Atopouse akonadictl e está listo para usar" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Atopouse o programa «%1» de control do servidor Akonadi e executouse " +"correctamente.\n" +"Resultado:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "Atopouse akonadictl pero non é utilizábel" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Atopouse o programa «%1» de control do servidor Akonadi pero non se puido " +"executar correctamente.\n" +"Resultado:\n" +"%2\n" +"Asegúrese de que o servidor Akonadi estea instalado correctamente." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "O proceso de control de Akonadi rexistrouse en D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"O proceso de control de Akonadi está rexistrado en D-Bus, o que xeralmente " +"indica que é operativo." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "O proceso de control de Akonadi non está rexistrado en D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"O proceso de control de Akonadi non está rexistrado en D-Bus, o que " +"xeralmente indica que non se iniciou ou que se atopou cun erro fatal durante " +"o arranque." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "O proceso servidor de Akonadi está rexistrado en D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"O proceso servidor de Akonadi está rexistrado en D-Bus, o que xeralmente " +"indica que é operativo." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "O proceso servidor de Akonadi non está rexistrado en D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"O proceso servidor de Akonadi non está rexistrado en D-Bus, o que xeralmente " +"indica que non se iniciou ou que se atopou cun erro fatal durante o arranque." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Non é posíbel comprobar a versión do protocolo." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Non é posíbel comprobar se a versión do protocolo se axusta aos " +"requirimentos se non hai conexión co servidor." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "A versión do protocolo do servidor é vella de máis." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"A versión do protocolo do servidor é %1, pero o cliente necesita a versión " +"%2. Se actualizou KDE PIM recentemente, reinicie tanto o servidor de Akonadi " +"como as aplicacións de KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "A versión do protocolo do servidor é nova de máis." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "A versión do protocolo do servidor coincide." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "A versión actual do protocolo é a %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Atopáronse axentes de recurso." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Atopouse polo menos un axente de recurso." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Non se atopou ningún axente de recurso." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Non se atopou ningún axente de recurso e Akonadi non é utilizábel sen polo " +"menos un deles. Isto polo xeral significa que non se instalou ningún axente " +"de recursos ou que hai un problema de configuración. Buscouse nas rutas " +"seguintes: «%1». A variábel de contorno XDG_DATA_DIRS ten o valor «%2»; " +"verifique que isto inclúe todas as rutas nas que estean instalados os " +"axentes de Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Non se atopou ningún rexistro de erros do servidor Akonadi actual." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "O servidor Akonadi non informou de ningún erro durante esta arrancada." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Atopouse un rexistro de erro do servidor Akonadi actual." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"O servidor Akonadi informou de erros durante esta arrancada. Pódese atopar o " +"rexistro en %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Non se atopou ningún rexistro anterior de erros do servidor Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"O servidor Akonadi non informou de ningún erro durante a arrancada anterior." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Atopouse un rexistro previo de erros do servidor Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"O servidor Akonadi informou de erros durante o arranque anterior. Pódese " +"atopar o rexistro en %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Non se atopou ningún rexistro actual de erros de Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"O proceso de control de Akonadi non informou de ningún erro durante esta " +"arrancada." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Atopouse o rexistro actual de erros de Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"O proceso de control de Akonadi informou de erros durante esta arrancada. " +"Pódese atopar o rexistro en %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Non se atopou ningún rexistro anterior de erros de Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"O proceso de control de Akonadi non informou de ningún erro durante a " +"anterior arrancada." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Atopouse un rexistro previo de erros do control de Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"O proceso de control de Akonadi informou en %1 de erros durante o anterior " +"arranque. Pódese atopar o rexistro en %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi iniciouse como root." + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Executar aplicacións que acceden a Internet como root ou administrador expón " +"a moitos riscos de seguranza. A versión de MySQL que esta instalación de " +"Akonadi usa non permite executarse como root para protexer fronte a estes " +"riscos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi non se está a executar como root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi non se está a executar como usuario root ou administrador, que é a " +"configuración recomendada para un sistema seguro." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Gardar o informe da proba" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Erro" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Non se puido abrir o ficheiro «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Produciuse un erro durante o arranque do servidor Akonadi. As probas " +"automáticas seguintes axudarano a atopar e resolver este problema. Ao " +"solicitar asistencia técnica ou informar de fallos inclúa sempre este " +"informe." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalles" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Para obter máis axuda para arranxar problemas, consulte userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Novo cartafol…" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Novo" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "Elimina&r o cartafol" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Eliminar" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Sincronizar o cartafol" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronizar" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propiedades do cartafol" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propiedades" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Pegar" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "&Pegar" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Xestionar as &subscricións locais…" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Xestionar as subscricións locais" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Engadir aos Cartafoles Favoritos" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Engadir aos favoritos" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Retirar dos cartafoles favoritos" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Retirar dos favoritos" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Renomear o favorito…" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Renomear" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copiar o cartafol para…" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copiar en" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copiar o elemento en…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mover o elemento para…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mover para…" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mover o cartafol para…" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "Re&cortar o elemento" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Recortar" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "Re&cortar o cartafol" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Crear un recurso" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Eliminar o recurso" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "P&ropiedades do recurso" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Sincronizar o recurso" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Traballar sen conexión" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronizar o cartafol recursivamente" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronizar recursivamente" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Botar o cartafol no lixo" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Botar o cartafol no lixo" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Botar o elemento no lixo" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Botar o elemento no lixo" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restaurar o cartafol do lixo" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restaurar o cartafol do lixo" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restaurar o elemento do lixo" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restaurar o elemento do lixo" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restaurar a colección do lixo" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restaurar o a colección do lixo" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronizar os cartafoles favoritos" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronizar os cartafoles favoritos" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronizar a árbore de cartafoles" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copiar o cartafol" +msgstr[1] "&Copiar os %1 cartafoles" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copiar o elemento" +msgstr[1] "&Copiar os %1 elementos" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Re&cortar o elemento" +msgstr[1] "Re&cortar os %1 elementos" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Re&cortar o cartafol" +msgstr[1] "Re&cortar os %1 cartafoles" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Elimina&r este elemento" +msgstr[1] "Elimina&r estes %1 elementos" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Elimina&r o cartafol" +msgstr[1] "Elimina&r %1 cartafoles" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronizar o cartafol" +msgstr[1] "&Sincronizar %1 cartafoles" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Elimina&r o recurso" +msgstr[1] "Elimina&r os %1 recursos" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronizar o recurso" +msgstr[1] "&Sincronizar os %1 recursos" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copiar o cartafol" +msgstr[1] "Copiar os %1 cartafoles" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copiar o elemento" +msgstr[1] "Copiar os %1 elementos" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Recortar o elemento" +msgstr[1] "Recortar os %1 elementos" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Recortar o cartafol" +msgstr[1] "Recortar os %1 cartafoles" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Eliminar este elemento" +msgstr[1] "Eliminar estes %1 elementos" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Eliminar o cartafol" +msgstr[1] "Eliminar os %1 cartafoles" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronizar o cartafol" +msgstr[1] "Sincronizar os %1 cartafoles" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Eliminar o recurso" +msgstr[1] "Eliminar os %1 recursos" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronizar o recurso" +msgstr[1] "Sincronizar os %1 recursos" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nome" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Seguro que quere eliminar este cartafol e todos os seus subcartafoles?" +msgstr[1] "" +"Seguro que quere eliminar os %1 cartafoles e todos os seus subcartafoles?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Eliminar o cartafol?" +msgstr[1] "Eliminar os cartafoles?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Non se puido eliminar o cartafol: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Fallou a eliminación do cartafol" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propiedades do cartafol %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Seguro que quere eliminar o elemento escollido?" +msgstr[1] "Seguro que quere eliminar %1 elementos?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Eliminar este elemento?" +msgstr[1] "Eliminar estes elementos?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Non se puido eliminar o elemento: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Fallou a eliminación do elemento" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Renomear o favorito" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nome:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Novo Recurso" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Non se puido crear o recurso: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Fallou a creación do recurso" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Seguro que quere eliminar este recurso?" +msgstr[1] "Seguro que quere eliminar estes %1 recursos?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Eliminar este recurso?" +msgstr[1] "Eliminar estes recursos?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Non se puideron pegar os datos: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Pegar fallou." + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Non pode engadirse «/» ao nome do cartafol." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Erro ao crear o novo cartafol" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Non pode engadirse «.» ao principio ou final do nome do cartafol." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Antes de sincronizar o cartafol «%1», o recurso ten que estar en liña. Quere " +"poñelo en liña?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "A conta «%1» está desconectada." + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Poñerse en liña" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mover para este cartafol" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copiar neste cartafol" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to update subscription: %1" +msgstr "Non se puido crear a relación." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Subscricións locais…" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Subscricións locais…" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Buscar:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Só os subscritos" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Subscribirse" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Cancelar a subscrición" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Non se puido crear unha etiqueta nova" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Produciuse un erro ao crear unha etiqueta nova" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Seguro que quere retirar a etiqueta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Eliminar esta etiqueta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Eliminar esta etiqueta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "Configurar as etiquetas que se desexa aplicar." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "Crear unha etiqueta nova" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Manage Tags" +msgid "Manage Tags" +msgstr "Xestionar as etiquetas" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Eliminar esta etiqueta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Limpar" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Prema para engadir etiquetas" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "…" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Convertedor de Akonadi en XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Converte unha subárbore dunha colección de Akonadi nun ficheiro XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Non se cargou ningún dato." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Non se especificou ningún nome de ficheiro" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Non se pode abrir o ficheiro de datos «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "O ficheiro %1 non existe." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Non se pode analizar o ficheiro de datos «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Non se puido cargar e analizar a definición do esquema." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Non se pode crear un contexto para o analizador de esquemas." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Non se pode crear o esquema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Non se pode crear un contexto de validación de esquemas." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "O formato de ficheiro é incorrecto." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Non se pode procesar o ficheiro de datos: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Non se pode atopar a colección %1" + +#~ msgid "Id" +#~ msgstr "Identificador" + +#~ msgid "Remote Id" +#~ msgstr "Identificador remoto" + +#~ msgid "MimeType" +#~ msgstr "Tipo mime" + +#~ msgid "Default Name" +#~ msgstr "Nome predeterminado" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Eliminar" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Cancelar" + +#~ msgid "Take left one" +#~ msgstr "Tomar o da esquerda" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Hai dúas actualizacións en conflito entre si.Escolla a(s) " +#~ "actualización(s) que desexa aplicar." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Sen ler" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Total" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Tamaño" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Recurso de Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nome" + +#~ msgid "Invalid collection specified" +#~ msgstr "Especificouse unha colección que non é válida" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Atopouse a versión %1 do protocolo; agardábase polo menos a %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "A versión do protocolo do servidor é recente de abondo." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "A versión do protocolo do servidor é %1, que é igual ou máis nova que a " +#~ "versión requirida %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Detectouse unha árbore de coleccións local que é inconsistente." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Forneceuse unha colección remota sen cadea de devanceiros que remate na " +#~ "raíz; o recurso está danado." + +#~ msgid "KDE Test Program" +#~ msgstr "Programa de proba de KDE" + +#~ msgid "Cannot list root collection." +#~ msgstr "Non é posíbel listar a colección raíz." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "O servizo de procuras Nepomuk está rexistrado en D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "O servizo de procuras está rexistrado en D-Bus, o que xeralmente indica " +#~ "que é operativo." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "O servizo de procuras Nepomuk non está rexistrado en D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "O servizo de procuras Nepomuk non está rexistrado en D-Bus, o que " +#~ "xeralmente indica que non foi iniciado ou que atopou un erro fatal " +#~ "durante o arranque." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "" +#~ "O servizo de procuras Nepomuk emprega unha infraestrutura inadecuada." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "O servizo de procuras Nepomuk emprega a infraestrutura \"%1\", que non se " +#~ "recomenda utilizar co Akonadi." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "" +#~ "O servizo de procuras Nepomuk emprega unha infraestrutura inadecuada. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "O servizo de procuras Nepomuk emprega unha das infraestruturas " +#~ "recomendadas." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "O complemento «%1» non foi construído de maneira estática; Inclúa esta " +#~ "información no informe de erro." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Complemento non construído estaticamente" diff --git a/po/hu/akonadi_knut_resource.po b/po/hu/akonadi_knut_resource.po new file mode 100644 index 0000000..9f4336a --- /dev/null +++ b/po/hu/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Kristóf Kiszel , 2011. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2011-12-22 01:30+0100\n" +"Last-Translator: Kristóf Kiszel \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nincs kijelölve adatfájl." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "A(z) „%1” fájl sikeresen betöltve." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Adatfájl kiválasztása" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut adatfájl" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nem található elem a(z) %1 távoli azonosítóhoz" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "A szülőgyűjtemény nem található a DOM-fában." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Nem lehet írni a gyűjteményt." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "A módosított gyűjtemény nem található a DOM-fában." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "A törölt gyűjtemény nem található a DOM-fában." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "A(z) „%1” szülőgyűjtemény nem található a DOM-fában." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Nem lehet írni az elemet." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "A módosított elem nem található a DOM-fában." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "A törölt elem nem található a DOM-fában." diff --git a/po/hu/libakonadi5.po b/po/hu/libakonadi5.po new file mode 100644 index 0000000..c305713 --- /dev/null +++ b/po/hu/libakonadi5.po @@ -0,0 +1,2778 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Tamas Szanto , 2008. +# Kristóf Kiszel , 2010, 2011, 2012, 2014, 2020, 2021. +# Balázs Úr , 2012, 2013, 2014. +msgid "" +msgstr "" +"Project-Id-Version: KDE 4.3\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-07-04 12:19+0200\n" +"Last-Translator: Kristóf Kiszel \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 21.07.70\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Kiszel Kristóf,Szántó Tamás,Úr Balázs" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ulysses@kubuntu.org,tszanto@interware.hu,urbalazs@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Nincs beállítva fiók." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "A fiókintegráció nem támogatott" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Nem sikerült regisztrálni az objektumot a dbus-on: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%2 típusú %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Azonosító" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi szolgáltatás" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Kész" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Kapcsolat nélkül" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Szinkronizálás…" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Hiba." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nincs beállítva" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Erőforrás-azonosító" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-erőforrás" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Érvénytelen elem lett fogadva" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Hiba az elem létrehozása közben: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Hiba a gyűjtemény frissítése közben: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "A helyi gyűjtemény frissítése nem sikerült: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "A helyi elemek frissítése nem sikerült: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Nem lehet elemet letölteni kapcsolat nélküli módban." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "„%1” mappa szinkronizálása" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Nem sikerült letölteni az erőforrás gyűjteményt szinkronizáláshoz." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Nem sikerült letölteni a gyűjteményt attribútumszinkronizáláshoz." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "A kért elem már nem létezik" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Feladat megszakítva." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nem létezik ilyen gyűjtemény." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Feloldatlan árva gyűjtemények találhatók" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Nem található egyéb elem az ütközéskezeléshez." + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Nem sikerült hozzáférni a létrehozott ügynök D-Bus felületéhez." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Az ügynökpéldány létrehozása túllépte az időkorlátot." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Nem sikerült megszerezni a(z) „%1” ügynöktípust." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Nem sikerült új ügynökpéldányt létrehozni." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Érvénytelen gyűjteménypéldány." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Érvénytelen erőforráspéldány." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Nem sikerült megszerezni a(z) „%1” erőforrás D-Bus interfészét" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "A gyűjtemény jellemzőinek szinkronizálása túllépte az időkorlátot." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Érvénytelen gyűjtemény, nem másolható" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Érvénytelen célgyűjtemény" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Érvénytelen szülő" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "A válaszban kapott gyűjtemény feldolgozása sikertelen" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Érvénytelen gyűjtemény" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Érvénytelen gyűjtemény lett megadva." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nincsenek objektumok megadva az áthelyezéshez" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nincs megadva érvényes cél" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Érvénytelen gyűjtemény." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Érvénytelen szülőgyűjtemény" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Nem sikerült csatlakozni az Akonadi szolgáltatáshoz." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Az Akonadi szolgáltatás verziója nem megfelelő. Biztosítani kell, hogy a " +"telepített szolgáltatás verziója megfelelő legyen." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "A felhasználó megszakította a műveletet." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Ismeretlen hiba." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Nem várt válasz" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Nem sikerült kapcsolatot létrehozni." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Az erőforrás szinkronizálása túllépte az időkorlátot." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Nem sikerült lekérni a(z) „%1” erőforrás gyökérgyűjteményét." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Nincs megadva erőforrás-azonosító." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Érvénytelen erőforrás-azonosító: „%1”" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Nem sikerült beállítani az alapértelmezett erőforrást a D-Bus által." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Nem sikerült letölteni az erőforrás gyűjteményt." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Időtúllépés a zár megszerzésének kísérlete közben." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Nem sikerült címkét létrehozni." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Nem sikerült a gyűjtemény áthelyezése a kukába, a kuka művelet megszakítása" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Érvénytelen elem lett átadva" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Érvénytelen gyűjtemény lett átadva" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Nincs érvényes gyűjtemény vagy üres az elemlista" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Nem található a visszaállítás gyűjtemény és a visszaállítás erőforrás nem " +"érhető el" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Név" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Betöltés…" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Hiba" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"A(z) „%1” célgyűjtemény már tartalmaz\n" +"„%2” nevű gyűjteményt." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Név" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Nem másolható az elem: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Nem másolható a gyűjtemény: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Nem helyezhető át az elem: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Nem helyezhető át a gyűjtemény: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Nem sikerült összekapcsolni a bejegyzést: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Hiba" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Kedvenc mappák" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Összes üzenet" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Olvasatlan üzenetek" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvóta" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Tárolóméret" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Almappa tárolóméret" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Olvasatlan" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Összesen" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Méret" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Címke" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Nem sikerült lekérni az elemet az indexhez" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Az index többé nem érhető el" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "A(z) „%1” hasznos teher rész nem érhető el ehhez az indexhez" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Nincs elérhető munkamenet ehhez az indexhez" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Nincs elérhető elem ehhez az indexhez" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Névtelen bővítmény" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Nincs elérhető leírás" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Az Akonadi kiszolgáló protokollverziója eltér a kliens által használt " +"protokoll verziójától.\n" +"Ha nemrég frissítette a rendszert, jelentkezzen ki és be, hogy az " +"alkalmazások biztosan a megfelelő protokollverziót használják." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "Nem érhető el Akonadi ügynök. Ellenőrizze a KDE PIM telepítését." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"A protokollverziók eltérnek. A kiszolgálóé (%1) régebbi, mint az Öné (%2). " +"Ha nemrég frissítette a rendszert, indítsa újra az Akonadi kiszolgálót." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"A protokollverziók eltérnek. A kiszolgálóé újabb (%1), mint az Öné (%2). Ha " +"nemrég frissítette a rendszert, indítsa újra az összes KDE PIM alkalmazást." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi önteszt" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Ellenőrzi és jelenti az Akonadi kiszolgáló állapotát" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© Volker Krause, 2008. " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Ú&j ügynökpéldány…" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Ügynökpéldány &törlése" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Ügynökpéldány &beállítása" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Új ügynökpéldány" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Nem sikerült új ügynökpéldányt létrehozni: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Az ügynökpéldány létrehozása nem sikerült" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Ügynökpéldány törlése?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Biztosan törölni szeretné a kijelölt ügynökpéldányt?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "A(z) %1 beállításai" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "A(z) %1 kézikönyve" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "A(z) %1 névjegye" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "A beállítóablak egy másik ablakban lett megnyitva" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "A(z) %1 beállításai máshol vannak megnyitva." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "A(z) %1 beállítóablak regisztrációja nem sikerült." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "perc" +msgstr[1] "perc" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Letöltés" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Beállítások használata a szülő mappából vagy fiókból" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Szinkronizálás a mappa kiválasztásakor" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatikus szinkronizáció ez után:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Soha" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "perc" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Helyileg gyorsítótárazott részek" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Letöltési beállítások" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Mindig töltse le a teljes üze&netet" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Üzenettö&rzsek lekérése igény szerint" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Üzenettörzsek megtartása helyileg ehhez:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Örökre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Keresés" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Mappa használata alapértelmezettként" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Ú&j almappa…" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Új almappa létrehozása a jelenleg kiválasztott mappa alá" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Új mappa" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Név" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Nem sikerült létrehozni ezt a mappát: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "A mappa létrehozása nem sikerült" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Általános" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Egy objektum" +msgstr[1] "%1 objektum" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "Né&v:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Egyedi ikon:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "mappa" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statisztika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Tartalom:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objektum" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Méret:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 bájt" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Az indexelés néhány percbe is beletelhet." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Karbantartás" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Hiba az indexelt elemek számának lekérésekor" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 elem indexelve ebben a mappában" +msgstr[1] "%1 elem indexelve ebben a mappában" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Indexelt elemek számolása…" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fájlok" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Mappatípus:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "ismeretlen" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elemek" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Összes elem:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Olvasatlan elem:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexelés" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Teljes szöveges indexelés bekapcsolása" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Indexelt elemek számának lekérése…" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Mappa újraindexelése" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Nincs mappa" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Gyűjteményablak megnyitása" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Gyűjtemény kiválasztása" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Mo&zgatás ide" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Má&solás ide" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Mégsem" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Módosítás ideje" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Zászlók" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribútum: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Az ütközés feloldása" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Saját megtartása" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Másik megtartása" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mindkettő megtartása" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"A változtatási ütközben valaki más módosításaival.
Hacsak valamelyik " +"verzió nem dobható el, a változásokat kézzel kell egyesíteni.
Kattintson " +"a Szövegszerkesztő megnyitása gombra a " +"szövegek másolatának megtartásához, majd válassza ki, melyik verzió helyes, " +"és nyissa meg újra azt, végül módosítsa a hiányzó dolgok hozzáadásával." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Adat" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Az Akonadi szolgáltatás elindul..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Az Akonadi szolgáltatás leáll..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Át&helyezés ide" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Másolás ide" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Hivatkozás ide" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Mégse" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Nem sikerült csatlakozni a személyi információ-kezelő szolgáltatáshoz.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "A személyes-információ kezelő szolgáltatás indul…" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "A személyes-információ kezelő szolgáltatás leáll…" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"A személyes-információ kezelő szolgáltatás adatbázis frissítést hajt végre." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"A személyes információ-kezelő szolgáltatás adatbázis-frissítést hajt végre.\n" +"Ez egy szoftverfrissítés után történik és szükséges a teljesítmény " +"optimalizálásához.\n" +"A személyes információk mennyiségétől függően ez eltarthat néhány percig." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Az Akonadi személyes-információ kezelő szolgáltatás nem fut. Ez az " +"alkalmazás nem használható anélkül." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Indítás" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Az Akonadi személyes-információ kezelő szolgáltatás keretrendszer nem " +"üzemképes.\n" +"Kattintson a „Részletek…” gombra, ha részletes leírást szeretne kapni a " +"hibáról." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Az Akonadi személyes-információ kezelő szolgáltatás nem üzemképes." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Részletek…" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Biztosan törölni szeretné ezt a fiókot: „%1”?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Fiók eltávolítása?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Beérkező fiókok (adjon hozzá legalább egyet):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "Hozzáa&dás…" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Módosítás…" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Eltávolítás" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Újraindítás" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Legutóbbi mappa" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Kedvenc átnevezése" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Név:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi kiszolgáló önteszt" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Jelentés mentése…" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Jelentés másolása a vágólapra" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"A(z) „%1” QtSQL-illesztőprogramra szüksége van a jelenlegi Akonadi " +"kiszolgáló beállításainak és nem található a rendszerén." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"A(z) „%1” QtSQL-illesztőprogramra szüksége van az Akonadi kiszolgáló " +"beállításainak.\n" +"Ezek az illesztőprogramok vannak telepítve: %2.\n" +"Kérjük telepítse a szükséges illesztőprogramot." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Adatbázis-illesztőprogram elérhető." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Nem található adatbázis-illesztőprogram." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "A MySQL kiszolgáló programfájlja ne lett letesztelve." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "A mostani beállítások mellett nincs szükség belső MySQL kiszolgálóra." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Az Akonadit jelenleg a(z) „%1” MySQL kiszolgáló használatára állította be.\n" +"Győződjön meg arról, hogy telepítve van-e a MySQL kiszolgáló, be van-e " +"állítva a helyes útvonal és biztosítva vannak-e a szükséges olvasási és " +"végrehajtási jogok a kiszolgáló programfájljára. A kiszolgáló programfájl " +"neve tipikusan „mysqld”, de a helye változik a disztribúciótól függően." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Nem található MySQL kiszolgáló." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "A MySQL kiszolgáló nem olvasható." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "A MySQL kiszolgáló nem végrehajtható." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "A MySQL programfájl neve eltér a várttól." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL kiszolgáló elérhető." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Elérhető MySQL kiszolgáló: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "A MySQL kiszolgáló végrehajtható." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Nem sikerült elindítani ezt a MySQL kiszolgálót: \"%1\". A hibaüzenet: " +"\"%2\"." + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Nem sikerült elindítani a MySQL kiszolgálót." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "A MySQL kiszolgáló naplófájlja nem tesztelt." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Nem érhető el aktuális MySQL naplófájl." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"A MySQL kiszolgáló nem jelzett hibát indulás közben. A napló megtalálható " +"itt: „%1”." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "A MySQL naplófájl nem olvasható." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "A MySQL kiszolgáló naplófájlja elérhető, de nem olvasható: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "A MySQL naplófájlja hibákat tartalmaz." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "A MySQL kiszolgáló naplófájlja („%1”) hibákat tartalmaz." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "A MySQL kiszolgáló naplófájlja figyelmeztetéseket tartalmaz." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "A MySQL kiszolgáló naplófájlja („%1”) figyelmeztetéseket tartalmaz." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "A MySQL kiszolgáló naplófájlja nem tartalmaz hibákat." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"A MySQL kiszolgáló naplófájlja („%1”) nem tartalmaz hibákat vagy " +"figyelmeztetéseket." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "A MySQL kiszolgáló beállításai nem lettek tesztelve." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "A MySQL kiszolgáló alapértelmezett beállítófájlja elérhető." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"A MySQL kiszolgáló alapértelmezett beállítófájlja elérhető és olvasható itt: " +"%1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "A MySQL kiszolgáló alapértelmezett beállítófájlja nem található." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"A MySQL kiszolgáló alapértelmezett beállítófájlja nem található vagy nem " +"olvasható. Ellenőrizze, hogy az Akonadi teljesen telepítve van-e és " +"megfelelőek-e a jogosultságok." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Nem található egyedi MySQL beállítófájl." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Nem található egyedi MySQL beállítófájl, de erre nincs feltétlenül szükség." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Elérhető egyedi MySQL beállítófájl." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Egy egyedi MySQL beállítófájl elérhető és olvasható itt: %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Nem olvasható egy egyedi MySQL beállítófájl." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Egy egyedi beállítófájl található a MySQL kiszolgálóhoz itt: %1, de a fájl " +"nem olvasható. Ellenőrizze a hozzáférési jogosultságot." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Nem található vagy nem olvasható a MySQL kiszolgáló beállítófájlja." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Nem található vagy nem olvasható a MySQL kiszolgáló beállítófájlja." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Található beállítófájl a MySQL kiszolgálóhoz." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Elérhető és olvasható a következő MySQL beállítófájl: %1." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Nem lehet kapcsolódni a PostgreSQL kiszolgálóhoz." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL kiszolgáló található." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL kiszolgáló található és a kapcsolat működik." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "nem található: akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Az „akonadictl” program mappájának szerepelnie kell az elérési útban ($PATH " +"környezeti változó). Ellenőrizze, hogy az Akonadi szolgáltatás telepítve van-" +"e." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl program: elérhető és használható" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Az Akonadi szolgáltatás beállítóprogramja (\"%1\") elérhető, sikerült " +"elindítani.\n" +"Az eredmény:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl program: elérhető, de nem használható" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Az Akonadi szolgáltatás beállítóprogramja (\"%1\") elérhető, de nem sikerült " +"elindítani.\n" +"Az eredmény:\n" +"%2\n" +"Ellenőrizze, megfelelően telepítve van-e az Akonadi szolgáltatás." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Az Akonadi vezérlőfolyamat regisztrálva a D-Bus-ban." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Az Akonadi vezérlőfolyamat regisztrálva van a D-Bus-ban, amely általában azt " +"jelenti, hogy működik." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Az Akonadi vezérlőfolyamat nincs regisztrálva a D-Bus-ban." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Az Akonadi vezérlőfolyamat nincs regisztrálva a D-Bus-ban, amely általában " +"azt jelenti, hogy nem indult el vagy egy hiba miatt kilépett." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Az Akonadi kiszolgálófolyamat regisztrálva van a D-Bus-ban." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Az Akonadi kiszolgálófolyamat regisztrálva van a D-Bus-ban, amely általában " +"azt jelenti, hogy működik." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Az Akonadi kiszolgálófolyamat nincs regisztrálva a D-Bus-ban." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Az Akonadi kiszolgálófolyamat nincs regisztrálva a D-Bus-ban, amely " +"általában azt jelenti, hogy nem indult el vagy egy hiba miatt kilépett." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Nem lehet ellenőrizni a protokollverziót." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Nincs kapcsolat a szolgáltatással, ezért nem lehet letesztelni a " +"protokollverziót." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "A szolgáltatás protokollverziója túl régi." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"A kiszolgáló protokollverziója: %1. Legalább %2 verzió szükséges. Ha nemrég " +"frissítette a KDE PIM-et, indítsa újra az Akonadi és KDE PIM alkalmazásokat " +"is." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "A szolgáltatás protokollverziója túl új." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "A kiszolgáló protokollverziója egyezik." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "A jelenlegi protokollverzió: %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Erőforrás ügynökök elérhetők." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Legalább egy erőforrás ügynök elérhető." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Erőforrás ügynökök nem találhatók." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nem található erőforrás ügynök, az Akonadi nem használható legalább egy " +"nélkül. Ez általában azt jelenti, hogy nincs erőforrás ügynök telepítve vagy " +"beállítási probléma van. A következő útvonalak lettek átnézve: „%1”. Az " +"XDG_DATA_DIRS környezeti változó „%2” értékre van állítva. Győződjön meg " +"róla, hogy ez tartalmaz minden olyan útvonalat, ahová az Akonadi ügynökök " +"telepítve vannak." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Jelenlegi Akonadi kiszolgáló hibanapló nem található." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Az Akonadi kiszolgáló nem jelzett hibát a jelenlegi induláskor." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Jelenlegi Akonadi kiszolgáló hibanapló található." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Az Akonadi kiszolgáló hibákat jelzett a jelenlegi induláskor. A napló " +"megtalálható itt: %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Korábbi Akonadi kiszolgáló hibanapló nem található." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Az Akonadi kiszolgáló nem jelzett hibát az előző induláskor." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Korábbi Akonadi kiszolgáló hibanapló található." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Az Akonadi kiszolgáló hibákat jelzett az előző induláskor. A napló " +"megtalálható itt: %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Jelenlegi Akonadi vezérlő hibanapló nem található." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "Az Akonadi vezérlő folyamat nem jelzett hibát a jelenlegi induláskor." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Jelenlegi Akonadi vezérlő hibanapló található." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Az Akonadi vezérlő folyamat hibákat jelzett a jelenlegi induláskor. A napló " +"megtalálható itt: %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Nem található korábbi naplófájl az Akonadi-vezérlőhöz." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "Az Akonadi-vezérlő nem jelzett hibákat az előző induláskor." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Korábbi Akonadi vezérlő hibanapló található." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Az Akonadi vezérlő folyamat hibákat jelzett az előző induláskor. A napló " +"megtalálható itt: %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Az Akonadi rendszergazdaként lett indítva" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Az internetről látszó alkalmazások futtatása rendszergazdaként számos " +"biztonsági veszélyt jelent. Az Akonadi telepítés által használt MySQL nem " +"fogja lehetővé tenni a rendszergazdaként való futtatást, hogy megvédje önt " +"ezektől a kockázatoktól." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Az Akonadi nem rendszergazdaként fut" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Az Akonadi nem rendszergazda felhasználóként fut, amely javasolt beállítás a " +"biztonságos rendszerhez." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "A tesztjelentés elmentése" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Hiba" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Nem sikerült megnyitni ezt a fájlt: \"%1\"" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Hiba történt az Akonadi szolgáltatás indulásakor. Az alábbi öntesztek " +"segíthetnek kideríteni ahiba okát. Kérjük, hogy hibabejelentés készítésekor " +"feltétlenül mellékelje az öntesztek eredményét." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Részletek" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

További hibaelhárítási tanácsok találhatók itt: userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Ú&j mappa…" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Új" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Mappa &törlése" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Törlés" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "Mappa &szinkronizálása" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Szinkronizálás" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Mappa &tulajdonságai" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Tulajdonságok" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Beillesztés" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Beillesztés" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "&Helyi előfizetések kezelése…" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Helyi előfizetések kezelése…" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Hozzáadás a kedvenc mappához" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Hozzáadás a kedvencekhez" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Eltávolítás a kedvenc mappából" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Eltávolítás a kedvencekből" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Kedvenc átnevezése…" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Átnevezés" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Mappa másolása ide…" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Másolás ide" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Elem másolása ide…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Elem mozgatása ide…" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mozgatás ide" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mappa áthelyezése ide…" + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Elem &kivágása" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Kivágás" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Mappa &kivágása" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Erőforrás létrehozása" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Erőforrás törlése" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "E&rőforrás-tulajdonságok" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Erőforrás szinkronizálása" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Kapcsolat nélküli munka" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Mappa &szinkronizálása rekurzívan" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Szinkronizálás rekurzívan" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mappa áthelyezése a kukába" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mappa áthelyezése a kukába" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Elem áthelyezése a kukába" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Elem áthelyezése a kukába" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Mappa visszaállítása a kukából" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Mappa visszaállítása a kukából" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Elem visszaállítása a kukából" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Elem visszaállítása a kukából" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Gyűjtemény visszaállítása a kukából" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Gyűjtemény visszaállítása a kukából" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Kedvenc mappák szinkronizálása" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Kedvenc mappák szinkronizálása" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Mappafa szinkronizálása" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Mappa má&solása" +msgstr[1] "%1 mappa má&solása" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Elem más&olása" +msgstr[1] "%1 elem más&olása" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Elem &kivágása" +msgstr[1] "%1 elem &kivágása" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Mappa &kivágása" +msgstr[1] "%1 mappa &kivágása" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Elem &törlése" +msgstr[1] "%1 elem &törlése" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Mappa &törlése" +msgstr[1] "%1 mappa &törlése" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Mappa &szinkronizálása" +msgstr[1] "%1 mappa &szinkronizálása" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Erőforrás törlése" +msgstr[1] "%1 &erőforrás törlése" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Erőforrás &szinkronizálása" +msgstr[1] "%1 erőforrás &szinkronizálása" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Mappa másolása" +msgstr[1] "%1 mappa másolása" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Elem másolása" +msgstr[1] "%1 elem másolása" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Elem kivágása" +msgstr[1] "%1 elem kivágása" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Mappa kivágása" +msgstr[1] "%1 mappa kivágása" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Elem törlése" +msgstr[1] "%1 elem törlése" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Mappa törlése" +msgstr[1] "%1 mappa törlése" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Mappa szinkronizálása" +msgstr[1] "%1 mappa szinkronizálása" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Erőforrás törlése" +msgstr[1] "%1 erőforrás törlése" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Erőforrás szinkronizálása" +msgstr[1] "%1 erőforrás szinkronizálása" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Név" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Biztos, hogy törölni akarja ezt a mappát és az almappáját?" +msgstr[1] "" +"Biztos, hogy törölni akarja a(z) %1 mappát és az összes almappáikat?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Mappa törlése?" +msgstr[1] "Mappák törlése?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Nem sikerült törölni ezt a mappát: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Nem sikerült törölni a mappát" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "A(z) %1 mappa tulajdonságai" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Biztosan törölni szeretné a kijelölt elemet?" +msgstr[1] "Biztosan törölni szeretne %1 elemet?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Elem törlése?" +msgstr[1] "Elemek törlése?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Nem törölhető az elem: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Az elem törlése nem sikerült" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Kedvenc átnevezése" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Név:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Új erőforrás" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Nem hozható létre az erőforrás: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Az erőforrás létrehozása nem sikerült" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Biztosan törölni szeretné ezt az erőforrást?" +msgstr[1] "Biztosan törölni szeretne %1 erőforrást?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Erőforrás törlése?" +msgstr[1] "Erőforrások törlése?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Nem sikerült adatokat beilleszteni: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Sikertelen beillesztés" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Nem lehet „/” karakter a mappa nevében." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Hiba az új mappa létrehozása során" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Nem lehet „.” karakter a mappa nevének elején vagy végén." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"A(z) „%1” mappa szinkronizálása előtt szükséges, hogy az erőforrás elérhető " +"legyen. Szeretné elérhetővé tenni?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "A(z) „%1” fiók offline" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Legyen elérhető" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Áthelyezés ebbe a mappába" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Másolás ebbe a mappába" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Nem sikerült frissíteni a feliratkozást: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Feliratkozási hiba" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Helyi feliratkozások" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Keresés:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "C&sak feliratkozottak" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Feliratkozás" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Leiratkozás" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Nem sikerült létrehozni új címkét" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Hiba történt az új címke létrehozása közben" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Valóban el akarja távolítani a címkét: %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Címke törlése" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Címke törlése" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Alkalmazandó címkék kiválasztása." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Új címke létrehozása" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Címkék kezelése" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Címkék kiválasztása…" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Címkék kiválasztása" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Törlés" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Kattintson címkék hozzáadásához" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "…" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi->XML átalakító" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Átalakít egy Akonadi gyűjtemény részfát XML fájllá." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© Volker Krause, 2009. " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nincs betöltött adat." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nincs megadva fájlnév" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Nem lehet megnyitni a(z) „%1” adatfájlt." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Nem létezik ez a fájl: %1." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Nem lehet feldolgozni a(z) „%1” adatfájlt." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "A sémadefiníciót nem sikerült betölteni és feldolgozni." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Nem lehet létrehozni a sémafeldolgozó kontextust." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Nem lehet létrehozni a sémát." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Nem lehet létrehozni a sémavalidációs kontextust." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Érvénytelen fájlformátum." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Nem lehet feldolgozni a(z) adatfájlt: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Nem található gyűjtemény: %1" + +#~ msgid "Id" +#~ msgstr "Azonosító" + +#~ msgid "Remote Id" +#~ msgstr "Távoli azonosító" + +#~ msgid "MimeType" +#~ msgstr "MIME-típus" + +#~ msgid "Default Name" +#~ msgstr "Alapértelmezett név" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Törlés" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Mégse" + +#~ msgid "Take left one" +#~ msgstr "Vegye a bal oldalit" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Két frissítés ütközik egymással.Válassza ki, melyik frissítés legyen " +#~ "alkalmazva." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Olvasatlan" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Összesen" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Méret" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-erőforrás" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Név" + +#~ msgid "Invalid collection specified" +#~ msgstr "Érvénytelen gyűjtemény lett megadva" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "" +#~ "Nem található ez a protokollverzió: %1. Legalább %2 verzió szükséges." + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "A kiszolgáló protokollverziója elég új." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "A kiszolgáló protokollverziója: %1. Ez azonos vagy újabb a szükséges " +#~ "verziónál: %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Inkonzisztens helyi gyűjteményfa található." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "A távoli gyűjtemény gyökér-végződésű őslánc nélkül van biztosítva, az " +#~ "erőforrás törött." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE tesztprogram" + +#~ msgid "Cannot list root collection." +#~ msgstr "Nem listázható a gyökér gyűjtemény." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "A Nepomuk keresőszolgáltatás regisztrálva van a D-Bus-ban." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "A Nepomuk keresőszolgáltatás regisztrálva van a D-Bus-ban, amely " +#~ "általában azt jelenti, hogy működik." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "A Nepomuk keresőszolgáltatás nincs regisztrálva van a D-Bus-ban." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "A Nepomuk keresőszolgáltatás nincs regisztrálva a D-Bus-ban, amely " +#~ "általában azt jelenti, hogy nem indult el vagy egy hiba miatt kilépett." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "A Nepomuk keresőszolgáltatás nem megfelelő háttérprogramot használ." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "A Nepomuk keresőszolgáltatás a(z) „%1” háttérprogramot használja, amelyet " +#~ "nem javasolt használni az Akonadival." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "A Nepomuk keresőszolgáltatás megfelelő háttérprogramot használ. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "A Nepomuk keresőszolgáltatás a javasolt háttérprogramok egyikét használja." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "A(z) „%1” bővítmény nem statikusan beépített, kérjük adja meg ezt az " +#~ "információt a hibajelentésben." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "A bővítmény nem statikusan fordított" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "Ú&j mappa..." + +#, fuzzy +#~| msgid "Folder &Properties" +#~ msgid "Resource Properties" +#~ msgstr "Mappat&ulajdonságok" + +#~ msgid "Cache" +#~ msgstr "Gyorsítótár" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "A szülő gyorsítótárkezelési módjának átvétele" + +#~ msgid "Cache Policy" +#~ msgstr "Gyorsítótárkezelés" + +#~ msgid "Interval check time:" +#~ msgstr "Ellenőrzési időköz:" + +#~ msgid "Local cache timeout:" +#~ msgstr "A helyi gyorsítótár várakozási ideje:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Szinkronizálás csak kérésre" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "A mappanézet beállítása" + +#, fuzzy +#~| msgctxt "search folder" +#~| msgid "Search:" +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Keresés:" + +#~ msgid "Available Folders" +#~ msgstr "Elérhető mappák" + +#~ msgid "Current Changes" +#~ msgstr "Változások" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Az előfizetés megszüntetése a kijelölt mappán" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Az Akonadi szolgáltatás nem jelzett hibát induláskor itt: %1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "Az Akonadi-vezérlő nem jelzett hibát induláskor itt: %1." + +#~ msgid "TODO" +#~ msgstr "Feladat" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Az Akonadi nem indítható el.
Részletek...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-erőforrás" diff --git a/po/ia/akonadi_knut_resource.po b/po/ia/akonadi_knut_resource.po new file mode 100644 index 0000000..41e3cda --- /dev/null +++ b/po/ia/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# g.sora , 2011. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2011-07-19 16:33+0200\n" +"Last-Translator: g.sora \n" +"Language-Team: Interlingua \n" +"Language: ia\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nulle file de datos selectionate." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "File '%1' cargate con successo." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Selectiona file de datos" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "File de datos de Knut de Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nulle elemento trovate per id remote %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Collection genitor non trovava se in arbore DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Incapace de scriber collection." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Il non trovava collection modificate in arbore DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Il non trovava collection delite in arbore DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Il non trovava collection parente '%1' in arbore DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Incapace de scriber elemento" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Il non trovava elemento modificate in arbore DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Il non trovava elemento delite in arbore DOM." diff --git a/po/ia/libakonadi5.po b/po/ia/libakonadi5.po new file mode 100644 index 0000000..3508a4f --- /dev/null +++ b/po/ia/libakonadi5.po @@ -0,0 +1,2739 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# g.sora , 2011, 2012, 2013, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-21 23:00+0100\n" +"Last-Translator: Giovanni Sora \n" +"Language-Team: Interlingua \n" +"Language: ia\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 2.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Giovanni Sora" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "g.sora@tiscali.it" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Currentemente il non ha alcun conto configurate." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integration de contos nonn es supportate" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Incapace de registrar objecto a dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 de typo %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificator de agente" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agente Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Preste" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Foras de linea" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synchronisante ..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Error." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Non configurate" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificator de ressource" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Ressource de Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Il recuperava un elemento invalide" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Error quando on creava elemento: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Il falleva actualisar collection : %1." + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Il falleva actualisar collection local: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Il falleva actualisar elementos local: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Non pote recercar elemento quando de modo foras de linea." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synchronisante dossier ' %1' " + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Il falleva rcuperar le collection per syncronisation." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Il falleva rcuperar le collection per syncronisation de attribute." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Le elemento requirite non existe plus" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Carga cancellate." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Necun tal collection." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Il trovava collectiones orphano non resolvite" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Il non trovava un altere elemento pro gestion de conflicto" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Incapace de acceder a interfacie de DBUD de agente create." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Creation de instantia de agente expirava." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Incapace de obtener typo de agente '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Incapace de crear instantia de agente" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Invalide instantia de collection" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Invalide instantia de ressource" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Il falleva a obtener interfacie D-Bus pro ressource '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Synchronisation de attributos de collection expirava." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Collection invalide de copiar" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Collection de destination invalide" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Genitor invalide" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Il falleva analysar le collection ex ressource." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Collection invalide" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Invalide collection date," + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Il non specificava alcun objecto de mover" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Non specificava ulle destination valide" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Collection invalide." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Collection genitor invalide" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Non pote connecter al servicio Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Le version de protocollo del servicio de Akonadi es incompatibile. Tu " +"assecura te que tu ha version compatibile installate." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operation cancellate per le usator." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Error incognite." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Responsa inexpectte" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Incapace a crear relation." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Synchronisation de ressource expirava." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Il non poteva trovar le collection radice de ressource %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Il non dava alcun ID de ressource." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Invalide identificator de ressource '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Il falleva configurar ressource predefinite via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Il falleva trovar le collection de ressource." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Expiration durante que il tentava obtener bloco." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Incapace de crear etiquetta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Il falleva mover collection a corbe, il aborta operation de inviar a corbe" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Il passava un elemento invalide" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Il passava un collection invalide" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Nulle valide collection o lista de elementos vacue" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Il non poteva trovar collection de restabilir e ressource de restabilir non " +"es disponibile" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nomine" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Cargante..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Error" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Le collection objectivo '%1' ja contine\n" +"un collection de nomine ' %2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nomine" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Non pote copiar elemento: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Non pote copiar collection: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Non pote mover elemento: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Non pote mover collection: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Non pote connecter entitate: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Error" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Dossieres favorite" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Messages Total" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Messages non legite" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Grandor de immagazinage" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Grandor de immagazinage de subdossier" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Non Legite" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Grandor" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiquetta" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Incapace de recercar elemento per indice" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indice non es plus disponibile" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Parte '%1' de carga non es disponibile pro iste indice" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Necun session disponibile pro iste indice" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Necun elemento disponibile pro iste indice" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Plugin sin nomine" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Nulle description disponibile" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Le protocollo de servitor de Akonadi differeab le version de protocollo " +"usate per iste application.\n" +"Si tu recentemente actualisava tu systema pro favor exi e face secure omne " +"le applicationes que usa le version de protocollo correcte." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Il non ha alun Agentes de Akonadi disponibile. Pro favor tu verifica tu " +"installation de KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Version de protocollo in disaccordo. Version de servitor es plus vetere (%1) " +"del nostre (%2). Si tu actualisava recentemente tu systema pro favor " +"restarta le servitor Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Version de protocollo in disaccordo. Vesion de servitor es plus nove ()%1 " +"del nostre (%2). Si tu actualisava recentemente tu systema pro favor " +"restarta le servitor Akonadi." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Auto-test de Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Verifica e report le stato del servitor Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nove Instantia de agente" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Dele Instantia de agente" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configura instantia de agente" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nove Instantia de agente" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Il non pote crear instantia de agente: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Falleva in creation de instantia de agente" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Dele instantia de agente?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Tu vermente vole deler le instantia de agente selectionate?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 Configuration" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 Manual" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "A proposito de %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Le dialogo de configuratin ha essite aperite in un altere fenestra" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Configuration per %1 es ja aperite alibi." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Il falleva a registrar dialogo de configuration %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuta" +msgstr[1] "minutas" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Recuperar" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usa optiones ex dossier genitor o conto" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchronisa quando on selige iste dossier" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synchronisa automaticamente post:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Jammais" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutas" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Partes ponite in cache localmente" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Optiones de recuperar" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Sempre recupera &messages integre" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Recupera corpores de message sur demanda" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Mantene corpores de message localmente per: " + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Per sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Cerca" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usa dossier de modo predefinite" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nove sub-dossier..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Crea un nove sub-dossier sub le dossier currentemente seligite" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nove dossier" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nomine" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Il non pote crear dossier: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Falleva in creation de dossier" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "General" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un objecto" +msgstr[1] "%1 objectos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nomine:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usa icone personalisate:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "Dossier" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistica" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contento:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objectos" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Dimension:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Memora que indicisar pote prender alcun minutas." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Mantenimento" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Error quando on recuperava computo deelemento indicisate" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indicisate %1 elemento in iste dossier" +msgstr[1] "Indicisate %1 elementos in iste dossier" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calculante elementos indicisate..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Files" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Typo de Dossier:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "incognite" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementos" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Elementos Total:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Elementos non legite" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indicisante" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Habilita indicisation de texto plen" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Recuperante computo de elementos indicisate ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Dossier de reindicisar " + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Nulle dossier" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Aperi dialogo de collection" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Selige un collection" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Move ci" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copia ci" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancella" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Tempore de modification" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Bandieras (Flags)" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attributo: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolution de conflicto" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Prende mi version" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Prende lor version" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mantene ambes versiones" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Tu modificationes conflige con los facite per alcun altere in le mesme " +"tempore.
A minus que un version pote solmente esser jectate, tu debera " +"integrar ille modificationes manualmente.
Pulsa sur \"Aperi editor de texto\" per mantener un copia del " +"texto, alora selige qual version es plus correcte, alora re-aperi lo e " +"modifica lo de novo per adder lo que es mancante." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Datos" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Initiante servitor de Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stoppante servitor de Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Move ci" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copia ci" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Conca&tena ci" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancella " + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Non pote connecter al servicio de gestion de infomation Personal.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Servicio de gestion de information personal es initiante..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Servicio de gestion de information personal es claudente..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Servicio de gestion de information personal es executante un actualisation " +"de base de datos." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Le servicio de gestion de information personal es executante un " +"actualisation de base de datos. Isto occurre post un actualisation de " +"software e il es necessari pro optimizar le execution.\n" +"In dependentia del amontar del information personal, isto poterea durar " +"alcun minutas. " + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Le servicio de gestion de information personal de Akonadi non es executante. " +"Iste application non pote esser usate sin illo." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Initia" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Le schema de gestion de information personal de Akonadi non es " +"functionante.\n" +"Pulsa sur \"Detalios...\" pro obtener information detaliate re iste problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Le servicio de gestion de information personal de Akonadi non es executante. " + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalios..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Tu vole remover le conto ' %1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Remove conto?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Contos in reception (adde al minus un):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&dde..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modifica..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "R&emove" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Re-Initia" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Dossier recente" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Renomina favorito" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nomine:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Auto-test de Servitor Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Salveguarda reporto..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copia reporta a area de transferentia" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Le driver QtSQL '%1' es requirite per tu configuration currente de servitor " +"Akonadi e illo esseva trovate sur tu systema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Le driver QtSQL '%1' es requirite per tu configuration currente de servitor " +"Akonadi\n" +"Le sequente drivers es installate: %2.\n" +"Tu assecura te que le driver requirite es installate." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Driver da Base de Datos trovate." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Driver da Base de Datos non trovate." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Executabile de servitor MySQL non verificate." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Le configuration currente non require un servitor interne MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Tu ha currentemente configurate Akonadi per usar le servitor MySQL '%1'.\n" +"Tu assecura te que tu ha le servitor MySQL installate, fixate le percurso " +"correcte e que tu ha le necessari derectos de lectura e execution sur le " +"executabile del servitor. Le executabile del servitor typicamente es " +"appellate 'mysqld', su location varia dependentemente del distribution. " + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Il non trovava servitor MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Servitor MySQL non legibile." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Servitor MySQL non executabile." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Il trovava MySQL con un nomine impreviste" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Il trovava servitor MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Il trovava servitor MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Servitor MySQL es executabile." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Il falleva executar le servitor MySQL '%1' con le sequente message de " +"error: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Il falleva executar le servitor MySQL." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Registro de error de servitor MySQL non verificate." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Necun registro de error de MySQL currente trovate." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Le servitor MySQL non reportava alcun error durante iste initio. Le registro " +"potere esser trovate in ' %1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Registro de error de MySQL non legibile." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Unn file de registro de error de servitor MySQL esseva trovate ma il non es " +"legibile: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Registro de servitor MySQL contine errores." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Le file de registro de servitor MySQL '%1' contine errores." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Registro de servitor MySQL contine avisos." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Le file de registro de servitor MySQL '%1' contine avisos." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Registro de servitor MySQL non contine errores." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Le file de registro de servitor MySQL '%1' non contine alcun errores o " +"avisos." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Configuration de servitor non essayate." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Il trovava configuration de servitor predefinite." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Le configuration predefinite pro le servitor MySQL esseva trovate e il es " +"legibile in %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Il non trovava configuration de servitor MySQL predefinite." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Il non trovava o il no esseva legibile le configuration predefinite per le " +"servitor MySQL. Verifica que tu installation de Akonadi es complete e que tu " +"ha tote le derectos de accesso requirite." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Configuration de servitor MySQL personalisate non disponibile." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Le configuration personalisate pro le servitor MySQL non esseva trovate, ma " +"illo es optional. " + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Il trovava configuration de servitor personalisate." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Le configuration personalisate pro le servitor MySQL esseva trovate e il es " +"legibile in %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Configuration de servitor MySQL personalisate non legibile." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Le configuration personalisate pro le servitor MySQL esseva trovate in %1 ma " +"illo non es legibile. Verifica tu derectos de accesso." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Configuration de servitor MySQL non trovate o non legibile." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"Configuration de servitor MySQL esseva non trovate o il non es legibile." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Configuration de servitor MySQL es usabile." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Configuration de servitor MySQL esseva trovate in %1 e es legibile." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Il non pote connecter a servitor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Il trovava servitor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Il trovava servitor PostgreSQL e le connexion ex functionante." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl non trovate" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Le programma 'akonadictl' necessita esser accessibile in $PATH. Tu assecura " +"te que tu ha le servitor Akonadi installate." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl trovate e usabile" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Le programma '%1' pro controlar le servitor Akonadi esseva trovate e poteva " +"esser executate con successo.\n" +"Exito:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl trovate ma non usabile" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Le programma '%1' pro controlar le servitor Akonadi esseva trovate ma non " +"poteva esser executate con successo.\n" +"Exito:\n" +"%2\n" +"Tu assecura te que le servitor Akonadi es installate correctemente.." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Processo de controlo de Akonadi registrava se a D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Le processo de controlo de Akonadi es registrate a D-Bus.Isto typicamente " +"indica que il es functionante." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Processo de controlo de Akonadi non registrava se a D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Le processo de controlo de Akonadi non es registrate a D-Bus.Isto " +"typicamente indica que il non initiava o que il incontrava un error fatal " +"durante le initio." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Processo de servitor Akonadi registrava se a D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Le processo de servitor Akonadi es registrate a D-Bus. Isto typicamente " +"indica que il es functionante." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Processo de servitor Akonadi non registrava se a D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Le processo de servitor Akonadi non es registrate a D-Bus.Isto typicamente " +"indica que il non initiava o que il incontrava un error fatal durante le " +"initio." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Verifica de version de protocollo non possibile." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Sin un connexion al servitor, il non es possibile verificar si le version de " +"protocollo satisface le requirimentos." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Version de protocollo de servitor es troppo vetule." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Le version de protocollo es %1, ma version version %2 es requirite per le " +"cliente. Si tu ha recentemente actualisate KDE PIM, pro favor assecura te de " +"restartar sia Akonadi que le applicationes de KDE PIMi." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Version de protocollo de servitor es troppo nove." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Version de protocollo de servitor coincide." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Version de protocollo de servitor currente es %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Il trovava agentes de ressource." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Il trovava al minus un agente de ressource." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Il non trovava alcun agente de ressource." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nulle agentes de ressource ha essite trovate. Akonadi non es usabile sin al " +"minus un de illos. Isto solitemente significa que nulle agentes de ressource " +"es installate o que il ha un problema de configuration. Le sequente " +"percursos ha essite cercate: '%1'. Le variabile de ambiente XDG_DATA_DIRS es " +"fixate a '%2'; tu assecura te que isto include omne percursos ubi agentes de " +"Akonadi es installate." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Il non trovava alcun registro de error de servitor Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Le servitor Akonadi non reportava alcun error durante su initio currente." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Il trovava registro de error currente de servitor Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Le servitor Akonadi reportava errores durante su initio currente. Le " +"registro pote esser trovate in %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Il non trovava alcun previe registro de error de servitor Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Le servitor Akonadi non reportava alcun error durante su initio previe." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Il trovava previe registro de error de servitor Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Le servitor Akonadi reportava errores durante su initio previe. Le registro " +"pote esser trovate in %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Il non trovava alcun registro de error currente de servitor Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Le processo de controlo de Akonadi non reportava alcun error durante su " +"initio currente." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Il trovava registro de error currente de controlo de Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Le processo de controlo de Akonadi reportava errores durante su initio " +"currente. Le registro pote esser trovate in %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Il non trovava alcun previe registro de error de controlo de Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Le processo de controlo de Akonadi non reportava alcun error durante su " +"initio previe." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Il trovava previe registro de error de controlo de Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Le processo de controlo de Akonadi reportava errores durante su initio " +"previe. Le registro pote esser trovate in %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi esseva initiate como super-usator (radice)" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Executar applicationes que confronta se con internet como root(super-usator)/" +"administrator expone te a multe riscos de securitate. MySQL, usate per iste " +"installation de Akonadi, non permittera a se mesme de executar como root, " +"pro proteger te ab iste riscos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi non es executante como super-usator" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi non es executante como usator root/administrator, lo que es le " +"configuration recommendate pro un systema secur." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Salveguarda reporto de essayar" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Error" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Non pote aperir le file '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Un error occurreva durante le initio del servitor Akonadi. Le sequente auto-" +"tests es supponite de adjutar teper traciar e solver iste problema. Quando " +"on require un supporto o reporta un bug, pro favor sempre tu include iste " +"reporto." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalios" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Pro altere consilios de investigation de problemas pro favor tu refere a " +"userbase.kde.org/Akonadi." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nove dossier..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nove " + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Dele dossier" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Dele" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Synchronisa dossier" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchronisa " + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Proprietates de dossier" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Proprietates" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Colla" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Colla" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gere &Subscriptiones local..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gere subscriptiones local" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Adde dossieres de favorite" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Adde a favoritos" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Remove ex dossieres favorite" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Remove ex favoritos" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Renomina favorito..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Renomina" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copia dossier a..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copia a" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copia elemento in..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Move elemento a..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Move a" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Move dossier a..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Talia elemento" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Talia" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Talia dossier" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Crea ressource" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Dele ressource" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Proprietates de &Ressource" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synchronisa Ressource" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Travalia Foras de linea" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synchronisa dossier recursivemente" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synchronisa recursivemente" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Move dossier a corbe" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Move dossier a corbe" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Move elemento a corbe" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Move elemento a corbe" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restabili dossier ex corbe" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restabili dossier ex corbe" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restabili elemento ex corbe" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restabili elemento ex corbe" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restabili collection ex corbe" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restabili collection ex corbe" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synchronisa dossieres favorite" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synchronisa dossieres favorite" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synchronisa Arbore de Dossier" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copia dossier" +msgstr[1] "&Copia %1 dossieres" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copia elemento in" +msgstr[1] "&Copia %1 elementos" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Talia elemento" +msgstr[1] "&Talia %1 elementos" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Talia dossier" +msgstr[1] "&Talia %1 dossieres" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Dele elemento" +msgstr[1] "&Dele %1 elementos" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Dele dossier" +msgstr[1] "&Dele %1 dossieres" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synchronisa dossier" +msgstr[1] "&Synchronisa %1 dossieres" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Dele ressource" +msgstr[1] "&Dele %1 ressources" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synchronisa Ressource" +msgstr[1] "&Synchronisa %1 ressources" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copia dossier" +msgstr[1] "Copia %1 dossieres" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copia elemento" +msgstr[1] "Copia %1 elementos" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Talia elemento" +msgstr[1] "Talia %1 elementos" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Talia dossier" +msgstr[1] "Talia %1 dossieres" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Dele elemento" +msgstr[1] "Dele %1 elementos" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Dele dossier" +msgstr[1] "Dele %1 dossieres" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synchronisa dossier" +msgstr[1] "Synchronisa %1 dossieres" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Dele ressource" +msgstr[1] "Dele %1 ressource" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synchronisa Ressource" +msgstr[1] "Synchronisa %1 ressources" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nomine" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Tu vermente vole deler iste dossier e tote su sub-dossieres?" +msgstr[1] "Tu vermente vole deler iste %1 dossieres e tote lor sub-dossieres?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Dele dossier?" +msgstr[1] "Dele dossieres?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Il non pote deler dossier: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Il falleva deler dossier" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Proprietate del dossier %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Tu vermente vole deler le elemento selectionate?" +msgstr[1] "Tu realmente tu vole deler %1 elementos?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Dele elemento?" +msgstr[1] "Dele elementos?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Il non pote deler elemento: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Il falleva deler elemento" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Renomina favorito" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nomine:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nove Ressource" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Non pote crear ressource: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Il falleva crear ressource" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Tu vermente vole deler iste ressource?" +msgstr[1] "Tu vermente vole deler %1 ressources?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Dele ressource?" +msgstr[1] "Dele ressources?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Il non pote collar datos: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Il falleva collar" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Nos non pote adder \"/\" al nomine de dossier." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Crea Error de Nove Dossier" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Nos non pote adder \".\" al initio o fin de nomine de dossier." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Ante synchronisar dossier \"%1\" il es necessari haber le ressource in " +"linea. Tu vole poner lo in linea?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Conto\"%1\" es foras de linea" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Vade in linea" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Move a iste dossier" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copia a iste dossier" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Il falleva actualisar subscription: %1 " + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Error de Subscription" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Subscriptiones local" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Cerca:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Solmente subscribite" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Su&bscribe" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "De-s&ubscribe" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Incapace de crear un nove etiquetta" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Error quando on creava un nove etiquetta" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Tu vermente vole remover le etiquetta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Dele etiquetta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Dele etiquetta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Configura qual etiquettas deberea esser applicate." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Crea nove etiquetta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gere etiquettas" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Selectiona etiquettas..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Selectiona etiquettas" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Netta" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Pulsa per adder etiquettas" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Convertitor ex Akonadi a XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Il converte un subarbore de collection Akonadi a un file XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Necun datos cargate." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Necune nomine de file specificate" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Incapace a aperir le file de datos '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "File %1 non existe." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Incapace a analysar file de datos '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Definition de schema non poteva esser cargate e analysate." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Incapace a crear un contexto de analysator de schema." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Incapace a crear schema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Incapace de crear un contexto de validation de schema." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Formato de file invalide." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Incapace a analysar file de datos: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Incapace a trovar collection %1" + +#~ msgid "Id" +#~ msgstr "id" + +#~ msgid "Remote Id" +#~ msgstr "Id remote" + +#~ msgid "MimeType" +#~ msgstr "Typo Mime" + +#~ msgid "Default Name" +#~ msgstr "Nomine predefinite" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Dele" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Cancella" + +#~ msgid "Take left one" +#~ msgstr "Prende illo a sinistre" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Duo actualisationes conflige le unes con le alteres.Per favor selige " +#~ "qual actualisation(es) applicar." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Non Legite" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Total" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Dimension" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Ressource de Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nomine" + +#~ msgid "Invalid collection specified" +#~ msgstr "Invalide collection specificate" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Il trovava version de protocollo %1, on expectava al minus %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Version de protocollo de servitor es assatis recente." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Le version de protocollo de servitor es %1, que equala o es plus nove " +#~ "del version requirite %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Relevate arbore de collection local inconsistente" + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Collection remote sin que le catena de ancestor terminate per radice es " +#~ "fornite, ressource es imperfecte." + +#~ msgid "KDE Test Program" +#~ msgstr "Programma de essayo de KDE" + +#~ msgid "Cannot list root collection." +#~ msgstr "Non pote listar collection radice." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Servicio de cerca Nepomuk registrava se a D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Le servicio de cerca Nepomuk es registrate a D-Bus. Isto typicamente " +#~ "indica que il es functionante." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Servicio de cerca Nepomuk non registrava se a D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Le servicio de cerca Nepomuk non es registrate a D-Bus.Isto typicamente " +#~ "indica que il non initiava o que il incontrava un error fatal durante le " +#~ "initio." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "" +#~ "Servicio de cerca Nepomuk usa un retro-administration inappropriate." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Le servicio de cerca Nepomuk usa le retro-administration '%1', que non es " +#~ "recommendate de usar con Akonadi." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Servicio de cerca Nepomuk usa un retro-administration appropriate. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Le servicio de cerca Nepomuk usa un del retro-administrationes " +#~ "recommendate." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Plugin \"%1\" non es construite internemente como static, pro favor " +#~ "specifica iste information in le reporto de bug." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Plugin non construite de modo static" + +#~ msgid "Fetch Job Error" +#~ msgstr "Error de carga de recerca (fetch)" diff --git a/po/it/akonadi_knut_resource.po b/po/it/akonadi_knut_resource.po new file mode 100644 index 0000000..1abc9f7 --- /dev/null +++ b/po/it/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# translation of akonadi_knut_resource.po to Italian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Luigi Toscano , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-07-21 23:36+0200\n" +"Last-Translator: Luigi Toscano \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nessun file di dati selezionato." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "File «%1» caricato correttamente." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Selezione file di dati" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "File di dati Knut di Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nessun elemento trovato avente remoteid %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Collezione genitore non trovata nell'albero DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Impossibile scrivere la collezione." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Collezione modificata non trovata nell'albero DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Collezione eliminata non trovata nell'albero DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Collezione genitore «%1» non trovata nell'albero DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Impossibile scrivere l'elemento." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Elemento modificato non trovato nell'albero DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Elemento eliminato non trovato nell'albero DOM." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Percorso del file di dati Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Non modificare i dati attuali del motore." diff --git a/po/it/libakonadi5.po b/po/it/libakonadi5.po new file mode 100644 index 0000000..c66905f --- /dev/null +++ b/po/it/libakonadi5.po @@ -0,0 +1,2823 @@ +# translation of libakonadi.po to Italian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the libakonadi package. +# Leonardo Finetti , 2007. +# Dario Panico , 2008. +# Luigi Toscano , 2009, 2011, 2012, 2013, 2014, 2015. +# Vincenzo Reale , 2015, 2017, 2018, 2019, 2020, 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-07-26 11:09+0200\n" +"Last-Translator: Vincenzo Reale \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 21.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Vincenzo Reale,Luigi Toscano" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "smart2128vr@gmail.com," + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Attualmente non ci sono account configurati." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "L'integrazione degli account non è supportata" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Impossibile registrare l'oggetto in DBUS: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 di tipo %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificatore agente" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agente Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pronto" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Non in linea" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sincronizzazione..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Errore." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Non configurato" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificatore di risorsa" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Risorsa di Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Recuperato elemento non valido" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Errore durante la creazione dell'elemento: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Errore durante l'aggiornamento della collezione: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Aggiornamento della collezione locale non riuscito: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Aggiornamento degli elementi locali non riuscito: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Impossibile caricare l'elemento durante la modalità non in linea." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sincronizzazione cartella «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Recupero della collezione non riuscito per la sincronizzazione." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Recupero della collezione non riuscito per la sincronizzazione degli " +"attributi." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "L'elemento richiesto non esiste più" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Operazione annullata." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nessuna collezione." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Trovate delle collezioni orfane non risolte" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Non è stato trovato un altro elemento per la gestione del conflitto" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Impossibile accedere all'interfaccia D-Bus dell'agente creato." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "La creazione di un'istanza dell'agente è scaduta." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Impossibile ricavare il tipo di agente «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Impossibile creare un'istanza dell'agente." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Istanza non valida di collezione." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Istanza non valida della risorsa." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Impossibile ottenere l'interfaccia D-Bus per la risorsa «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" +"Tempo scaduto per la sincronizzazione degli attributi della collezione." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Collezione non valida da copiare" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Collezione di destinazione non valida" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Genitore non valido" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Analisi della collezione dalla risposta non riuscito" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Collezione non valida" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "È stata fornita una collezione non valida." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nessun elemento da spostare specificato" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nessun destinazione valida specificata" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Collezione non valida." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Collezione genitrice non valida" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Impossibile connettersi al server Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"La versione del protocollo del server Akonadi è incompatibile. Assicurati di " +"avere una versione compatibile installata." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operazione annullata dall'utente." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Errore sconosciuto." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Risposta imprevista" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Impossibile creare una relazione." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tempo scaduto per la sincronizzazione della risorsa." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Impossibile recuperare la collezione radice di risorse %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Nessun identificativo di risorsa fornito." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identificativo di risorsa «%1» non valido" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Configurazione della risorsa predefinita via D-Bus non riuscita." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Recupero della collezione di risorse non riuscito." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tempo scaduto nell'attesa di ottenere un lock." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Impossibile creare un'etichetta." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Cestinamento non riuscito nella collezione, interruzione dell'operazione di " +"cestinamento" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Passato elemento non valido" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Passata collezione non valida" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Collezione non valida o lista di elementi vuota" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Impossibile trovare la collezione di ripristino e la risorsa di ripristino " +"non è disponibile" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Caricamento..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Errore" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"La collezione di destinazione «%1» contiene già\n" +"una collezione avente nome «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Impossibile copiare l'elemento: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Impossibile copiare la collezione: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Impossibile spostare l'elemento: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Impossibile spostare la collezione: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Impossibile collegare l'entità: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Errore" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Cartelle preferite" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Messaggi totali" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Messaggi non letti" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Spazio impiegato" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Spazio impiegato dalla sottocartella" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Non letti" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Totale" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Dimensione" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etichetta" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Impossibile recuperare l'elemento legato all'indice" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "L'indice non è più disponibile" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "La parte di contenuto «%1» non è disponibile per questo indice" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Nessuna sessione disponibile per questo indice" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Nessun elemento disponibile per questo indice" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Estensione senza nome" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Nessuna descrizione disponibile" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"La versione del protocollo del server Akonadi differisce dalla versione del " +"protocollo utilizzata da questa applicazione.\n" +"Se hai recentemente aggiornato il tuo sistema disconnettiti e accedi " +"nuovamente per assicurarti che tutte le applicazioni utilizzino la versione " +"corretta del protocollo." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Ci sono agenti Akonadi disponibili. Verifica l'installazione di KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Mancata corrispondenza della versione del protocollo. La versione del server " +"è più datata (%1) della nostra (%2). Se hai aggiornato il tuo sistema di " +"recente, riavvia il server Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Mancata corrispondenza della versione del protocollo. La versione del server " +"è più recente (%1) della nostra (%2). Se hai aggiornato il tuo sistema di " +"recente, riavvia tutte le applicazioni KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Autodiagnostica di Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Verifica e riferisce lo stato del server Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nuova istanza dell'agente..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Elimina istanza dell'agente" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configura istanza dell'agente" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nuova istanza dell'agente" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Impossibile creare un'istanza dell'agente: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "La creazione di un'istanza dell'agente non è riuscita" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Elimina l'istanza dell'agente?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Vuoi davvero eliminare l'istanza selezionata dell'agente?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configurazione di %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manuale di %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Informazioni su %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "La finestra di configurazione è stata aperta in un'altra finestra" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "La configurazione per %1 è già aperta altrove." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Impossibile la finestra di configurazione di %1" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuto" +msgstr[1] "minuti" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Recupero" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usa le opzioni della cartella o dell'account di livello superiore" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronizza quando questa cartella viene selezionata" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronizza automaticamente dopo:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Mai" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuti" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Parti memorizzate localmente in cache" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opzioni di recupero" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Recupera se&mpre i messaggi completi" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "&Recupera a richiesta il corpo dei messaggi" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Mantieni localmente il corpo dei messaggi per:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Cerca" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usa cartella in modo predefinito" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nuova sottocartella..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Crea una nuova sotto-cartella nella cartella selezionata" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nuova cartella" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nome" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Impossibile creare la cartella: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Creazione cartella non riuscita" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Generale" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un oggetto" +msgstr[1] "%1 oggetti" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nome:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usa icona personalizzata:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "cartella" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistiche" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Contenuto:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 oggetti" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Dimensione:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Ricorda che l'indicizzazione può richiedere alcuni minuti." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Manutenzione" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Errore durante il recupero del numero di elementi indicizzati" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indicizzato %1 elemento in questa cartella" +msgstr[1] "Indicizzati %1 elementi in questa cartella" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calcolo degli elementi indicizzati..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "File" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipo cartella:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "sconosciuto" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementi" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Totale elementi:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Elementi non letti:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indicizzazione" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Abilita l'indicizzazione full-text" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Recupero del numero di elementi indicizzati..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indicizza nuovamente la cartella" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Nessuna cartella" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Apri la finestra di dialogo delle collezioni" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Seleziona una collezione" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Sposta qui" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copia qui" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Annulla" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Ora di modifica" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Contrassegni" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attributo: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Risoluzione del conflitto" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Usa la mia versione" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Usa la loro versione" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Mantieni entrambi le versioni" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Le tue modifiche sono in conflitto con quelle generate da qualcun altro." +"
A meno che una versione non possa essere eliminata, dovrai integrare " +"tali modifiche manualmente.
Fai clic su \"Apri " +"editor di testo\" per mantenere una copia dei testi, poi seleziona quale " +"versione è la più corretta, quindi aprila nuovamente e modificala per " +"aggiungere cosa manca." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dati" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Avvio del server Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Interruzione del server Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Sposta qui" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copia qui" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Co&llega qui" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Annulla" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Impossibile connettersi al servizio di gestione delle informazioni " +"personali.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" +"Avvio del servizio per la gestione delle informazioni personali in corso..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" +"Chiusura del servizio per la gestione delle informazioni personali in " +"corso..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Il servizio per la gestione delle informazioni personali sta eseguendo un " +"aggiornamento della banca dati." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Il servizio per la gestione delle informazioni personali sta eseguendo un " +"aggiornamento della banca dati.\n" +"Questo succede dopo un aggiornamento del software ed è necessario per " +"ottimizzare le prestazioni.\n" +"In base al numero di informazioni personali questa operazione potrebbe " +"richiedere alcuni minuti." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Il servizio Akonadi per la gestione delle informazioni personali non è " +"operativo. Questa applicazione non può essere utilizzata senza di esso." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Avvia" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"L'infrastruttura Akonadi per la gestione delle informazioni personali non è " +"operativa.\n" +"Fai clic su \"Dettagli...\" per ottenere ulteriori informazioni su questo " +"problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Il servizio Akonadi per la gestione delle informazioni personali non è " +"operativo." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Dettagli..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Vuoi rimuovere l'account «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Vuoi rimuovere l'account?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Account in entrata (aggiungine almeno uno):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&ggiungi..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modifica..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "R&imuovi" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Riavvia" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Cartella recente" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Rinomina preferito" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nome:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Auto-diagnostica del server Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Salva resoconto..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copia resoconto negli appunti" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Il driver QtSQL «%1» è richiesto dall'attuale configurazione del server " +"Akonadi ed è stato trovato sul sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Il driver QtSQL «%1» è richiesto dall'attuale configurazione del server " +"Akonadi.\n" +"I seguenti driver sono installati: %2.\n" +"Assicurati che i driver richiesti siano installati." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Trovati driver per la banca dati." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Driver della banca dati non trovati." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Eseguibile del server MySQL non verificato." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "La configurazione attuale non richiede un server MySQL interno." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"La tua configurazione attuale di Akonadi richiede l'uso del server MySQL " +"«%1».\n" +"Controlla di aver installato il server MySQL ed impostato il percorso " +"corretto, ed assicurati di avere i necessari diritti di lettura ed " +"esecuzione sul file eseguibile del server. Quest'ultimo normalmente si " +"chiama 'mysqld' e la sua posizione varia in base alla distribuzione." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Server MySQL non trovato." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Server MySQL non leggibile." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Server MySQL non eseguibile." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Trovato MySQL con un nome non previsto." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Trovato server MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Trovato server MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Il server MySQL è eseguibile." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"L'esecuzione del server MySQL «%1» non è riuscita ed ha restituito il " +"seguente errore: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Esecuzione del server MySQL non riuscita." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Registro degli errori del server MySQL non verificato." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Registro degli errori dell'attuale server MySQL non trovato." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Il server MySQL non ha restituito alcun errore durante il suo ultimo avvio. " +"Il registro degli errori può essere consultato in «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Registro degli errori di MySQL non leggibile." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Un registro degli errori del server MySQL è stato trovato, ma non è " +"leggibile: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Il registro del server MySQL contiene alcuni errori." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Il file registro «%1» del server MySQL contiene alcuni errori." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Il registro del server MySQL contiene alcuni avvisi." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Il file registro «%1» del server MySQL contiene alcuni avvisi." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Il registro del server MySQL non contiene alcun errore." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Il file registro «%1» del server MySQL non contiene né errori né avvisi." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Configurazione del server MySQL non verificata." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Trovata configurazione predefinita del server MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"La configurazione predefinita del server MySQL è stata trovata ed è " +"leggibile in %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Configurazione predefinita del server MySQL non trovata." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"La configurazione predefinita del server MySQL non è stata trovata o non è " +"leggibile. Controlla se la tua installazione di Akonadi è completa e se " +"disponi di tutti i permessi di accesso richiesti." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Configurazione personalizzata del server MySQL non disponibile." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"La configurazione personalizzata del server MySQL non è stata trovata, ma è " +"opzionale." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Trovata configurazione personalizzata del server MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"La configurazione personalizzata del server MySQL è stata trovata in %1 ed è " +"leggibile" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Configurazione personalizzata del server MySQL non leggibile." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"La configurazione personalizzata del server MySQL è stata trovata in %1 ma " +"non è leggibile. Controlla i tuoi permessi di accesso." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Configurazione del server MySQL non trovata o non leggibile." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"La configurazione del server MySQL non è stata trovata o non è leggibile." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "La configurazione del server MySQL è usabile." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"La configurazione del server MySQL è stata trovata in %1 ed è leggibile." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Impossibile connettersi al server PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Trovato server PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Il server PostgreSQL è stato trovato e la connessione è funzionante." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl non trovato" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Il programma 'akonadictl' deve essere disponibile nel $PATH. Assicurati che " +"il server Akonadi sia installato." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl trovato ed utilizzabile" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Il programma di controllo del server Akonadi «%1» è stato trovato e la sua " +"esecuzione è andata a buon fine.\n" +"Risultato:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl trovato, ma non utilizzabile" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Il programma di controllo del server Akonadi «%1» è stato trovato ma la sua " +"esecuzione non è andata a buon fine.\n" +"Risultato:\n" +"%2\n" +"Assicurati che il server Akonadi sia installato correttamente." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Processo di controllo di Akonadi registrato su D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Il processo di controllo di Akonadi è registrato su D-Bus; questo " +"normalmente indica che è operativo." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Processo di controllo di Akonadi non registrato su D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Il processo di controllo di Akonadi non è registrato su D-Bus; questo " +"normalmente indica che non si è avviato o ha riscontrato un errore critico " +"durante l'avvio." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Processo del server Akonadi registrato su D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Il processo del server Akonadi è registrato su D-Bus; questo normalmente " +"indica che è operativo." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Processo del server Akonadi non registrato su D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Il processo del server Akonadi non è registrato su D-Bus; questo " +"normalmente indica che non si è avviato o ha riscontrato un errore critico " +"durante l'avvio." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Impossibile controllare la versione del protocollo." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Senza la connessione al server è impossibile verificare se la versione del " +"protocollo rispetta i requisiti." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "La versione del protocollo del server è troppo vecchia." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"La versione del protocollo del server è %1, ma è richiesta la versione %2 " +"dal client. Se hai aggiornato di recente KDE PIM, assicurati di riavviare " +"sia Akonadi che le applicazioni KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "La versione del protocollo del server è troppo nuova." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "La versione del protocollo del server corrisponde." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "La versione attuale del protocollo è %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Trovati agenti delle risorse." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "È stato trovato almeno un agente delle risorse." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Agenti delle risorse non trovati." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nessun agente delle risorse è stato trovato. Akonadi non è utilizzabile " +"senza almeno uno di essi. Questo normalmente indica che non è stato " +"installato alcun agente delle risorse o c'è un problema con la " +"configurazione. Gli agenti sono stati ricercati nei seguenti percorsi: «%1». " +"La variabile d'ambiente XDG_DATA_DIRS è impostata a «%2», assicurati che " +"includa tutti i percorsi in cui sono installati agenti di Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Registro degli errori dell'attuale server Akonadi non trovato." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Il server Akonadi non ha restituito alcun errore durante il suo ultimo avvio." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Trovato registro degli errori dell'attuale server Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Il server Akonadi ha restituito alcuni errori durante il suo ultimo avvio. " +"Il registro degli errori può essere consultato in «%1»." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" +"Registro degli errori del precedente avvio del server Akonadi non trovato." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Il server Akonadi non ha restituito alcun errore durante l'avvio precedente." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Trovato registro degli errori del precedente avvio del server Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Il server Akonadi ha restituito alcuni errori durante l'avvio precedente. Il " +"registro degli errori può essere consultato in «%1»." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" +"Registro degli errori dell'attuale processo di controllo di Akonadi non " +"trovato." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Il processo di controllo di Akonadi non ha restituito alcun errore durante " +"il suo ultimo avvio." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" +"Trovato registro degli errori dell'attuale processo di controllo di Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Il processo di controllo di Akonadi ha restituito alcuni errori durante il " +"suo ultimo avvio. Il registro degli errori può essere consultato in «%1»." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" +"Registro degli errori del precedente avvio del processo di controllo di " +"Akonadi non trovato." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Il processo di controllo di Akonadi non ha restituito alcun errore durante " +"l'avvio precedente." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" +"Trovato registro degli errori del precedente avvio del processo di controllo " +"di Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Il processo di controllo di Akonadi ha restituito alcuni errori durante " +"l'avvio precedente. Il registro degli errori può essere consultato in «%1»." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi è stato eseguito come utente root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"L'esecuzione di applicazioni che accedono ad Internet, e che sono eseguite " +"con i privilegi dell'utente root/amministratore, ti espone a molti rischi di " +"sicurezza. MySQL, usato da questa installazione di Akonadi, non si avvia se " +"viene eseguito come root, in modo da proteggerti da questi rischi." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi non è eseguito come utente root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi non è eseguito con le credenziali dell'utente root/amministratore; " +"questa è l'impostazione raccomandata per una macchina sicura." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Salva resoconto verifiche" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Errore" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Impossibile aprire il file «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Si è verificato un errore durante l'avvio del server Akonadi. I seguenti " +"controlli di autodiagnosi dovrebbero aiutare a comprendere e quindi " +"risolvere il problema. Quando richiedi supporto o segnali dei bug, includi " +"sempre questo resoconto." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Dettagli" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Per ulteriori suggerimenti per la risoluzione dei problemi puoi fare " +"riferimento a userbase.kde.org/" +"Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nuova cartella..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nuovo" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Elimina cartella" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Elimina" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sincronizza cartella" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronizza" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Proprietà cartella" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Proprietà" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Incolla" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Incolla" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gestisci &sottoscrizioni locali..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gestisci sottoscrizioni locali" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Aggiungi alle cartelle preferite" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Aggiungi ai preferiti" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Rimuovi dalle cartelle preferite" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Rimuovi dai preferiti" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Rinomina preferito..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Rinomina" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copia cartella in..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copia in" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copia elemento in..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Sposta elemento in..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Sposta in" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Sposta cartella in..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Taglia elemento" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Taglia" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Taglia cartella" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Crea risorsa" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Elimina risorsa" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Proprietà della risorsa" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sincronizza risorsa" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Lavora non in linea" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronizza cartella ricorsivamente" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronizza ricorsivamente" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Cestina cartella" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Cestina cartella" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Cestina elemento" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Cestina elemento" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Ripristina cartella dal cestino" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Ripristina cartella dal cestino" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Ripristina elemento dal cestino" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Ripristina elemento dal cestino" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Ripristina collezione dal cestino" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Ripristina collezione dal cestino" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronizza cartelle preferite" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronizza cartelle preferite" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronizza albero delle cartelle" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copia cartella" +msgstr[1] "&Copia %1 cartelle" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copia elemento" +msgstr[1] "&Copia %1 elementi" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Taglia elemento" +msgstr[1] "&Taglia %1 elementi" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Taglia cartella" +msgstr[1] "&Taglia %1 cartelle" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Elimina elemento" +msgstr[1] "&Elimina %1 elementi" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Elimina cartella" +msgstr[1] "&Elimina %1 cartelle" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronizza cartella" +msgstr[1] "&Sincronizza %1 cartelle" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Elimina risorsa" +msgstr[1] "&Elimina %1 risorse" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronizza risorsa" +msgstr[1] "&Sincronizza %1 risorse" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copia cartella" +msgstr[1] "Copia %1 cartelle" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copia elemento" +msgstr[1] "Copia %1 elementi" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Taglia elemento" +msgstr[1] "Taglia %1 elementi" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Taglia cartella" +msgstr[1] "Taglia %1 cartelle" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Elimina elemento" +msgstr[1] "Elimina %1 elementi" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Elimina cartella" +msgstr[1] "Elimina %1 cartelle" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronizza cartella" +msgstr[1] "Sincronizza %1 cartelle" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Elimina risorsa" +msgstr[1] "Elimina %1 risorse" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronizza risorsa" +msgstr[1] "Sincronizza %1 risorse" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nome" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Vuoi davvero eliminare questa cartella e tutte le sue sotto-cartelle?" +msgstr[1] "" +"Vuoi davvero eliminare %1 cartelle e tutte le relative sotto-cartelle?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Eliminare la cartella?" +msgstr[1] "Eliminare le cartelle?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Impossibile eliminare la cartella: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Eliminazione cartella non riuscita" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Proprietà della cartella %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Vuoi davvero eliminare l'elemento selezionato?" +msgstr[1] "Vuoi davvero eliminare %1 elementi?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Eliminare elemento?" +msgstr[1] "Eliminare elementi?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Impossibile eliminare l'elemento: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Eliminazione elemento non riuscita" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Rinomina preferita" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nome:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nuova risorsa" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Impossibile creare la risorsa: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Creazione della risorsa non riuscita" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Vuoi davvero eliminare questa risorsa?" +msgstr[1] "Vuoi davvero eliminare %1 risorse?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Eliminare la risorsa?" +msgstr[1] "Eliminare le risorse?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Impossibile incollare i dati: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Impossibile incollare" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Non è possibile aggiungere «/» nel nome della cartella" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Errore nella creazione di una nuova cartella" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Non è possibile aggiungere «.» all'inizio o alla fine del nome della " +"cartella." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Prima di sincronizzare la cartella «%1» è necessario che la risorsa sia in " +"linea. Vuoi renderla in linea?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "L'account «%1» non è in linea" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Vai \"in linea\"" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Sposta in questa cartella" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copia in questa cartella" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Aggiornamento della sottoscrizione non riuscito: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Errore di sottoscrizione" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Sottoscrizioni locali" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Cerca:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Solo sottoscritte" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "S&ottoscrivi" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Canc&ella sottoscrizione" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Impossibile creare una nuova etichetta" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Errore durante la creazione di una nuova etichetta" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Vuoi davvero eliminare l'etichetta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Elimina etichetta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Elimina etichetta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Seleziona le etichette che dovrebbero essere applicate." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Crea nuova etichetta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gestisci etichette" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Seleziona etichette..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Seleziona etichette" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Pulisci" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Clic per aggiungere etichette" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Convertitore da Akonadi a XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Converte un sottoalbero di una collezione Akonadi in un file XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nessun dato caricato." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nessun nome di file specificato" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Impossibile aprire il file di dati «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Il file %1 non esiste." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Impossibile analizzare il file di dati «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "La definizione dello schema non può essere caricata e analizzata." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Impossibile creare il contesto per l'analizzatore dello schema." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Impossibile creare lo schema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Impossibile creare il contesto per la validazione dello schema." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Formato file non valido." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Impossibile analizzare il file di dati: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Impossibile trovare la collezione %1" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Id remoto" + +#~ msgid "MimeType" +#~ msgstr "MimeType" + +#~ msgid "Form" +#~ msgstr "Modulo" + +#~ msgid "Default Name" +#~ msgstr "Nome predefinito" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Elimina" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Annulla" + +#~ msgid "Take left one" +#~ msgstr "Usa quello a sinistra" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Due aggiornamenti sono in conflitto tra loro.Scegli quale/i " +#~ "aggiornamento/i usare." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Non letti" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Totale" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Dimensione" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Risorsa Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nome" + +#~ msgid "Invalid collection specified" +#~ msgstr "È stata specificata una collezione non valida" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Trovata versione %1 del protocollo, necessaria almeno la %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "La versione del protocollo del servizio è sufficientemente recente." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "La versione del protocollo del server è %1, pari o più recente della " +#~ "versione richiesta %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Rilevato albero di collezioni locali incoerente." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "È stata fornita una collezione remota senza una catena di antenati che " +#~ "termini con una radice, la risorsa è danneggiata." + +#~ msgid "KDE Test Program" +#~ msgstr "Programma di test di KDE" + +#~ msgid "Cannot list root collection." +#~ msgstr "Impossibile elencare la collezione radice." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "L'estensione «%1» non è incorporata e compilata in modo statico, indica " +#~ "questa informazione nella segnalazione di bug." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Estensione non compilata staticamente" + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Servizio di ricerca Nepomuk registrato su D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Il servizio di ricerca Nepomuk è registrato su D-Bus; questo normalmente " +#~ "indica che è operativo." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Servizio di ricerca Nepomuk non registrato su D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Il servizio di ricerca Nepomuk non è registrato su D-Bus; questo " +#~ "normalmente indica che non si è avviato o ha riscontrato un errore " +#~ "critico durante l'avvio." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Il servizio di ricerca Nepomuk usa un motore non adatto." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Il servizio di ricerca Nepomuk usa il motore «%1», il cui uso con Akonadi " +#~ "non è raccomandato." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Il servizio di ricerca Nepomuk usa un motore appropriato. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Il servizio di ricerca Nepomuk usa uno dei motori consigliati." + +#~ msgid "Fetch Job Error" +#~ msgstr "Errore nella procedura di recupero" + +#~ msgid "Cache" +#~ msgstr "Cache" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Eredita dal genitore la politica della cache" + +#~ msgid "Cache Policy" +#~ msgstr "Politica della cache" + +#~ msgid "Interval check time:" +#~ msgstr "Ogni quanti minuti controllare:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Scadenza della cache locale:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Sincronizza su richiesta" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Scegli quali cartelle vuoi vedere nell'albero delle cartelle" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Cerca" + +#~ msgid "Available Folders" +#~ msgstr "Cartelle disponibili" + +#~ msgid "Current Changes" +#~ msgstr "Cambiamenti attuali" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Annulla sottoscrizione alla cartella selezionata" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi non è operativo.
Dettagli...

" + +#~ msgid "TODO" +#~ msgstr "Da fare" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Risorsa Akonadi" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Il server Akonadi ha riportato alcuni errori in %1 durante l'avvio." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Il processo di controllo di Akonadi ha riportato alcuni errori in %1 " +#~ "durante l'avvio." + +#, fuzzy +#~ msgid "Nepomuk search service uses Sesame2 backend. " +#~ msgstr "Il servizio di ricerca Nepomuk usa motore Sesame2. " diff --git a/po/ja/akonadi_knut_resource.po b/po/ja/akonadi_knut_resource.po new file mode 100644 index 0000000..1ba1d69 --- /dev/null +++ b/po/ja/akonadi_knut_resource.po @@ -0,0 +1,82 @@ +# Fumiaki Okushi , 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-11-21 13:35-0800\n" +"Last-Translator: Fumiaki Okushi \n" +"Language-Team: Japanese \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "データファイルが選択されていません。" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "ファイル %1 を読み込みました。" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "データファイルを選択" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut データファイル" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "リモート ID %1 のアイテムがありません。" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM ツリーに親コレクションがありません。" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "コレクションを書くことができません。" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM ツリーに変更されたコレクションはありません。" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM ツリーに削除されたコレクションはありません。" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "親コレクション %1 が DOM ツリーにありません。" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "アイテムを書くことができません。" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM ツリーに変更されたアイテムはありません。" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM ツリーに削除されたアイテムはありません。" diff --git a/po/ja/libakonadi5.po b/po/ja/libakonadi5.po new file mode 100644 index 0000000..cd5e763 --- /dev/null +++ b/po/ja/libakonadi5.po @@ -0,0 +1,2588 @@ +# Translation of libakonadi into Japanese. +# This file is distributed under the same license as the kdepimlibs package. +# Yukiko Bando , 2007, 2008. +# Fumiaki Okushi , 2014, 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-05-15 17:47-0700\n" +"Last-Translator: Fumiaki Okushi \n" +"Language-Team: Japanese \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Yukiko Bando" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ybando@k6.dion.ne.jp" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "エージェント識別子" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi エージェント" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "準備完了" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "オフライン" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "同期中..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "エラー。" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "リソースの識別子" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi リソース" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "オフラインモードではアイテムの取得はできません。" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "フォルダ “%1” を同期中" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "そのようなコレクションはありません。" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "エージェントのインスタンスの作成がタイムアウトしました。" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to obtain agent type '%1'." +msgstr "エージェントのインスタンスを作成できません。" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "エージェントのインスタンスを作成できません。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "無効なコレクションのインスタンスです。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "無効なリソースのインスタンス。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, fuzzy, kde-format +#| msgid "Agent instance creation timed out." +msgid "Collection attributes synchronization timed out." +msgstr "エージェントのインスタンスの作成がタイムアウトしました。" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "No such collection." +msgid "Invalid collection to copy" +msgstr "そのようなコレクションはありません。" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "No such collection." +msgid "Invalid destination collection" +msgstr "そのようなコレクションはありません。" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Folder &Properties..." +msgid "Failed to parse Collection from response" +msgstr "フォルダのプロパティ(&P)..." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "無効なコレクション" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "無効なコレクションです。" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "無効なコレクション。" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "No such collection." +msgid "Invalid parent collection" +msgstr "そのようなコレクションはありません。" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Akonadi サービスに接続できません。" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi サーバのプロトコルのバージョンが非互換です。互換性のあるバージョンが" +"インストールされていることを確認してください。" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "ユーザが操作をキャンセルしました。" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "未知のエラーです。" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "エージェントのインスタンスを作成できません。" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "リソースの同期がタイムアウトしました。" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "リソース %1 のルートコレクションを取得できませんでした。" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "リソース ID が指定されていません。" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "無効なリソースの識別子 '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "タグの作成に失敗しました。" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "無効なコレクションが渡されました" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "無効なコレクションか、アイテムリストが空です" + +#: core/jobs/trashrestorejob.cpp:90 +#, fuzzy, kde-format +#| msgid "No such collection." +msgid "Could not find restore collection and restore resource is not available" +msgstr "そのようなコレクションはありません。" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "名前" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "詠み込み中..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "エラー" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "名前" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "アイテムをコピーできません: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "コレクションをコピーできません: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "アイテムを移動できません: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "コレクションを移動できません: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "エラー" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "お気に入りフォルダ" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "合計件数" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "未読件数" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "クォータ" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "保存サイズ" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "サブフォルダの保存サイズ" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "未読" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "合計" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "サイズ" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, fuzzy, kde-format +#| msgid "No description available" +msgid "No session available for this index" +msgstr "説明はありません" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "名前のないプラグイン" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "説明はありません" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi のセルフテスト" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi サービスに接続できません。" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "エージェントのインスタンスを削除(&D)" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "エージェントのインスタンスを設定(&C)" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "新しいエージェントのインスタンス" + +#: widgets/agentactionmanager.cpp:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Could not create agent instance: %1" +msgstr "エージェントのインスタンスを作成できません。" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "エージェントのインスタンスの作成が失敗しました。" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "エージェントのインスタンスを削除しますか?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "本当に選択したエージェントのインスタンスを削除しますか?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 の設定" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 のハンドブック" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "%1 について" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "エージェントのインスタンスを作成できません。" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "分" +msgstr[1] "分" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "このフォルダを選択したときに同期" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "チェックしない" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "分" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "ローカルにキャッシュされた部分" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, fuzzy, kde-format +#| msgctxt "no cache timeout" +#| msgid "Never" +msgctxt "no cache timeout" +msgid "Forever" +msgstr "なし" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "検索" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "新しいサブフォルダ(&N)..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "新しいフォルダ" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "名前" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "フォルダを作成できませんでした: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "フォルダの作成に失敗" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "全般" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "オブジェクト 1 個" +msgstr[1] "オブジェクト %1 個" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "名前(&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "カスタムアイコンを使う(&U):" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "フォルダ" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "統計" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "コンテンツ:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "オブジェクトなし" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "サイズ:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 バイト" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "フォルダのタイプ:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "アイテム" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "合計アイテム数:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "未読アイテム数:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Reindex folder" +msgstr "フォルダを削除(&D)" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "フォルダなし" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "コレクションダイアログを開きます" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "コレクションを選択" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "ここに移動(&M)" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "ここにコピー(&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "キャンセル" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi サーバを起動..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi サーバを停止..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "ここに移動(&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "ここにコピー(&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "ここにリンク(&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "キャンセル(&A)" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Akonadi サービスに接続できません。" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "Personal information management service is starting..." +msgstr "" +"Akonadi PIM フレームワークは使用できません。\n" +"詳細...をクリックすると問題点を確認することができま" +"す。" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "Personal information management service is shutting down..." +msgstr "" +"Akonadi PIM フレームワークは使用できません。\n" +"詳細...をクリックすると問題点を確認することができま" +"す。" + +#: widgets/erroroverlay.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Akonadi PIM フレームワークは使用できません。\n" +"詳細...をクリックすると問題点を確認することができま" +"す。" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi PIM フレームワークは使用できません。\n" +"詳細...をクリックすると問題点を確認することができま" +"す。" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi PIM フレームワークは使用できません。\n" +"「詳細...」をクリックすると問題点を確認することができます。" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Akonadi PIM フレームワークは使用できません。\n" +"詳細...をクリックすると問題点を確認することができま" +"す。" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "詳細..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "最近のフォルダ" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "お気に入りの名前を変更" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "名前:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi サーバのセルフテスト" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "レポートを保存..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "レポートをクリップボードにコピー" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"現在の Akonadi サーバの設定には QtSQL ドライバ '%1' が必要であり、そのドライ" +"バがあなたのシステムに発見されました。" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"現在の Akonadi サーバの設定には QtSQL ドライバ %1 が必要です。\n" +"以下のドライバがインストールされています: %2\n" +"必要なドライバがインストールされていることを確認してください。" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "データベースドライバが見つかりました。" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "データベースドライバが見つかりませんでした。" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL サーバの実行ファイルはテストされていません。" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "現在の設定は組み込み MySQL サーバを必要としません。" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Akonadi が MySQL サーバ %1 を使うように設定されています。\n" +"MySQL サーバがインストールされていることを確認して正しいパスを設定し、サーバ" +"の実行ファイルに対して必要な読み取りと実行権限があることを確認してください。" +"サーバの実行ファイルの名前は一般的には ‘mysqld’ ですが、場所はディストリ" +"ビューションによって異なります。" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL サーバが見つかりませんでした。" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL サーバが読み取り可能ではありません。" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL サーバが実行可能ではありません。" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL が予期しない名前で見つかりました。" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL サーバが見つかりました。" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL サーバが見つかりました: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL サーバは実行可能です。" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "MySQL サーバ %1 の実行が以下のエラーメッセージで失敗しました: %2" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL サーバの実行が失敗しました。" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL サーバのエラーログはテストされていません。" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "現在の MySQL のエラーログは見つかりませんでした。" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "MySQL サーバは起動中に ‘%1’ に何もエラーを報告しませんでした。" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL のエラーログが読み取り可能ではありません。" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"MySQL サーバのエラーログファイルが見つかりましたが、読み取り可能ではありませ" +"ん: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL サーバのログにエラーが含まれています。" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL サーバのエラーログファイル ‘%1’ にエラーが含まれています。" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL サーバのログに警告が含まれています。" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL サーバのログファイル ‘%1’ に警告が含まれています。" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL サーバのログにエラーは含まれていません。" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL サーバのログファイル ‘%1’ にはエラーも警告も含まれていません。" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL サーバの設定はテストされていません。" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL サーバのデフォルトの設定が見つかりました。" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "MySQL サーバのデフォルトの設定が %1 に見つかり、読み取り可能です。" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL サーバのデフォルトの設定が見つかりませんでした。" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL サーバのデフォルトの設定が見つからなかったか、読み取り可能ではありませ" +"ん。Akonadi のインストールが完全で、あなたに必要なすべてのアクセス権限がある" +"ことを確認してください。" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL サーバのカスタム設定はありません。" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"MySQL サーバのカスタム設定が見つかりませんでしたが、これはオプションです。" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL サーバのカスタム設定が見つかりました。" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "MySQL サーバのカスタム設定が %1 に見つかり、読み取り可能です。" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL サーバのカスタム設定が読み取り可能ではありません。" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL サーバのカスタム設定が %1 に見つかりましたが、読み取り可能ではありませ" +"ん。あなたのアクセス権限を確認してください。" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL サーバの設定が見つからなかったか、読み取り可能ではありません。" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL サーバの設定が見つからなかったか、読み取り可能ではありません。" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL サーバの設定は使用可能です。" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL サーバの設定が %1 に見つかり、読み取り可能です。" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL サーバに接続できません。" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL サーバが見つかりました。" + +#: widgets/selftestdialog.cpp:378 +#, fuzzy, kde-format +#| msgid "The MySQL server log file '%1' contains warnings." +msgid "The PostgreSQL server was found and connection is working." +msgstr "" +"MySQL サーバのログファイル %1 に警告が含まれています。" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl が見つかりませんでした" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"プログラム akonadictl が $PATH になければなりません。Akonadi サーバがインス" +"トールされていることを確認してください。" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl が見つかり、使用可能です" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi サーバを制御するプログラム %1 が見つかり、実行することができまし" +"た。\n" +"結果:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl が見つかりましたが、使用可能ではありません" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi サーバを制御するプログラム %1 が見つかりましたが、実行することができ" +"ませんでした。\n" +"結果:\n" +"%2\n" +"Akonadi サーバが正しくインストールされていることを確認してください。" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi の制御プロセスが D-Bus に登録されています。" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi の制御プロセスが D-Bus に登録されています。これは一般的には Akonadi " +"が使用可能であること意味します。" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi の制御プロセスが D-Bus に登録されていません。" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi の制御プロセスが D-Bus に登録されていません。これは一般的には " +"Akonadi が起動しなかったか、起動中に致命的なエラーが発生したことを意味しま" +"す。" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi サーバのプロセスが D-Bus に登録されています。" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi サーバのプロセスが D-Bus に登録されています。これは一般的には " +"Akonadi が使用可能であることを意味します。" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi サーバのプロセスが D-Bus に登録されていません。" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi サーバのプロセスが D-Bus に登録されていません。これは一般的には " +"Akonadi が起動しなかったか、起動中に致命的なエラーが発生したことを意味しま" +"す。" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "プロトコルのバージョンを調べることができません。" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"サーバに接続できないため、プロトコルのバージョンが要件を満たしているかどうか" +"を調べることができません。" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "サーバのプロトコルのバージョンが古すぎます。" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"サーバのプロトコルのバージョンは %1 ですが、クライアントはバージョン %2 が必" +"要です。最近 KDE PIM をアップデートした場合、Akonadi と KDE PIM のアプリケー" +"ションを再起動してください。" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "サーバのプロトコルのバージョンが新しすぎます。" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "サーバのプロトコルのバージョンが合致します。" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "現在のプロトコルバージョンは %1 です。" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "リソースエージェントが見つかりました。" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "少なくとも 1 つのリソースエージェントが見つかりました。" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "リソースエージェントが見つかりませんでした。" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"リソースエージェントが見つかりませんでした。少なくとも 1 つなければ Akonadi " +"は使用できません。これは通常、リソースエージェントが全くインストールされてい" +"ないか、その設定に問題があること意味します。以下のパスを探しました: %1。" +"XDG_DATA_DIRS 環境変数は %2 に設定されています。Akonadi エージェントがインス" +"トールされているすべてのパスがこれに含まれていることを確認してください。" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "現在の Akonadi サーバのエラーログは見つかりませんでした。" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi サーバは現在の起動中に何もエラーを報告しませんでした。" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "現在の Akonadi サーバのエラーログが見つかりました。" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi サーバは現在の起動中にエラーを報告しました。ログは %1 にあります。" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "以前の Akonadi サーバのエラーログは見つかりませんでした。" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi サーバは以前の起動中に何もエラーを報告しませんでした。" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "以前の Akonadi サーバのエラーログが見つかりました。" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi サーバは以前の起動中にエラーを報告しました。ログは %1 にあります。" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "現在の Akonadi 制御プロセスのエラーログは見つかりませんでした。" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "Akonadi 制御プロセスは現在の起動中に何もエラーを報告しませんでした。" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "現在の Akonadi 制御プロセスのエラーログが見つかりました。" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi 制御プロセスは現在の起動中にエラーを報告しました。ログは %1 にありま" +"す。" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "以前の Akonadi 制御プロセスのエラーログは見つかりませんでした。" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "Akonadi 制御プロセスは以前の起動中に何もエラーを報告しませんでした。" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "以前の Akonadi 制御プロセスのエラーログが見つかりました。" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi 制御プロセスは以前の起動中にエラーを報告しました。ログは %1 にありま" +"す。" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "テストレポートを保存" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "エラー" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "ファイル ‘%1’ を開けませんでした" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi サーバの起動中にエラーが発生しました。以下のセルフテストがこの問題の" +"原因究明と解決に役立ちます。サポートを求めたりバグを報告するときは、必ずこの" +"レポートを添えてください。" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "詳細" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

userbase.kde.org/Akonadi " +"にもトラブルシューティングのヒントがあります。

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "新しいフォルダ(&N)..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "フォルダを削除(&D)" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "削除" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "フォルダを同期(&S)" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "同期" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "フォルダのプロパティ(&P)" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "プロパティ" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "貼り付け(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "貼り付け" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "ローカル購読の管理(&S)..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "ローカル購読の管理" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "お気に入りフォルダに追加" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "お気に入りに追加" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "お気に入りから削除" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "お気に入りの名前を変更..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "名前変更" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "フォルダをコピー..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "コピー" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "アイテムをコピー..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "アイテムを移動..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "移動" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "フォルダを移動..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "アイテムを切り取り(&C)" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "切り取り" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "フォルダを切り取り(&C)" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "リソースを作成" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "リソースを削除" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "リソースのプロパティ(&R)" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "リソースを同期" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "オフラインで作業" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "再帰的にフォルダを同期(&S)" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "再帰的に同期" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "フォルダをごみ箱へ移動(&M)" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "フォルダをごみ箱へ移動" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "アイテムをごみ箱へ移動(&M)" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "アイテムをごみ箱へ移動" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "ごみ箱からフォルダを復元(&R)" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "ごみ箱からフォルダを復元" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "ごみ箱からアイテムを復元(&R)" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "ごみ箱からアイテムを復元" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "ごみ箱からコレクションを復元(&R)" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "ごみ箱からコレクションを復元" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "お気に入りフォルダを同期(&S)" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "お気に入りフォルダを同期" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "フォルダツリーを同期" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "フォルダをコピー(&C)" +msgstr[1] "%1 フォルダをコピー(&C)" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "アイテムをコピー(&C)" +msgstr[1] "%1 アイテムをコピー(&C)" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "アイテムを切り取り(&C)" +msgstr[1] "%1 個のアイテムを切り取り(&C)" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "フォルダを切り取り(&C)" +msgstr[1] "%1 個のフォルダを切り取り(&C)" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "アイテムを削除(&D)" +msgstr[1] "%1 アイテムを削除(&D)" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "フォルダを削除(&D)" +msgstr[1] "%1 個のフォルダを削除(&D)" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "フォルダを同期(&S)" +msgstr[1] "%1 個のフォルダを同期(&S)" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "リソースを削除(&D)" +msgstr[1] "%1 個のリソースを削除(&D)" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "リソースを同期(&S)" +msgstr[1] "%1 個のリソースを同期(&S)" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "フォルダをコピー(&C)" +msgstr[1] "%1 個のフォルダをコピー(&C)" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "アイテムをコピー(&C)" +msgstr[1] "%1 個のアイテムをコピー(&C)" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "アイテムを切り取り(&C)" +msgstr[1] "%1 個のアイテムを切り取り(&C)" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "フォルダを切り取り(&C)" +msgstr[1] "%1 個のフォルダを切り取り(&C)" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "アイテムを削除(&D)" +msgstr[1] "%1 個のアイテムを削除(&D)" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "フォルダを削除" +msgstr[1] "%1 個のフォルダを削除" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "フォルダを同期" +msgstr[1] "%1 個のフォルダを同期" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "リソースを削除" +msgstr[1] "%1 個のリソースを削除" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "リソースを同期" +msgstr[1] "%1 個のリソースを同期" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "名前" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "このフォルダとそのすべてのサブフォルダを本当に削除しますか?" +msgstr[1] "%1 個のフォルダとそれらすべてのサブフォルダを本当に削除しますか?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "フォルダを削除しますか?" +msgstr[1] "フォルダを削除しますか?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "フォルダを削除できませんでした: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "フォルダの削除に失敗" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "フォルダ “%1” のプロパティ" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "選択したアイテムを本当に削除しますか?" +msgstr[1] "%1 個のアイテムを本当に削除しますか?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "アイテムを削除" +msgstr[1] "アイテムを削除" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "アイテムを削除できませんでした: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "アイテムの削除に失敗" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "お気に入りの名前を変更" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "名前:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "新しいリソース" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "リソースを作成できませんでした: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "リソースの作成に失敗" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "このリソースを本当に削除しますか?" +msgstr[1] "%1 個のリソースを本当に削除しますか?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "リソースを削除しますか?" +msgstr[1] "リソースを削除しますか?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "データを貼り付けできませんでした: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "貼り付け失敗" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "オフライン" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "このフォルダに移動" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "このフォルダにコピー" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "購読を更新できません: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "購読エラー" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "ローカル購読" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "検索:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "購読のみ(&S)" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "購読(&B)" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "購読中止(&U)" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "新しいタグの作成に失敗しました" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "タグ %1 を本当に削除しますか?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "タグを削除" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "タグを削除" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "新しいタグを作成" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "タグを選択..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "タグを選択" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "データファイル '%1' を開けませんでした" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "データファイル '%1' を解析できませんでした。" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "エージェントのインスタンスを作成できません。" + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "エージェントのインスタンスを作成できません。" + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "エージェントのインスタンスを作成できません。" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "無効なファイルフォーマット。" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "データファイルを解析できませんでした: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "コレクション %1 は見つかりませんでした。" diff --git a/po/kk/akonadi_knut_resource.po b/po/kk/akonadi_knut_resource.po new file mode 100644 index 0000000..a1e9cc2 --- /dev/null +++ b/po/kk/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Sairan Kikkarin , 2010. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-11-12 06:07+0600\n" +"Last-Translator: Sairan Kikkarin \n" +"Language-Team: Kazakh \n" +"Language: kk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Дерек файлы таңдалмаған." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "'%1' файлы сәтті жүктелді." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Деректер файлын таңдау" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut деректер файлы" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Қашықтағы %1 идентификаторы үшін ештеңе табылған жоқ" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM бұтағында аталық жинағы табылған жоқ." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Жинақты жазуы болмады." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM бұтағында өзгертілген жинағы табылған жоқ." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM бұтағында өшірілген жинағы табылған жоқ." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM бұтағында '%1' аталық жинағы табылған жоқ." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Аталым жазылмады." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM бұтағында өзгертілген аталым табылған жоқ." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM бұтағында өшірілген аталым табылған жоқ." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Knut деректер файының жолы" + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Істегі тетік деректерін өзгертпеу." diff --git a/po/kk/libakonadi5.po b/po/kk/libakonadi5.po new file mode 100644 index 0000000..d849604 --- /dev/null +++ b/po/kk/libakonadi5.po @@ -0,0 +1,2781 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Sairan Kikkarin , 2011, 2012, 2013. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2013-07-14 03:20+0600\n" +"Last-Translator: Sairan Kikkarin \n" +"Language-Team: Kazakh \n" +"Language: kk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Lokalize 1.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Сайран Киккарин" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "sairan@computer.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "D-Bus нысанды тіркемеді: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%2 түрдегі %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Агенттің идентификаторы" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi агенті" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Дайын" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Желіден тыс" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Қадамдастыру..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Қате." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Бапталмаған" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Деректер көзінің идентификаторы" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Akonadi ресурсы" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Жарамсыз аталым алынды" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Аталымды құру қатесі: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Жинақты жаңарту жаңылысы: %1." + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Жергілікті жинақты жаңарту жаңылысы: %1." + +#: agentbase/resourcebase.cpp:718 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Жергілікті жинақты жаңарту жаңылысы: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Желіден тыс режімде керегі алынбайды." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "'%1' қапшығын қадамдастыру" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for sync." +msgstr "Дерек жинағын алу жаңылысы." + +#: agentbase/resourcebase.cpp:983 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for attribute sync." +msgstr "Дерек жинағын алу жаңылысы." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Сұралғаны енді жоқ екен" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Тапсырма доғарылды." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Бұндай жинақ жоқ." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Түзелмейтіндей зақымдалған жинаққа тап болды" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Қайшылығын шешетін басқа аталым табылған жоқ" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Құрылған агенттің D-Bus интерфейсіне қатынау жоқ." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Агент данасын құруын күту уақыты өті." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Агенттің '%1' түрі туралы мәлімет алынбады." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Агент данасын құруға болмайды." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Жарамсыз жинақ данасы." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Жармсыз деректер көзінің данасы." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "'%1' дерек көзінің D-Bus интерфейсі қол жеткізбеді" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Жинақ атрибуттарын қадамдастыруын күту уақыты өтті." + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Жарамсыз жинақ" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Жарамсыз жинақ" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Жарамсыз аталығы" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to parse Collection from response" +msgstr "Дерек жинағын алу жаңылысы." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Жарамсыз жинақ" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Келтірілген жинақ дұрыс емес." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Жылжытатын аталымдар келтірілмеген" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Қайда - дұрыс келтірілмеген" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Жарамсыз жинақ" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Жарамсыз жинақ" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Akonadi қызметіне қосыла алмады." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi серверінің протокол нұсқасы үйлесімсіз. Сізде үйлесімді нұқсасы " +"орнатылғанын тексеріңіз." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Пайдаланушысы доғарған әрекет." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Беймәлім қате." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "Агент данасын құруға болмайды." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Деректер көзін қадамдастыруын күту уақыты өтті." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "%1 деректер көзінің түбірлі жинағы алынбады." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Ресурстың ID-і келтірілмеген." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Ресурс идентификаторы жарамсыз: '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Әдетті дерек көзін D-Bus арқылы баптау жаңылысы." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Дерек жинағын алу жаңылысы." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Бұғаттауды күту уақыты өтті." + +#: core/jobs/tagcreatejob.cpp:49 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create tag." +msgstr "Агент данасын құруға болмайды." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Жинақты Шелекке тастау жаңылысы, тастау доғарылады" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Жарамсыз аталымдарды тастап кеттік" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Жарамсыз жинақты тастап кеттік" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Жарамсыз жинақ жоқ немесе бос аталым тізімі" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Қалпына келтіретін жинақ табылмады не дерек көзі қол жеткізбеуде" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Атауы" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Жүктеу.." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Қате." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Көздеген '%1' жинақта\n" +"'%2'. деген жинақ бар екен." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Атауы" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Көшірілінбегені:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Мынау жинақ алынбады:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Жылжытылмағаны:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Мынау жинақ жылжытылмады:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Жасалмаған сілтеме:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Қате." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Таңдамалы қапшықтар" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Жалпы хаттар саны" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Оқылмаған хаттар" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Квота" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Жинақтауыш көлемі" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Ішкі қапшық жинақтауышының көлемі" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Оқылмаған" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Барлығы" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Өлшемі" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Инекстейтіні алынбады" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Индексі енді жоқ" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Индекстің керек ('%1') бөлігі жоқ" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Бұл индекстің сеансы табылмады" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Бұл индексте аталымдар жоқ" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Атаусыз плагині" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Сипаттамасы жоқ" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "Akonadi серверінің өзін-өзі сынағы" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi қызметіне қосыла алмады." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Жана агент данасы..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Агент данасын ө&шіру" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Агент данасын &баптау" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Жана агент данасы" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Агент данасы құрылмады: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Агент данасын құру жаңылысы" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Агент данасын өшірмексіз бе?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Шынымен таңдалған агент данасын өшірмексіз бе?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "Агент данасын құруға болмайды." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "минут" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Кіріс" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Аталық қапшық не тіркелгісінің параметрлері қолданылсын" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Бұл қапшық таңдалғанда қадамдастырылсын" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Автоқадамдастыру аралығы:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Ешқашан" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "минут" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Жергілікті кэштелетін бөліктер" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Кіріс параметрлері" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "Әрқашанда толық хаттар алынсын" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "Хат беттері - сұрағанда" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Хат беттері келесі үшін жергілікті сақталсын:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Үнемі" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "" +#| "@info/plain Displayed grayed-out inside the textbox, verb to search" +#| msgid "Search" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Іздеу" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Жаңа ішкі қапшық..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Таңдалған қапшығының ішінде жаңасын құру" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Жаңа қапшық" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Атауы" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Қапшықты құруы болмады: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Қапшықты құру жаңылысы" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Жалпы" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 нысан" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Аты:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Өзгеше таңбашасы:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "қапшық" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Статистика" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Мазмұны:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 нысан" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Өлшемі:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 байт" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "Аталымды құру қатесі: %1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "Қапшықтың қа&сиеттері" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "%1 аталымды қиып алу" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Жалпы хаттар саны" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Оқылмаған хаттар" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "Жуырдағы қапшық" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Қапшық жоқ" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Жинақ диалогын ашу" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Жинақты таңдау" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Осында жылжыту" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Осында көшірмелеу" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Бас тарту" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Өзгерткен кезі" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Жалаушалары" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Атрибуты: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Қайшылықты шешу" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "Оң жақтағысын алу" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Екеуіде қалсын" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Дерек" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi серверін жегу..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi серверін тоқтату..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Мұнда &жылжыту" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "Мұнда &көшірмелеу" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Осында &сілтеме құру" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "Бас &тарту" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Akonadi қызметіне қосыла алмады." + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Дербес ақпарат басқару қызметін жегу..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Дербес ақпарат басқару қызметін доғару..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Дербес ақпарат басқару қызметі дерек қорды жаңартуда." + +#: widgets/erroroverlay.cpp:243 +#, fuzzy, kde-format +#| msgid "" +#| "Personal information management service is performing a database upgrade. " +#| "This happens after a software update and is necessary to optimize " +#| "performance. Depending on the amount of personal information, this might " +#| "take a few minutes." +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Дербес ақпарат басқару қызметі дерек қорды жаңартуда. Бұл жұмысын жылдамдату " +"үшін керек бағдарлама жаңартудан кейін істейітін іс. Ол дербес мәлімет " +"мөлшеріне қарай біршама минут уақыт алады." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi дербес ақпарат басқару қызметі жегілмеген. Ол болмаса бұл бағдарлама " +"істемейді." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Айдау" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi дербес ақпарат басқару құрылымы істемейді.\n" +"Мәселесін білу үшін \"Егжей-тегжейі...\" дегенді түртіңіз." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi дербес ақпарат басқару қызметі істемейді." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Егжей-тегжейі..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "Айдау" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Жуырдағы қапшық" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Таңдамалылыны қайта атау" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Атауы:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi серверінің өзін-өзі сынағы" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Хабарын сақтау" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Алмасу буферіне көшіріп алу" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Қолданыстағы Akonadi серверіңіздің баптауы бойынша '%1' QtSQL драйвері қажет " +"болып, ол жүйеңізде табылды." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Қолданыстағы Akonadi серверіңіздің баптауы бойынша '%1' QtSQL драйвері қажет " +"болды.\n" +"Оның орнына мынауы орнатылды: %2.\n" +"Жарайтынына көз жеткізіңіз." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Деректер қорының драйвері табылды." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Деректер қорының драйвері табылған жоқ." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL сервер бағдарламасы сынақтан өткезілмеді.." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Қолданыстағы баптауы бойынша, ішкі MySQL сервері қажет етілмейді.." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Қолданыстағы Akonadi-дің баптауы бойынша '%1' MySQL сервері қажет болады.\n" +"MySQL сервері орнатылғанын, ол PATH айнымалы көрсететін жолдарда бар екенін, " +"және оны жегуге жеткілікті рұқсатыңызды түгелдеңіз. Серверінің орындалатын " +"файлы әдетте 'mysqld' деп аталады, оның орналасуы дистрибутивіне тәуелді," + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL сервері табылған жоқ." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL сервері оқылмады." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL сервер бағдарламасы орындалмады." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Табылған MySQL бағдарламасының атауы біртүрлі." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL сервері табылды." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL сервері табылды: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL сервері жегуге жарайды." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "'%1' MySQL серверін жегу жаңылысы. Қате туралы хабарламасы: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL серверін жегу жаңылысы." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL серверінің қателер журналы тексерілмеді." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Жүргізіп жатқан MySQL серверінің қателер журналы табылмады" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL сервері бұл жегілген ретте ешбір қатеге ұшыраған жоқ. Журналын '%1' " +"дегенде таба аласыз." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL қателер журналы оқылмады." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "MySQL серверінің қателер журналы табылды, бірақ ол оқылмады: %1." + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL серверінің журналында қателер тіркелді." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL серверінің '%1' журнал файлында қателер тіркелді." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL серверінің журналында ескертулер бар." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL серверінің '%1' журнал файлында ескертулер бар." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL серверінің журналында қателер жоқ." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL серверінің '%1' журнал файлында қате мен ескертулер жоқ." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL серверінің баптауы тексерілмеген." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL серверінің баптауы әдеттегідей." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "MySQL серверінің әдетті баптауы табылды, оны %1 дегенде оқи аласыз." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL серверінің әдетті баптауы табылды." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL серверінің әдетті баптауы табылған не оқылған жоқ. Akonadi түгелдей " +"орнатылғанын және қатынауға рұқсатыңыз бар екенін тексеріңіз." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL серверінің пайданушының баптауы қол жеткізбеді." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"MySQL серверінің пайданушының баптауы табылған жоқ, бірақ оның болуы " +"міндетті емес." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL серверінің пайданушының баптауы табылды" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"MySQL серверінің пайдаланушы бейімдеген баптауы табылды, оны %1 дегенде оқи " +"аласыз" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL серверінің пайданушының баптауы оқылмады." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL серверінің пайдаланушының баптауы табылды, ол %1 дегенде, бірақ оқуға " +"келмейді. Қатынауға рұқсаттарыңызды түгелдеңіз." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL серверінің баптауы табылмады не оқылмады." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL серверінің баптауы табылған жоқ не оқылмайды." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL серверінің баптауы жарайды." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL серверінің баптауы %1 дегенде табылды және оқуға келеді." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL серверіне қосыла алмады." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL сервері табылды." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL сервері табылды, онымен байланыс бар." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl табылған жоқ" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"'akonadictl' бағдарламасы $PATH жолдарында болу керек. Akonadi сервері " +"орнатылғанын тексеріңіз." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl табылды және жарамды" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi серверін басқаратын '%1' бағдарламасы табылды жане іске дайын.\n" +"Нәтижесі:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl табылды бірақ жарамсыз" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi серверін басқаратын '%1' бағдарламасы табылды, бірақ іске " +"жарамсыз.\n" +"Нәтижесі:\n" +"%2\n" +"Akonadi сервері дұрыс орнатылғанын тексеріңіз." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi басқару процесі D-Bus-та тіркелді." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi басқару процесі D-Bus-та тіркелді, бұл, әдетте, ол іске жарамды " +"дегені." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi басқару процесі D-Bus-та тіркеуден өтпеді." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi басқару процесі D-Bus-та тіркеуден өтпеді. Бұл, әдетте, ол " +"жегілмеді, не жегілгенде түзелмейтін қатеге тап болғанын көрсетеді." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi сервер процесі D-Bus-та тіркелді." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi сервер процесі D-Bus-та тіркелді, бұл, әдетте, ол іске жарамды " +"дегені." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi сервер процесі D-Bus-та тіркеуден өтпеді." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi сервер процесі D-Bus-та тіркеуден өтпеді. Бұл, әдетте, ол жегілмеді, " +"не жегілгенде түзелмейтін қатеге тап болғанын көрсетеді." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Протоколдың нұсқасын тексеру мүмкін емес." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Серверімен байланыспай, протоколдың нұсқасы талаптарға қаншалықты сай екенін " +"тексеру мүмкін емес." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Протоколдың нұсқасы тым ескі.." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Сервер протоколының нұсқасы - %1, ал керегі - кемінде %2. Akonadi серверінің " +"жаңа нұсқасын орнатып алыңыз." + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "Протоколдың нұсқасы тым ескі.." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "Протоколдың нұсқасы тым ескі.." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "Протоколдың нұсқасы тым ескі.." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Ресурс агенті табылды." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Кемінде бір деректер көзінің агенті бар." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Ресурс агенті табылған жоқ." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Дерек көздерінің агенттері табылған жоқ. Akonadi істеу үшін кемінде бірі " +"керек.Әдетте бұл бірдебір ресурс агенті орнатылмаған,немесе орнату " +"мәселелерге ұшырағанның белгісі. Іздеген жолдары: '%1'. Ортаның " +"XDG_DATA_DIRS айнымалысы '%2' деп көрсетеді. Бұған Akonadi агенттері " +"орналаса алатын бүкіл жолдар кіретінін тексеріңіз." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Жүргізіп жатқан Akonadi серверінің қателер журналы табылмады" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Akonadi сервері бұл жегілген ретте ешбір қатеге ұшырағанын хабарлаған жоқ." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Жүргізіп жатқан Akonadi серверінің қателер журналы табылды" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"ңAkonadi сервері бұл жегілген ретте қатеге ұшырағанын хабарлады. Журналын %1 " +"дегенде таба аласыз." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Алдында жүргізген Akonadi серверінің қателер журналы табылмады" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Akonadi сервері өткен жегілген ретте ешбір қатеге ұшырағанын хабарлаған жоқ." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Алдында жүргізген Akonadi серверінің қателер журналы табылды" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi сервері өткен жегілген ретте қатеге ұшырағанын хабарлады. Журналын " +"%1 дегенде таба аласыз." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Жүргізіп жатқан Akonadi-ді басқару қателер журналы табылмады" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadi басқару процесі бұл жегілген ретте ешбір қатеге ұшырағанын " +"хабарлаған жоқ." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Жүргізіп жатқан Akonadi-ді басқару қателер журналы табылды" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi басқару процесі бұл жегілген ретте қатеге ұшырағанын хабарлады. " +"Журналын %1 дегенде таба аласыз." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Алдында жүргізген Akonadi-ді басқару қателер журналы табылмады" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi басқару процесі өткен жегілген ретте ешбір қатеге ұшырағанын " +"хабарлаған жоқ." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Алдында жүргізген Akonadi-ді басқару қателер журналы табылды." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi басқару процесі өткен жегілген ретте қатеге ұшырағанын хабарлады. " +"Журналын %1 дегенде таба аласыз." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi root-әкімшінің атынан жегілген" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Интернетке шығатын бағдарламаны root/әкімшінің атынан жегу - қөп " +"тәуекелдерге бару. Осындағы Akonadi қолданатын MySQL, сол тәуекелдерден " +"сақтап, өзін root атынан жегуге бермейді." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi root атынан жегілмеген" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Жүйе қауіпсіздігін сақтау талабына сай, Akonadi root/әкімші пайдаланушының " +"атынан жегілмеген." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Сынақ хабарлауын сақтау" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Қате." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "'%1' файлы ашылмады" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi сервері жегу кезінде бір қатеге тап болды. Іле жасалатын өз-өзін " +"сынау мәселесін тауып, оны шешуге сеп болар. Көмек сұрағанда не қате туралы " +"хабарлағанда осы сынақ нәтижесін қоса жіберіңіз." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Егжей-тегжейі" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Мәселе шеешетін қенестер үшін userbase.kde.org/Akonadi дегенді қараңыз.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Жаңа қапшық..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Жаңа" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "%1 қапшықты ө&шіру" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Өшіру" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "%1 қапшықты қ&дамдастыру" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Қадамдастыру" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Қапшықтың қа&сиеттері" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Қасиеттері" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Орналастыру" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Орналастыру" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Жергілікті жа&зылып алуын басқару..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Жергілікті жазылып алуын басқару" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Таңдамалы қапшықтарға қосу" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Таңдамалы қылу" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Таңдамалы қапшықтардан кетіру" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Таңдамалылардан өшіру" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Таңдамалылыны қайта атау" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Атауын өзгерту" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Қапшықты мынаған көшірмелеу..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Мынаған көшірмелеу" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Аталымды мынаған көшірмелеу..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Аталымды мынаған жылжыту..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Мынаған жылжыту" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Қапшықты мынаған жылжыту:..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "%1 аталымды қиып алу" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Қиып алу" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "%1 қапшықты қи&ып алу" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Дерек көзін құру" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "%1 дерек көзін кетіру" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Ресурс қасиеттері" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "%1 деректер көзін қадамдастыру" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Желіден тыс істеу" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Ішіндегісімен қоса &қадамдастыру" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Ішіндегі қапшықтарымен қоса қадамдастыру" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Қапшықты Шелекке &тастау" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Қапшықты Шелекке тастау" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Аталымды Шелекке &тастау" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Аталымды Шелекке тастау" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Қапшықты Шелектен қ&айтару" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Қапшықты Шелектен қайтару" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Аталымды Шелектен қ&айтару" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Аталымды Шелектен қайтару" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Жинақты Шелектен қ&айтару" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Жинақты Шелектен қайтару" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Таңдамалы қапшықтарын қ&дамдастыру" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Таңдамалы қапшықтарын қдамдастыру" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize Folder Tree" +msgstr "%1 қапшықты қадамдастыру" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "%1 қапшықты көшіріп алу" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "%1 аталымды көшіріп алу" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "%1 аталымды қиып алу" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "%1 қапшықты қи&ып алу" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "%1 аталымды &өшіру" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "%1 қапшықты ө&шіру" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "%1 қапшықты қ&дамдастыру" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "%1 дерек көзін &өшіру" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "%1 деректер көзін қа&дамдастыру" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "%1 қапшықты көшіріп алу" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "%1 аталымды көшіріп алу" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "%1 аталымды қиып алу" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "%1 қапшықты қиып алу" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "%1 аталымды өшіру" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "%1 қапшықты өшіру" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "%1 қапшықты қадамдастыру" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "%1 дерек көзін кетіру" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "%1 деректер көзін қадамдастыру" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Атауы" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Шынымен %1 қапшығын, олардын ішіндегісімен бірге өшірмексіз бе?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Өшіру керек пе?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Мынау қапшығын өшіруі болмады: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Қапшықты өшіру жаңылысы" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "%1 қапшығының қасиеттері" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "%1 аталымды өшірмексіз бе?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Нысандарды өшіру керек пе? " + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "%1 деген аталымды өшіруі болмады." + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Нысанды өшіру жаңылысы" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Таңдамалылыны қайта атау" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Атауы:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Жаңа ресурс" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Мынау деректер көзін құрылмады: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Деректер көзін құру жаңылысы" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Шынымен %1 ресурсты өшірмексіз бе?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Дерек көздерін кетіру керек пе?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Дерек орналастырылмады: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Орналастыру жаңылысы" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Қапшықтың атауына \"/\" деген қосылмайды." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Жаңа қапшық құру қатесі" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Қапшықтың атауының алдына не артына \".\" деген қосылмайды." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"\"%1\" қапшығын қадамдастыру үшін ресурс желіге қосылған болу керек. " +"Қосылсын ба?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "\"%1\" тіркелгісі желіден тыс" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Желіге кіру" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Осы қапшыққа жылжыту" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Осы қапшыққа көшірмелеу" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to update subscription: %1" +msgstr "Агент данасын құруға болмайды." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Жергілікті жазылып алу" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Жергілікті жазылып алу" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Іздеу" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Тек жазылғанның арасынан" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Жазылу" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Жазылудан айну" + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create a new tag" +msgstr "Агент данасын құруға болмайды." + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "An error occurred while creating a new tag" +msgstr "Аталымды құру қатесі: %1" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete this resource?" +#| msgid_plural "Do you really want to delete %1 resources?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Шынымен %1 ресурсты өшірмексіз бе?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "%1 аталымды өшіру" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "%1 аталымды өшіру" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Create new tag" +msgstr "Агент данасын құруға болмайды." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "%1 аталымды өшіру" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi-ден XML-ге аударғышы" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Akonadi бұтақ жинағын XML файлға аудару." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Дерек жүктелген жоқ." + +#: xml/xmldocument.cpp:123 +#, fuzzy, kde-format +#| msgid "No valid destination specified" +msgid "No filename specified" +msgstr "Қайда - дұрыс келтірілмеген" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to open data file '%1'." +msgstr "Агенттің '%1' түрі туралы мәлімет алынбады." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "%1 деген файл жоқ." + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to parse data file '%1'." +msgstr "Агенттің '%1' түрі туралы мәлімет алынбады." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Сұлба анықтамасы жүктеп талдауға келмеді." + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "Агент данасын құруға болмайды." + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "Агент данасын құруға болмайды." + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "Агент данасын құруға болмайды." + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Invalid item retrieved" +msgid "Invalid file format." +msgstr "Жарамсыз аталым алынды" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "Дерек орналастырылмады: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "Жарамсыз жинақ" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "Қашық ID" + +#~ msgid "MimeType" +#~ msgstr "MIME түрі" + +#~ msgid "Default Name" +#~ msgstr "Әдетті атауы" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Өшіру" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Бас тарту" + +#~ msgid "Take left one" +#~ msgstr "Сол жақтағысын алу" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Екі жаңартуы бір бірімен қайшылықта. Қайсын қолданбақсыз - таңдаңыз." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Оқылмаған" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Барлығы" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Өлшемі" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi деректер көзі" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Атауы" + +#~ msgid "Invalid collection specified" +#~ msgstr "Келтірілген жинақ дұрыс емес" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Протоколдың %1 нұсқасы табылды, керегі кемінде - %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Сервер протоколының нұсқасы ескірмеген." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Сервер протоколының нұсқасы - %1, ол кемінде керегінен (%2) жаңа немесе " +#~ "оған тең." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Жергілікті жинақ бұтағында қателігі табылды." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "Қашықтағы жинақ түбірсіз берілді - дерек көзі бүлінген." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE сынақ бағдарламасы" + +#~ msgid "Cannot list root collection." +#~ msgstr "Түбір жинағы тізімделмеді." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk іздеу қызметі D-Bus-та тіркелді." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Nepomuk іздеу қызметі D-Bus-та тіркелді, бұл, әдетте, ол іске жарамды " +#~ "дегені." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk іздеу қызметі D-Bus-та тіркеуден өтпеді." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Nepomuk іздеу қызметі D-Bus-та тіркеуден өтпеді. Бұл, әдетте, ол " +#~ "жегілмеді, не жегілгенде түзелмейтін қатеге тап болғанын көрсетеді." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuk іздеу қызметі іске лайықсыз серверін пайдаланады." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Nepomuk іздеу қызметі '%1' деген, Akonadi-мен істеуге лайықсыз деп " +#~ "табылатын серверін пайдаланады." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuk іздеу қызметі іске лайықты серверін пайдаланады. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Nepomuk іздеу қызметі, Akonadi-мен істеуге лайықnты деп табылатын бір " +#~ "серверін пайдаланады." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "\"%1\" плагині статикалық түрде құрылмаған, бұл мәліметті қате туралы " +#~ "хабарламада келтіріңіз." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Плагин статикалық түрде құрылмаған" + +#~ msgid "Fetch Job Error" +#~ msgstr "Қабылдау тапсырма қатесі" diff --git a/po/km/akonadi_knut_resource.po b/po/km/akonadi_knut_resource.po new file mode 100644 index 0000000..28e0be0 --- /dev/null +++ b/po/km/akonadi_knut_resource.po @@ -0,0 +1,82 @@ +# translation of akonadi_knut_resource.po to Khmer +# Khoem Sokhem , 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-11-29 09:02+0700\n" +"Last-Translator: Khoem Sokhem \n" +"Language-Team: Khmer \n" +"Language: km\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "គ្មាន​ឯកសារ​ទិន្នន័យ​ដែល​បាន​ជ្រើស​ទេ ។" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "បាន​ផ្ទុក​ឯកសារ '%1' ដោយ​ជោគជ័យ ។" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "ជ្រើស​ឯកសារ​ទិន្នន័យ" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "ឯកសារ​ទិន្នន័យ Akonadi Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "រក​មិនឃើញ​ធាតុ​សម្រាប់ remoteid %1 ទេ" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "សម្រាំង​មេ​រកមិនឃើញ​នៅ​ក្នុង​មែកធាង DOM ទេ ។" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "មិនអាច​សរសេរ​​សម្រាំង​បាន​ទេ ។" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "សម្រាំង​ដែលបានកែប្រែ​រក​មិនឃើញ​នៅ​ក្នុង​មែកធាង DOM ទេ ។" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "សម្រាំង​ដែល​បាន​លុប​រក​មិនឃើញ​នៅ​ក្នុង​មែកធាង DOM ទេ ។" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "សម្រាំង​មេ '%1' រក​មិនឃើញ​នៅ​ក្នុង​មែកធាង DOM ទេ ។" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "មិនអាច​សរសេរ​ធាតុ​បាន​ទេ ។" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "ធាតុ​ដែលបានកែប្រែ​រក​មិនឃើញ​នៅ​ក្នុង​មែកធាង DOM ទេ ។" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "ធាតុ​ដែល​បានលុប​រក​មិនឃើញ​នៅ​ក្នុង​មែកធាង DOM នោះ​ទេ ។" diff --git a/po/km/libakonadi5.po b/po/km/libakonadi5.po new file mode 100644 index 0000000..8980e5f --- /dev/null +++ b/po/km/libakonadi5.po @@ -0,0 +1,2765 @@ +# translation of libakonadi.po to Khmer +# Khoem Sokhem , 2008, 2009, 2010, 2012. +# Auk Piseth , 2008. +# Morn Met, 2009. +# Seng Sutha , 2010. +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2012-07-11 10:23+0700\n" +"Last-Translator: Khoem Sokhem \n" +"Language-Team: Khmer\n" +"Language: km\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: KBabel 1.11.4\n" +"X-Language: km-KH\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "ខឹម សុខែម, ម៉ន ម៉េត, សេង សុត្ថា" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "khoemsokhem@khmeros.info,​​mornmet@khmeros.info,sutha@khmeros.info" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 នៃ​ប្រភេទ %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "គ្រឿង​សម្គាល់​ភ្នាក់​ងារ" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "ភ្នាក់ងារ​របស់ Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "រួចរាល់​" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "ក្រៅបណ្ដាញ" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "ធ្វើសមកាលកម្ម..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "កំហុស ។" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "គ្រឿង​សម្គាល់​​ធនធាន​" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "ធនធាន​របស់ Akonadi​" + +#: agentbase/resourcebase.cpp:579 +#, fuzzy, kde-format +#| msgid "Invalid items passed" +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "បាន​ហុច​ធាតុ​ដែល​មិន​ត្រឹមត្រូវ" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ធ្វើ​បច្ចុប្បន្នភាព​​សម្រាំង​មូលដ្ឋាន​ ​៖ %1 ។" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ធ្វើ​បច្ចុប្បន្នភាព​​សម្រាំង​មូលដ្ឋាន​ ​៖ %1 ។" + +#: agentbase/resourcebase.cpp:718 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ធ្វើ​បច្ចុប្បន្នភាព​​សម្រាំង​មូលដ្ឋាន​ ​៖ %1 ។" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "មិនអាច​ទៅ​យក​ធាតុ​នៅ​ក្នុង​របៀប​ក្រៅបណ្ដាញ​បាន​ទេ ។" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "ធ្វើ​សមកាលកម្ម​ថត '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for sync." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ទៅ​ប្រមូល​យក​​​សម្រាំង​ធនធាន ។" + +#: agentbase/resourcebase.cpp:983 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for attribute sync." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ទៅ​ប្រមូល​យក​​​សម្រាំង​ធនធាន ។" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "គ្មាន​ធាតុ​ដែល​បាន​ទាមទារ​ទៀត​ឡើយ​​" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "គ្មាន​សម្រាំង​បែប​នេះ​ទេ ។​​" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "បាន​រក​ឃើញ​សម្រាំង​តែ​មួយ​​ដែល​មិន​ទាន់​ដោះស្រាយ" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "កុំ​​រក​ធាតុ​ផ្សេង​ទៀត​ សម្រាប់​ដោះ​ស្រាយ​ការ​ប៉ះ​ទង្គិច​" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "មិន​អាច​ចូល​ដំណើរ​ការ​ចំណុច​ប្រទាក់​ D-Bus នៃ​ភ្នាក់​ងារ​ដែល​បាន​បង្កើត​ទេ ។" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "អស់​ពេល​ក្នុង​ការ​បង្កើត​​​​ភ្នាក់​ងារ​ ។" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "មិន​អាច​ទទួល​យក​ប្រភេទ​ភ្នាក់ងារ '%1' បាន​ឡើយ ។" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "ធាតុ​សម្រាំង​មិន​ត្រឹមត្រូវ​ ។" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "ធាតុ​ធន​ធាន​មិន​ត្រឹម​ត្រូវ ។" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "មិន​អាច​​ទៅយក​​ចំណុច​ប្រទាក់ D-Bus សម្រាប់​ធន​ធាន '%1' បាន​ទេ​" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "អស់​ពេល​​ធ្វើ​សម​កាល​កម្ម​គុណ​លក្ខណៈ​​​សម្រាំង​ ។" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "សម្រាំង​មិន​ត្រឹម​ត្រូវ​" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "សម្រាំង​មិន​ត្រឹម​ត្រូវ​" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "មេ​ដែល​មិន​ត្រឹម​ត្រូវ​" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to parse Collection from response" +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ទៅ​ប្រមូល​យក​​​សម្រាំង​ធនធាន ។" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "សម្រាំង​មិន​ត្រឹម​ត្រូវ​" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "សម្រាំង​ដែល​ផ្ដល់​ឲ្យ​មិន​ត្រឹម​ត្រូវ ។​" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "គ្មាន​វត្ថុ​ដែល​បាន​បញ្ជាក់​សម្រាប់​ការ​ផ្លាស់​ទី​​ឡើយ​" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "គ្មាន​ទិស​ដៅ​ត្រឹមត្រូវ​​​បាន​បញ្ជាក់" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "សម្រាំង​មិន​ត្រឹម​ត្រូវ ។" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "សម្រាំង​មិន​ត្រឹម​ត្រូវ​" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "មិន​អាច​តភ្ជាប់​ទៅ​កាន់​សេវា Akonadi បាន​ទេ ។" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"កំណែ​ពិធីការ​របស់​ម៉ាស៊ីន​បម្រើ Akonadi មិន​ឆប​គ្នា​ទេ ។ សូម​ប្រាកដ​ថា អ្នក​​បាន​ដំឡើង​កំណែ​ដែល​ឆបគ្នា ។" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "ប្រតិបត្តិការ​ត្រូវ​បាន​បោះបង់​ដោយ​អ្នកប្រើ ។" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "កំហុស​ដែល​មិន​ស្គាល់ ។" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "អស់​ពេល​​ធ្វើ​សម​កាល​កម្ម​​ធន​ធាន ។​" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "មិន​អាច​ទៅ​ប្រមូល​យក​សម្រាំង​​ root នៃ​ធន​ធាន %1 បានទេ ។" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "គ្មាន​លេខ​សម្គាល់​ធនធាន​ផ្ដល់​ឲ្យ​ឡើយ​ ។" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "គ្រឿង​សម្គាល់​ធនធាន​មិន​ត្រឹម​ត្រូវ​ '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​កំណត់​រចនាសម្ព័ន្ធ​ធនធាន​លំនាំ​ដើម​តាម​រយៈ​ D-Bus ។​" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ទៅ​ប្រមូល​យក​​​សម្រាំង​ធនធាន ។" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "អស់​ពេល​ក្នុង​ការ​ព្យាយាម​ទទួល​យក​ការ​ជាប់​សោ ។" + +#: core/jobs/tagcreatejob.cpp:49 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create tag." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ផ្លាស់ទី​សម្រាំង​ធុងសំរាម សូម​បោះបង់​ប្រតិបត្តិការ​សម្អាត" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "បាន​ហុច​ធាតុ​ដែល​មិន​ត្រឹមត្រូវ" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "បាន​ហុច​សម្រាំង​ដែល​មិន​ត្រឹមត្រូវ" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "គ្មាន​សម្រាំង​ត្រឹមត្រូវ ឬ​បញ្ជី​ធាតុ​ទទេ" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "រក​មិន​ឃើញ​សម្រាំង​ដែល​បាន​ស្ដារ ឬ​មិន​មាន​ធនធាន​ដែល​បាន​ស្ដារ" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "ឈ្មោះ" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "កំពុង​ផ្ទុក..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "កំហុស ។" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"មាន​កា​រជ្រើសរើស​គោលដៅ '%1' ដែល​មាន​ឈ្មោះ\n" +" '%2' រួច​ហើយ ។" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "ឈ្មោះ​" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "មិន​អាច​ចម្លង​​ធាតុ​បាន​ឡើយ​ ៖​" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "មិន​អាច​ចម្លង​សម្រាំង​បាន​ឡើយ​ ៖" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "មិន​អាច​ផ្លាស់ទី​ធាតុ​បាន​ឡើយ​ ៖" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "មិន​អាច​ផ្លាស់​ទី​សម្រាំង​បាន​ឡើយ​ ៖" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "មិន​អាច​ភ្ជាប់​អង្គភាព​បាន​ឡើយ​ ៖​" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "កំហុស ។" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "ថត​សំណព្វ​" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "សារ​សរុប​" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "សារ​មិន​ទាន់​អាន​" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "កូតា​" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "ទំហំ​ផ្ទុក​" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "ទំហំ​ឧបករណ៍​ផ្ទុក​ថត​រង" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "មិន​ទាន់​អាន​" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "សរុប​" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "ទំហំ​" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "មិន​អាច​ទៅ​ប្រមូល​យក​ធាតុ​សម្រាប់​លិបិក្រម​បាន​ឡើយ​​" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "លិបិក្រម​មិន​អាច​រក​បាន​ទៀត​ឡើយ​​" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "ផ្នែក Payload '%1' សម្រាប់​លិបិក្រម​នេះ​មិន​អាច​រក​បាន​ឡើយ​" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "គ្មាន​សម័យ​​សម្រាប់​លិបិក្រម​នេះ​ឡើយ​" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "គ្មាន​ធាតុ​សម្រាប់​លិបិក្រម​នេះ​ឡើយ​" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "កម្មវិធី​ជំនួយ​គ្មាន​ឈ្មោះ" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "មិន​មាន​សេចក្ដី​អធិប្បាយ" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "ម៉ាស៊ីន​បម្រើ Akonadi សាកល្បង​ដោយ​ខ្លួន​ឯង" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "មិន​អាច​តភ្ជាប់​ទៅ​កាន់​សេវា Akonadi បាន​ទេ ។" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "ធាតុ​ភ្នាក់ងារ​ថ្មី​​..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "លុប​ធាតុ​​ភ្នាក់ងារ" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "កំណត់​រចនាសម្ព័ន្ធ​ធាតុ​​ភ្នាក់ងារ" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "ធាតុ​ភ្នាក់ងារ​ថ្មី​" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "មិន​អាច​បង្កើត​ធាតុ​​ភ្នាក់ងារ​បាន​ទេ​ ៖​​ %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "បាន​បរាជ័យ​ការ​បង្កើត​ធាតុ​​ភ្នាក់ងារ​" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "លុប​ធាតុ​​ភ្នាក់ងារ​ឬ​ ?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "តើ​អ្នក​ពិតជា​ចង់​លុប​ធាតុ​ភ្នាក់ងារ​ដែល​បាន​ជ្រើស​ឬ ?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "នាទី" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "ដែល​ទៅ​យក​" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "ប្រើ​ជម្រើស​ពី​ថត​មេ​ ឬ​គណនី​​" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "ធ្វើ​សម​កាល​កម្ម​ នៅ​ពេល​​ជ្រើស​ថត​នេះ​" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "ធ្វើ​សមកាលកម្ម​ដោយស្វ័យប្រវត្តិ​បន្ទាប់ពី ៖" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "មិនដែល" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "នាទី" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "ផ្នែក​ឃ្លាំង​សម្ងាត់​មូលដ្ឋាន" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "ជម្រើស​ដែល​​ទៅ​យក" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "ទៅ​យក​សារ​ពេញ​ជានិច្ច​​" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "ទៅ​យក​តួ​សារ​តាម​តម្រូវ​ការ​" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "រក្សា​ទុក​តួ​សារ​តាម​កន្លែង​សម្រាប់​ ៖​​" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "ជានិច្ច" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "" +#| "@info/plain Displayed grayed-out inside the textbox, verb to search" +#| msgid "Search" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "ស្វែងរក " + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "ថត​រង​​ថ្មី..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "បង្កើត​ថត​រង​ថ្មី​នៅ​ក្រោម​ថត​ដែល​បាន​ជ្រើស​បច្ចុប្បន្ន​​" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "​ថត​ថ្មី" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "ឈ្មោះ​" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "មិន​អាច​បង្កើត​ថត ៖ %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "កា​រ​បង្កើត​ថត​បាន​បរាជ័យ" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "ទូទៅ" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "វត្ថុ %1" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "ឈ្មោះ ៖" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "ប្រើ​រូបតំណាង​ផ្ទាល់ខ្លួន ៖" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "ថត" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "ស្ថិតិ" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "មាតិកា ៖" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "វត្ថុ ០" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "ទំហំ ៖" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "០ បៃ" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "លក្ខណសម្បត្តិ​ថត​" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "កាត់​ធាតុ %1 ​" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "សារ​សរុប​" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "សារ​មិន​ទាន់​អាន​" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "ថត​បច្ចុប្បន្ន" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "គ្មាន​ថត​" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "បើក​ប្រអប់​សម្រាំង" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "ជ្រើស​សម្រាំង​" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "ផ្លាស់ទី​នៅ​ទី​នេះ​" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "ចម្លង​ទីនេះ" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "បោះបង់" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "ពេលវេលា​កំណែប្រែ​" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "ទង់​" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "គុណ​លក្ខណៈ ៖​​ %1​" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "ប៉ះ​ទង្គិច​គុណភាព​ដោះស្រាយ​" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "យក​ពី​ស្ដាំ​មួយ​" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "រក្សាទុក​ទាំងពីរ​" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "ទិន្នន័យ​" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "កំពុង​ចាប់ផ្ដើម​ម៉ាស៊ីន​បម្រើ Akonadi​..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "កំពុង​បញ្ឈប់​ម៉ាស៊ីនបម្រើ Akonadi​..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "ផ្លាស់​ទី​នៅ​ទីនេះ​" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "ចម្លង​នៅ​ទី​នេះ​" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "ភ្ជាប់​នៅ​ទី​នេះ​" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "បោះបង់​" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "មិន​អាច​តភ្ជាប់​ទៅ​កាន់​សេវា Akonadi បាន​ទេ ។" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "សេវា​គ្រប់គ្រង​ព័ត៌មាន​ផ្ទាល់ខ្លួន​កំពុង​ចាប់ផ្ដើម​​..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "សេវា​គ្រប់គ្រង​ព័ត៌មាន​ផ្ទាល់ខ្លួន​កំពុង​បិទ​​...​" + +#: widgets/erroroverlay.cpp:241 +#, fuzzy, kde-format +#| msgid "Personal information management service is starting..." +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "សេវា​គ្រប់គ្រង​ព័ត៌មាន​ផ្ទាល់ខ្លួន​កំពុង​ចាប់ផ្ដើម​​..." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"សេវា​​គ្រប់គ្រង​ព័ត៌មាន​ផ្ទាល់ខ្លួនរបស់ Akonadi មិនកំពុង​​ដំណើរការ​ឡើយ​ ។ កម្ម​វិធី​នេះ​អាច​មិនត្រូវបាន​ប្រើ​ដោយ​" +"គ្មានវា​។" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "ចាប់​ផ្តើម​ " + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"គ្រោងការណ៍​គ្រប់គ្រង​ព័ត៌មាន​ផ្ទាល់ខ្លួន​របស់ Akonadi មិន​អាច​ប្រតិបត្តិ​បានទេ ។\n" +"ចុច​លើ \"សេចក្ដី​លម្អិត...\" ដើម្បី​ទទួល​ព័ត៌មាន​លម្អិត​​​អំពី​បញ្ហា​នេះ ។" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "សេវា​​គ្រប់គ្រង​ព័ត៌មាន​ផ្ទាល់ខ្លួន​របស់ Akonadi មិន​អាច​ប្រតិបត្តិ​បាន​ឡើយ​ ។" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "សេចក្ដី​លម្អិត​​..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "ចាប់​ផ្តើម​ " + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "ថត​បច្ចុប្បន្ន" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "ប្ដូរ​ឈ្មោះ​សំណព្វ​" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "ឈ្មោះ ៖​​" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "ម៉ាស៊ីន​បម្រើ Akonadi សាកល្បង​ដោយ​ខ្លួន​ឯង" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "រក្សាទុក​របាយការណ៍..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "ចម្លង​របាយការណ៍​ទៅ​ក្ដារតម្បៀតខ្ទាស់" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"កម្មវិធី​បញ្ជា​របស់​ QtSQL '%1' ត្រូវ​បាន​ទាម​ទារ​ដោយ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ម៉ាស៊ីន​បម្រើ​ Akonadi " +"បច្ចុប្បន្ន​របស់​អ្នក​ ហើយ​ត្រូវ​បាន​រក​ឃើញ​លើ​ប្រព័ន្ធ​របស់​អ្នក ។" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"កម្មវិធី​បញ្ជា QtSQL '%1' ត្រូវ​បានទាមទារការ​កំណត់​រចនាសម្ព័ន្ធ​ម៉ាស៊ីន​ Akonadi បច្ចុប្បន្ន​របស់​" +"អ្នក ។\n" +"កម្មវិធី​បញ្ជា​ខាងក្រោម​ត្រូវ​បាន​ដំឡើង ៖ %2 ។\n" +"សូម​ប្រាកដ​ថា​កម្មវិធី​បញ្ជា​ដែល​ត្រូវ​ការ​ត្រូវ​បាន​ដំឡើង ។" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "រក​ឃើញ​កម្មវិធី​បញ្ជា​មូលដ្ឋាន​ទិន្នន័យ ។" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "រក​មិនឃើញ​កម្មវិធី​បញ្ជា​មូលដ្ឋាន​ទិន្នន័យ ។" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "ម៉ាស៊ីនបម្រើ MySQL ដែលអាច​ប្រតិបត្តិ​បាន​មិនបានសាកល្បង​ទេ ។" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "ការ​កំណត់​រចនាសម្ព័ន្ធ​បច្ចុប្បន្ន​មិន​ទាមទារ​ម៉ាស៊ីនបម្រើ MySQL ខាង​ក្នុង​ទេ ។" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"បច្ចុប្បន្ន​អ្នក​បាន​កំណត់​រចនាសម្ព័ន្ធ Akonadi ដើម្បី​ប្រើ​ម៉ាស៊ីន​បម្រើ​ MySQL '%1' ។\n" +"សូម​ប្រាកដ​ថា​អ្នក​បាន​ដំឡើង​ម៉ាស៊ីន​បម្រើ​ MySQL បាន​កំណត់​ផ្លូវ​ត្រឹម​ត្រូវ​ និង​សូម​ប្រាកដ​ថា​អ្នក​មាន​សិទ្ធិ​ក្នុង​" +"ការ​អាន​ និង​ប្រតិបត្តិ​​ដែល​ចាំ​បាច់​​លើ​​ឯកសារ​ប្រតិបត្តិ​ម៉ាស៊ីន​បម្រើ​​ ។ ម៉ាស៊ីន​បម្រើ​ដែល​អាច​ប្រតិបត្តិ​បាន​ជា​ធម្មតា​" +"ហៅ​ថា 'mysqld' ទី​តាំង​របស់​វា​ខុស​គ្នា​អាស្រ័យ​ទៅ​លើ​ការចែក​ចាយ ។" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "រកមិនឃើញ​ម៉ាស៊ីនបម្រើ MySQL ។" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "ម៉ាស៊ីន​បម្រើ MySQL មិនអាច​​អាន ។" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "ម៉ាស៊ីន​បម្រើ MySQL មិនអាច​ប្រតិបត្តិបាន ។" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "រក​ឃើញ​ MySQL ដែល​មានឈ្មោះ​ដែល​មិនបាន​រំពឹង​ទុក ។" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "រក​ឃើញ​ម៉ាស៊ីន​បម្រើ MySQL ។" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "រក​ឃើញ​ម៉ាស៊ីនបម្រើ MySQL ៖ %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "ម៉ាស៊ីន​បម្រើ MySQL អាច​ប្រតិបត្តិ​បាន ។" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "បាន​បរាជ័យ​ក្នុង​កា​រ​ប្រតិបត្តិ​ម៉ាស៊ីន​បម្រើ MySQL '%1' ដោយ​មាន​សារ​កំហុស​ដូច​ខាងក្រោម ៖ '%2'​" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​ប្រតិបត្តិ​ម៉ាស៊ីន​បម្រើរបស់​ MySQL ។" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "​កំណត់ហេតុ​កំហុស​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL មិន​បាន​សាកល្បង​ឡើយ​ ។" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "រក​មិន​ឃើញ​កំណត់ហេតុ​កំហុស​របស់​ MySQL បច្ចុប្បន្ន ។" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"ម៉ាស៊ីន​បម្រើ MySQL ​មិន​បាន​រាយ​ការណ៍​ថា​មាន​កំហុស​កំឡុង​ពេល​ចាប់​ផ្ដើម​ ។​ កំណត់​ហេតុ​អាច​ត្រូវ​បាន​រក​ឃើញ​ក្នុង​​ " +"'%1' ។" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "កំណត់​ហេតុ​កំហុស​របស់​​ MySQL មិន​អាច​អាន​បាន​ទេ ។" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "រក​ឃើញ​ឯកសារ​កំណត់​ហេតុ​កំហុស​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL ប៉ុន្តែ​មិន​អាច​អាន​បានទេ ៖ %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "កំណត់​ហេតុ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL មានកំហុស ។" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "ឯកសារ​កំណត់​ហេតុ​​កំហុស​របស់​ម៉ាស៊ីន​ប្រើ​របស់​ MySQL '%1' មាន​កំហុស ។" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "កំណត់​ហេតុ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL មាន​ការ​ព្រមាន ។" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "ឯកសារ​កំណត់ហេតុ​របស់​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL '%1' មាន​ការ​ព្រមាន ។" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "កំណត់ហេតុ​របស់​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL មិន​មាន​កំហុស​​ទេ ។" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "ឯកសារ​កំណត់​ហេតុ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL '%1' មិន​មាន​កំហុស​ ឬ​ការ​ព្រមាន​ទេ ។" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "កា​រ​កំណត់​រចនាសម្ព័ន្ធ​របស់​ MySQL មិន​បាន​សាកល្បង​ទេ ។" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "រក​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​លំនាំដើម​​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL ទេ ។" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "រក​មិន​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​លំនាំដើម​សម្រាប់​​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL និង​អាច​អាន​បាន​​នៅ %1 ។" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "រក​មិន​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​លំនាំដើម​​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL ទេ ។" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"ការ​កំណត់​រចនាសម្ព័ន្ធ​លំនាំដើម ​សម្រាប់​ម៉ាស៊ីន​បម្រើ MySQL រក​មិន​ឃើញ ឬ​មិន​អាច​អាន​បាន ។ ពិនិត្យ​មើ​ល​ការ​" +"ដំឡើង Akonadi របស់​អ្នក​​ថា​បាន​បញ្ចប់ ហើយ​អ្នក​មាន​សិទ្ធិ​ចូល​ដំណើរការ​ដែល​ទាមទារ​​ទាំងអស់ ។" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "មិន​មាន​ការ​កំណត់​រចនាសម្ព័ន្ធ​ផ្ទាល់ខ្លួន​របស់​ម៉ាស៊ីន​បម្រើ MySQL ឡើយ​។​" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "​រក​មិន​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ផ្ទាល់ខ្លួន​ សម្រាប់​ម៉ាស៊ីន​បម្រើ MySQL ឡើយ​​ ប៉ុន្តែ​​ជា​ជម្រើស​​​ ។" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "រក​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ផ្ទាល់ខ្លួន​​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL ។" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "រក​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ផ្ទាល់ខ្លួន​សម្រាប់​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL និង​អាច​អាន​បាន​នៅ %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "ការ​កំណត់​រចនាសម្ព័ន្ធ​ផ្ទាល់ខ្លួន​​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL មិន​អាច​អាន​បាន​ឡើយ​ ។" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"រក​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ផ្ទាល់ខ្លួន​សម្រាប់​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL នៅ​ %1 ប៉ុន្តែ​មិន​អាច​អាន​បានទេ ។ " +"សូម​ពិនិត្យ​មើល​សិទ្ធិ​ចូល​ដំណើរការ​របស់​អ្នក ។" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "រក​មិន​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL ឬ​​មិន​អាច​អាន​បាន​ឡើយ​ ។" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "រក​មិន​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL ឬ​មិន​អាច​អាន​បាន​ឡើយ​ ។" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "ការ​កំណត់​រចនាសម្ព័ន្ធ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL អាច​ប្រើ​បាន ។" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "រក​​ឃើញ​ការ​កំណត់​រចនាសម្ព័ន្ធ​ម៉ាស៊ីន​បម្រើ​របស់​ MySQL នៅ​ %1 និង​អាច​អាន​បាន ។" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "មិន​អាច​តភ្ជាប់​ទៅ​ម៉ាស៊ីន​បម្រើ​របស់​​ PostgreSQL បាន​ឡើយ​ ។​​" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "រក​ឃើញ​ម៉ាស៊ីន​បម្រើ​របស់​​ PostgreSQL ឡើយ​ ។​" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "រក​ឃើញ​ម៉ាស៊ីន​បម្រើ​របស់​ PostgreSQL ឡើយ​ ហើយ​ការ​តភ្ជាប់​កំពុង​តែ​ដំណើរការ​ ។" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "រក​មិន​ឃើញ akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"កម្មវិធី 'akonadictl' ត្រូវ​ការ​ឲ្យ​ចូល​ដំណើរការ​នៅ​ក្នុង $PATH ។ សូម​ប្រាកដ​ថា អ្នក​បាន​ដំឡើង​ម៉ាស៊ីន​" +"បម្រើ​របស់​ Akonadi ហើយ​ ។" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "រក​ឃើញ akonadictl និង​អាច​ប្រើ​បាន" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"កម្មវិធី​ '%1' ដែល​ត្រូវ​ត្រួត​ពិនិត្យ​​ម៉ាស៊ីន​បម្រើ​របស់​​ Akonadi ត្រូវ​បាន​រក​ឃើញ​ ហើយ​អាច​ត្រូវ​ប្រតិបត្តិ​ដោយ​ជោគ​" +"ជ័យ ។\n" +"លទ្ធផល ​៖\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "រក​ឃើញ​ akonadictl ប៉ុន្តែ​មិន​អាច​ប្រើ​​បាន" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"កម្មវិធី​ '%1' ដែល​ត្រូវ​ត្រួត​ពិនិត្យ​ម៉ាស៊ីន​បម្រើ​របស់​​ Akonadi ត្រូវ​បាន​រក​ឃើញ ប៉ុន្តែ​មិន​អាច​ត្រូវ​ប្រតិបត្តិ​ដោយ​" +"ជោគ​ជ័យ​ឡើយ​ ។\n" +"លទ្ធផល​ ៖\n" +"%2\n" +"សូម​ប្រាកដ​ថា​ម៉ាស៊ីន​បម្រើ​របស់​ Akonadi ត្រូវ​បាន​ដំឡើង​យ៉ាង​ត្រឹម​ត្រូវ​ ។" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "ដំណើរ​ការ​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi ត្រូវ​​បាន​ចុះ​ឈ្មោះ​នៅ​ D-Bus ។" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"ដំណើរការ​​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi ត្រូវ​បាន​ចុះឈ្មោះ​នៅ D-Bus ដែល​ជា​ទូទៅ​បង្ហាញ​ថា​វា​អាច​ប្រតិបត្តិ​" +"បាន ។" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "ដំណើរការ​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi មិន​បាន​ចុះឈ្មោះ​នៅ D-Bus ឡើយ ។" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"ដំណើរការ​​នៃ​វត្ថុ​បញ្ជា​របស់​​ Akonadi មិន​ត្រូវ​បាន​ចុះឈ្មោះ​នៅ​ D-Bus ដែល​ជា​ទូទៅ​មានន័យ​ថា​វា​​មិន​អាច​ត្រូវ​" +"បាន​ចាប់ផ្ដើម​ ឬ​ជួប​ប្រទះ​កំហុស​ធ្ងន់ធ្ងរ​នៅ​​ខណៈ​ពេល​ដែល​​ចាប់ផ្ដើម​ឡើយ​ ។" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "ដំណើរការ​ម៉ាស៊ីន​បម្រើ Akonadi បាន​ចុះឈ្មោះ​នៅ D-Bus ។" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"ដំណើរការ​ម៉ាស៊ីន​បម្រើ Akonadi ត្រូវ​បាន​ចុះឈ្មោះ​នៅ D-Bus ដែល​ជា​ទូទៅ​បង្ហាញ​ថា​វា​អាច​ប្រតិបត្តិ​បាន ។" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "ដំណើរការ​ម៉ាស៊ីន​បម្រើ Akonadi មិន​បាន​ចុះឈ្មោះ​នៅ D-Bus ទេ ។" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"ដំណើរការ​ម៉ាស៊ីន​បម្រើ Akonadi មិន​ត្រូវ​​បាន​ចុះឈ្មោះ​នៅ​ D-Bus ដែល​ជា​ទូទៅ​មាន​ន័យ​ថា​វា​មិន​ត្រូ​វ​បាន​" +"ចាប់ផ្ដើម ឬ​ជួប​ប្រទះ​កំហុស​ធ្ងន់ធ្ងរ​នៅ​ពេល​ចាប់ផ្ដើម ។" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "មិន​អាច​ពិនិត្យ​មើល​កំណែ​របស់​ពិធីការ​បាន​ទេ ។" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "ដោយ​គ្មានការ​តភ្ជាប់​ទៅ​កាន់​ម៉ាស៊ីនបម្រើ វា​មិនអាច​ពិនិត្យ​មើល​កំណែ​ពិធីការ​ត្រូវ​តាម​បំណង​បាន​ទេ ។" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "កំណែ​ពិធីការ​ម៉ាស៊ីន​​បម្រើ​ចាស់​ពេក ។" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"កំណែ​ពិធីការ​ម៉ាស៊ីន​បម្រើ %1 ប៉ុន្តែ​យ៉ាង​ហោច​ណាស់​កំណែ​ %2 ត្រូវ​បាន​ទាមទារ​ ។ ដំឡើង​កំណែ​​របស់​ម៉ាស៊ីន​បម្រើ " +"Akonadi ដែល​ថ្មី​ជាង​នេះ ។" + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "កំណែ​ពិធីការ​ម៉ាស៊ីន​​បម្រើ​ចាស់​ពេក ។" + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "កំណែ​ពិធីការ​ម៉ាស៊ីន​​បម្រើ​ចាស់​ពេក ។" + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "កំណែ​ពិធីការ​ម៉ាស៊ីន​​បម្រើ​ចាស់​ពេក ។" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "រក​ឃើញ​ភ្នាក់ងារ​ធនធាន ។" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "យ៉ាង​ហោច​ណាស់​មាន​ភ្នាក់​ងារ​​ធន​ធាន​មួយ​ត្រូវ​បាន​រក​ឃើញ ។​" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "រក​មិន​ឃើញ​ភ្នាក់ងារ​ធនធាន ។" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"រក​មិន​ឃើញ​ភ្នាក់ងារ​ធនធាន​របស់​ Akonadi មិន​អាច​ប្រើ​ដោយ​គ្មាន​យ៉ាង​ហោច​ណាស់​មួយ​នោះ​ទេ ។ តាម​ធម្មតា​វា​" +"មាន​ន័យ​ថា​គ្មាន​ភ្នាក់ងារ​ធនធាន​ត្រូវ​បាន​ដំឡើង ឬ​​មាន​បញ្ហា​ក្នុង​កា​រ​រៀបចំ ។ ផ្លូវ​ដូច​ខាងក្រោម​ត្រូវ​បាន​" +"ស្វែងរក ៖ '%1' ។ អថេរ​បរិស្ថាន XDG_DATA_DIRS ត្រូវ​បាន​កំណត់​ទៅ '%2' សូម​ប្រាកដ​ថា វា​រួម​មាន​" +"ផ្លូវ​ទាំងអស់​ដែល​ភ្នាក់ងារ​របស់​ Akonadi ត្រូវ​បាន​ដំឡើង ។" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "រក​មិន​ឃើញ​កំណត់​ហេតុ​កំហុស​ម៉ាស៊ីន​បម្រើ​របស់​ Akonadi បច្ចុប្បន្ន​ទេ ។" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"ម៉ាស៊ីន​បម្រើ​របស់​​ Akonadi ​មិន​បាន​រាយការណ៍​កំហុស​ណា​មួយ​ក្នុង​កំឡុង​ពេល​នៃ​ការ​ចាប់ផ្ដើម​​​បច្ចុប្បន្ន​របស់​វា​ទេ ។​" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "រក​ឃើញ​​កំណត់​ហេតុ​កំហុស​ម៉ាស៊ីន​បម្រើ​របស់​​ Akonadi បច្ចុប្បន្ន ។​" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"ម៉ាស៊ីន​បម្រើ​របស់​​ Akonadi ​មិន​បាន​រាយការណ៍​កំហុស​ក្នុង​កំឡុង​ពេល​​នៃ​ការ​ចាប់ផ្ដើម​​បច្ចុប្បន្ន​របស់​វា​ទេ ។ កំណត់​" +"ហេតុ​ត្រូវ​បាន​រក​ឃើញ​ក្នុង​ %1 ។​" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "រក​មិនឃើញ​កំណត់ហេតុ​កំហុស​ម៉ាស៊ីនបម្រើ​របស់ Akonadi ពី​មុន ។" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "ម៉ាស៊ីនបម្រើ Akonadi មិនបាន​រាយការណ៍​ថា​មាន​កំហុស​ក្នុង​អំឡុង​ពេល​ចាប់ផ្ដើម​ពីមុន​របស់​វា​ទេ ។" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "រក​ឃើញ​កំណត់​ហេតុ​កំហុស​ម៉ាស៊ីន​បម្រើ​របស់​ Akonadi មុន ។" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"ម៉ាស៊ីន​បម្រើ​របស់​​ Akonadi បាន​រាយការណ៍​​កំហុស​កំឡុង​​ពេល​ការ​ចាប់​ផ្ដើម​ឡើង​​​ពី​មុន​ ។ ​ កំណត់​ហេតុ​អាច​ត្រូវ​បាន​រក​" +"ឃើញ​ក្នុង​ %1 ។" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "រក​មិន​ឃើញ​កំណត់ហេតុ​កំហុស​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi បច្ចុប្បន្ន​ទេ ។" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"ដំណើរការ​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi មិន​បាន​រាយការណ៍​ថា​មាន​កំហុស​កំឡុង​ពេល​​​ចាប់ផ្ដើម​ឡើង​បច្ចុប្បន្ន​របស់​វា​" +"ទេ ។" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "រក​ឃើញ​កំណត់​ហេតុ​កំហុស​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi បច្ចុប្បន្ន ។" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"ដំណើរការ​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi ​​បាន​រាយការណ៍​ថា​មាន​កំហុស​កំឡុង​​ពេល​​​ចាប់ផ្ដើម​​បច្ចុប្បន្ន​​របស់​វា​ទេ ។ " +"កំណត់ហេតុ​អាច​ត្រូវ​បាន​រក​ឃើញ​ក្នុង​ %1 ។" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "រក​មិន​ឃើញ​កំណត់ហេតុ​កំហុស​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi ពី​មុន ។" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"ដំណើរការ​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi មិន​បាន​រាយការណ៍​​ថា​មាន​កំហុស​ក្នុង​អំឡុង​ពេល​ចាប់ផ្ដើម​​​ពី​មុន​របស់​វា​ទេ ។" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "រក​ឃើញ​​កំណត់​ហេតុ​កំហុស​នៃ​វត្ថុ​បញ្ជា Akonadi ពី​មុន ។" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"ដំណើរ​ការ​នៃ​វត្ថុ​បញ្ជា​របស់​ Akonadi បាន​រាយការណ៍​​ថា​មាន​កំហុស​កំឡុង​ពេល​ចាប់​ផ្ដើម​​​ពី​មុន​របស់​វា ​។ កំណត់​ហេតុ​" +"អាច​ត្រូវ​បាន​រក​ឃើញ​​​ក្នុង​ %1 ។" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi បាន​ចាប់​ផ្ដើម​​ជា​ root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"ការ​ដំណើរ​ការ​កម្មវិធី​ប្រឈម​មុខ​អ៊ីនធឺណិត​ជា​​ root/administrator ដែល​ឃើញ​ថា​អ្នក​ត្រូវ​មាន​ហានិភ័យ​​​សុវត្ថិភាព​" +"ជាច្រើន​ ។ MySQL បាន​ប្រើ​ដោយ​ការដំឡើង​​ Akonadi នេះ​ នឹង​មិន​អនុញ្ញាត​ឲ្យ​ខ្លួន​វា​ដំណើរការ​ជា​ root " +"ដើម្បី​ការពារ​អ្នក​ពី​​ហានិភ័យ​ទាំងនេះ​​ ។" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi មិន​កំពុង​ដំណើរការ​ជា​​ root ឡើយ​" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi មិន​កំពុង​ដំណើរ​ការ​ក្នុងនាម​​ជា​អ្នក​ប្រើ​ root/administrator ដែល​រៀបចំ​ផ្ដល់​អនុសាសន៍​​ សម្រាប់​" +"ប្រព័ន្ធ​ដែល​ដែល​មាន​សុវត្ថិភាព​ទេ​ ។​" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "រក្សា​ទុក​របាយការណ៍​សាកល្បង​" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "កំហុស ។" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "មិនអាច​បើក​ឯកសារ '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"កំហុស​មួយ​បាន​កើត​ឡើង​អំឡុង​ពេល​ចាប់ផ្ដើម​ម៉ាស៊ីនបម្រើ Akonadi ។ ការ​សាកល្បង​ដោយ​ខ្លួន​ឯង​ដូច​ខាងក្រោម​ត្រូវ​" +"បានគិតថា​​​ ដើម្បីជួយតាមដាន​ និង​ដោះស្រាយ​បញ្ហា​នេះ ​​។ នៅពេល​ស្នើ​សុំ​កា​រគាំទ្រ ឬ​រាយការណ៍​កំហុស តែងតែ​រួម​" +"បញ្ចូល​របាយការណ៍​នេះ​ជា​និច្ច ។." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "សេចក្ដី​លម្អិត" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

សម្រាប់​​ព័ត៌មាន​ជំនួយ​​ដោះស្រាយ​បន្ថែម​សូម​មើល userbase.kde.org/Akonadi ។

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "ថត​ថ្មី..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "ថ្មី​" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "លុប​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "លុប​" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "​ធ្វើ​សម​កាល​កម្ម​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "ធ្វើ​សម​កាល​កម្ម​" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "លក្ខណសម្បត្តិ​ថត​" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "លក្ខណសម្បត្តិ​" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "បិទភ្ជាប់" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "បិទភ្ជាប់​" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "គ្រប់​គ្រង​ការ​ជាវ​ប្រចាំ​ជា​មូលដ្ឋាន​..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "គ្រប់​គ្រង​ការ​ជាវ​ប្រចាំ​ជា​​មូលដ្ឋាន​​..." + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "បន្ថែម​ទៅ​ថត​សំណព្វ​" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "បន្ថែម​ទៅកាន់​សំណព្វ​" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "យក​ចេញ​ពី​ថត​សំណព្វ​" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "យក​ចេញ​ពី​​សំណព្វ" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "ប្ដូរ​ឈ្មោះ​សំណព្វ..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "ប្ដូរ​ឈ្មោះ​" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "ចម្លង​ថត​ទៅ...​" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "ចម្លង​ទៅកាន់​" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "ចម្លង​ធាតុ​ទៅ...​" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "ផ្លាស់​ទី​ធាតុ​ទៅ...​" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "ផ្លាស់ទី​ទៅកាន់​" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "ផ្លាស់​ទី​ថត​ទៅ...​" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "កាត់​ធាតុ %1 ​" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "កាត់​" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "កាត់​ថត %1 ​" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "បង្កើត​ធនធាន​" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "​លុប​ធនធាន​​ %1 " + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "លក្ខណ​សម្បត្តិ​ធនធាន​" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "ធ្វើ​សម​កាល​កម្ម​ធនធាន​​ %1" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "កិច្ច​ការ​ក្រៅ​បណ្ដាញ" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "ធ្វើ​សម​កាល​កម្ម​ថត​ដោយ​​សរសេរ​ជាន់​ឡើង​វិញ" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "ធ្វើ​សម​កាល​កម្ម​ដោយ​សរសេរ​ជាន់​ឡើង​វិញ​" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "ផ្លាស់ទី​ទៅ​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "ផ្លាស់ទី​ថត​ទៅ​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "ផ្លាស់ទី​ធាតុ​ទៅ​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "ផ្លាស់ទី​ធាតុ​ទៅ​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "ស្ដារ​ថត​ពី​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "ស្ដារ​ថត​ពី​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "ស្ដារ​ធាតុ​ពី​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "ស្ដារ​ធាតុ​ពី​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "ស្ដារ​សម្រាំង​ពី​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "ស្ដារ​សម្រាំង​ពី​ធុងសំរាម" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "ធ្វើ​សមកាលកម្ម​ថត​សំណព្វ" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "ធ្វើ​សមកាលកម្ម​ថត​សំណព្វ" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize Folder Tree" +msgstr "ធ្វើ​​សម​កាលកម្ម​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "ចម្លង​ថត %1" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "ចម្លង​ធាតុ %1" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "កាត់​ធាតុ %1 ​" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "កាត់​ថត %1 ​" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "លុប​ធាតុ %1" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "លុប​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "​ធ្វើ​សម​កាល​កម្ម​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "លុប​ធនធាន​​ %1" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "ធ្វើ​សម​កាល​កម្ម​ធនធាន​​ %1" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "ចម្លង​ថត %1​" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "ចម្លង​ធាតុ %1​" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "កាត់​ធាតុ %1 ​" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "កាត់​ថត %1 ​" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "លុប​ធាតុ %1​" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "លុប​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "ធ្វើ​​សម​កាលកម្ម​ថត​​ %1" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "​លុប​ធនធាន​​ %1 " + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "ធ្វើ​សម​កាល​កម្ម​ធនធាន​​ %1" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "ឈ្មោះ" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "តើ​អ្នក​ពិត​ជា​ចង់​លុប​ថត '%1' និង​ថត​រង​របស់​វា​ទាំងអស់​ឬ ?​" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "លុប​ថត​ឬ​ ?​​" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "មិន​អាច​លុប​ថត ៖ %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "កា​រលុបថត​បាន​បរាជ័យ" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "លក្ខណ​សម្បត្តិ​នៃ​ថត​ %1​" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "តើ​អ្នក​ពិត​ជា​ចង់​លុប​ធាតុ​​ %1 ឬ​ ?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "លុប​​ធាតុ​ឬ​ ?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "មិន​អាច​លុប​ថត ៖ %1​" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "បាន​បរាជ័យ​ការ​លុប​ធាតុ​" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "ប្ដូរ​ឈ្មោះ​សំណព្វ​" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "ឈ្មោះ ៖​​" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "ធនធាន​ថ្មី​" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "មិន​អាច​បង្កើត​ធនធាន​ ៖​​ %1 បាន​ឡើយ​" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "បាន​បរាជ័យ​ការ​បង្កើត​ធនធាន​" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "តើ​អ្នក​ពិតជា​ចង់​លុប​ធនធាន​​ %1 ឬ ?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "លុប​ធនធាន​ឬ​ ?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "មិនអាច​បិទភ្ជាប់​​ទិន្នន័យ ៖ %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "បាន​បរាជ័យ​ក្នុង​ការ​បិទភ្ជាប់" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "យើង​មិន​អាច​បន្ថែម \"/\" ទៅ​ក្នុង​ឈ្មោះ​ថត​បាន​ទេ ។" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "បង្កើត​កំហុស​ថត​ថ្មី" + +#: widgets/standardactionmanager.cpp:756 +#, fuzzy, kde-format +#| msgid "We can not add \"/\" in folder name." +msgid "We can not add \".\" at begin or end of folder name." +msgstr "យើង​មិន​អាច​បន្ថែម \"/\" ទៅ​ក្នុង​ឈ្មោះ​ថត​បាន​ទេ ។" + +#: widgets/standardactionmanager.cpp:995 +#, fuzzy, kde-format +#| msgid "" +#| "Before to sync folder \"%1\" it's necessary to have resource online. Do " +#| "you want to make it online?" +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"មុន​នឹង​ធ្វើ​សមកាលកម្ម​ថត \"%1\" ចាំបាច់​ត្រូវតែ​មាន​ធនធាន​នៅ​លើ​បណ្ដាញ ។ តើ​អ្នក​ចង់​បង្កើត​ធនធាន​នៅ​លើ​" +"បណ្ដាញ​ដែរ​ឬ​ទេ ?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "គណនី \"%1\" នៅ​ក្រៅបណ្ដាញ" + +#: widgets/standardactionmanager.cpp:997 +#, fuzzy, kde-format +#| msgid "Work Offline" +msgctxt "@action:button" +msgid "Go Online" +msgstr "កិច្ច​ការ​ក្រៅ​បណ្ដាញ" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "ផ្លាស់​ទី​​ទៅ​​ថត​នេះ " + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "ចម្លង​ទៅ​ថត​នេះ​" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to update subscription: %1" +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "ការ​ជាវ​ប្រចាំ​ជា​មូលដ្ឋាន​" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "ការ​ជាវ​ប្រចាំ​ជា​មូលដ្ឋាន​" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "ស្វែងរក ៖​" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "តែ​អ្វី​ដែល​បាន​ជាវ​ប៉ុណ្ណោះ" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "Su&bscribe" +msgstr "តែ​អ្វី​ដែល​បាន​ជាវ​ប៉ុណ្ណោះ" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Unsubscribe" +msgstr "តែ​អ្វី​ដែល​បាន​ជាវ​ប៉ុណ្ណោះ" + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create a new tag" +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete this resource?" +#| msgid_plural "Do you really want to delete %1 resources?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "តើ​អ្នក​ពិតជា​ចង់​លុប​ធនធាន​​ %1 ឬ ?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "លុប​ធាតុ %1​" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "លុប​ធាតុ %1​" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Create new tag" +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "លុប​ធាតុ %1​" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, fuzzy, kde-format +#| msgid "No valid destination specified" +msgid "No filename specified" +msgstr "គ្មាន​ទិស​ដៅ​ត្រឹមត្រូវ​​​បាន​បញ្ជាក់" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to open data file '%1'." +msgstr "មិន​អាច​ទទួល​យក​ប្រភេទ​ភ្នាក់ងារ '%1' បាន​ឡើយ ។" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to parse data file '%1'." +msgstr "មិន​អាច​ទទួល​យក​ប្រភេទ​ភ្នាក់ងារ '%1' បាន​ឡើយ ។" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "មិន​អាច​បង្កើត​ធាតុ​ភ្នាក់ងារ​បាន​ឡើយ ។" + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgid "Invalid items passed" +msgid "Invalid file format." +msgstr "បាន​ហុច​ធាតុ​ដែល​មិន​ត្រឹមត្រូវ" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "មិនអាច​បិទភ្ជាប់​​ទិន្នន័យ ៖ %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "សម្រាំង​មិន​ត្រឹម​ត្រូវ​" + +#~ msgid "Id" +#~ msgstr "លេខ​សម្គាល់" + +#~ msgid "Remote Id" +#~ msgstr "លេខ​សម្គាល់​ពី​ចម្ងាយ" + +#~ msgid "MimeType" +#~ msgstr "MimeType" + +#~ msgid "Default Name" +#~ msgstr "ឈ្មោះ​លំនាំដើម" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "លុប​" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "បោះបង់" + +#~ msgid "Take left one" +#~ msgstr "យក​ពី​ឆ្វេង​មួយ​" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "ធ្វើ​បច្ចុប្បន្នភាព​ទាំងពីរ​ប៉ះទង្គិច​គ្នា​ ។​សូម​ជ្រើស​ធ្វើ​បច្ចុប្បន្នភាព​ណាមួយ​ ដើម្បី​អនុវត្ត ។" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "មិន​ទាន់​អាន" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "សរុប" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "ទំហំ" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "ធនធាន​របស់ Akonadi​" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "ឈ្មោះ​" + +#~ msgid "Invalid collection specified" +#~ msgstr "សម្រាំង​ដែល​បាន​បញ្ជាក់មិន​ត្រឹម​ត្រូវ" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "រក​ឃើញ​កំណែ​ពិធីការ %1 ដែល​រំពឹង​ទុក​ថា​យ៉ាង​ហោច​ណាស់ %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "កំណែ​ពិធីការ​ម៉ាស៊ីនបម្រើ​បច្ចុប្បន្ន​គ្រប់គ្រាន់​ហើយ ។" + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "កំណែ​ពិធីការ​ម៉ាស៊ីន​បម្រើ %1 ដែល​ស្មើ ឬ​ថ្មី​ជាង​កំណែ​ដែល​ត្រូវការ %2 ។" + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "បាន​រក​ឃើញ​មែក​ធាង​សម្រាំង​មូលដ្ឋាន​ដែល​អ​ស្ថេរ​ភាព​ ។" + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "សម្រាំង​ពី​ចម្ងាយ​ដោយ​គ្មាន​សែស្រ​ឡាយ​​ដែល​បញ្ចប់​​ដំណើរការ​​ root ដែល​ផ្ដល់​ឲ្យ ធនធាន​ត្រូវ​ខូច ។" + +#~ msgid "KDE Test Program" +#~ msgstr "កម្មវិធី​សាកល្បង​របស់ KDE​" + +#~ msgid "Cannot list root collection." +#~ msgstr "មិន​អាច​រាយ​សម្រាំង root បាន​ឡើយ ។" + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "សេវា​ស្វែង​​រក​របស់​​ Nepomuk ត្រូវ​បាន​ចុះ​ឈ្មោះ​នៅ​ D-Bus ។​" + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "សេវា​ស្វែង​រក​របស់​​ Nepomuk ត្រូវ​បាន​ចុះឈ្មោះ​នៅ D-Bus ដែល​ជាទូទៅ​បង្ហាញ​ថា​វា​អាច​ប្រតិបត្តិ​បាន " +#~ "។" + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "សេវា​​ស្វែង​រក​របស់​ Nepomuk មិន​ត្រូវ​បាន​ចុះ​ឈ្មោះ​នៅ​ D-Bus ឡើយ​ ។" + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "សេវា​ស្វែង​រក​របស់​​ Nepomuk មិន​ត្រូវ​​បាន​ចុះឈ្មោះ​នៅ​ D-Bus ដែល​ជា​ទូទៅ​មាន​ន័យ​ថា​វា​មិន​ត្រូវ​បាន​" +#~ "ចាប់ផ្ដើម ឬ​ជួប​ប្រទះ​កំហុស​ធ្ងន់ធ្ងរ​ខណៈ​ពេល​ដែល​​ចាប់ផ្ដើម​ឡើយ​ ។" + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "សេវា​ស្វែង​រក​របស់​​ Nepomuk ប្រើ​កម្មវិធី​ខាង​ក្រោយដែល​មិន​សម​ស្រប ។" + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "សេវា​ស្វែង​រក​​របស់​​ Nepomuk ប្រើ​កម្មវិធី​ខាង​ក្រោយ​ '%1' ដែល​មិន​ត្រូវ​បាន​ផ្ដល់​អនុ​សាសន៍​សម្រាប់​ការ​" +#~ "ប្រើ​ជាមួយ​ Akonadi ឡើយ ។" + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "សេវា​ស្វែង​រក​របស់​​ Nepomuk ប្រើ​កម្មវិធី​ខាង​ក្រោយ​ដែល​សម​​ស្រប​ ។" + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "សេវា​ស្វែង​រក​របស់​​ Nepomuk ប្រើ​កម្មវិធី​ខាង​ក្រោយ​ដែល​បាន​ផ្ដល់​អនុសាសន៍ ។​" + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "កម្មវិធី​ជំនួយ \"%1\" មិន​ស្ថិតស្ថេរ​ទេ សូម​បញ្ជាក់​ព័ត៌មាន​នេះ​នៅ​ក្នុង​របាយការណ៍​កំហុស ។" + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "កម្មវិធី​ជំនួយ​មិន​ស្ថិតស្ថេរ​ទេ" + +#~ msgid "Fetch Job Error" +#~ msgstr "កំហុស​ក្នុង​ការ​ទៅ​ប្រមូល​យក​ការងារ" diff --git a/po/ko/akonadi_knut_resource.po b/po/ko/akonadi_knut_resource.po new file mode 100644 index 0000000..073851d --- /dev/null +++ b/po/ko/akonadi_knut_resource.po @@ -0,0 +1,83 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Shinjo Park , 2010. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-07-07 00:08+0900\n" +"Last-Translator: Shinjo Park \n" +"Language-Team: Korean \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "데이터 파일을 선택하지 않았습니다." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "파일 '%1'을(를) 불러왔습니다." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "데이터 파일 선택" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut 데이터 파일" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "remoteid %1인 항목을 찾을 수 없음" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM 트리에서 부모 모음집을 찾지 못했습니다." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "모음집에 쓸 수 없습니다." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM 트리에서 수정한 모음집을 찾지 못했습니다." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM 트리에서 삭제한 모음집을 찾지 못했습니다." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM 트리에서 부모 모음집 '%1'을(를) 찾지 못했습니다." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "항목을 쓸 수 없습니다." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM 트리에서 수정한 항목을 찾지 못했습니다." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM 트리에서 삭제한 항목을 찾지 못했습니다." diff --git a/po/ko/libakonadi5.po b/po/ko/libakonadi5.po new file mode 100644 index 0000000..674b152 --- /dev/null +++ b/po/ko/libakonadi5.po @@ -0,0 +1,2600 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Shinjo Park , 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-05-17 00:11+0200\n" +"Last-Translator: Shinjo Park \n" +"Language-Team: Korean \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Lokalize 20.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "박신조" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kde@peremen.name" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "계정을 설정하지 않았습니다." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "계정 통합이 지원되지 않음" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "DBus에 객체를 등록할 수 없음: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1(%2 형식)" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "에이전트 식별자" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi 에이전트" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "준비" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "오프라인" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "동기화 중..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "오류." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "설정되지 않음" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "자원 식별자" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi 자원" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "잘못된 항목 가져옴" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "항목 생성 오류: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "모음집 갱신 오류: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "로컬 모음집 갱신 실패: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "로컬 항목 갱신 실패: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "오프라인 모드에서는 항목을 가져올 수 없습니다." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "'%1' 폴더 동기화 중" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "동기화할 모음집을 가져올 수 없습니다." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "속성 동기화할 모음집을 가져올 수 없습니다." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "요청한 항목이 더 이상 없음" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "작업이 취소되었습니다." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "모음집이 없습니다." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "부모를 찾을 수 없는 고아 모음집 찾음" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "충돌을 해결할 다른 항목을 찾을 수 없음" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "생성한 에이전트의 DBus 인터페이스에 접근할 수 없습니다." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "에이전트 생성 시간이 초과되었습니다." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "에이전트 종류 '%1'을(를) 가져올 수 없습니다." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "에이전트 인스턴스를 만들 수 없습니다." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "잘못된 모음집 인스턴스입니다." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "잘못된 자원 인스턴스입니다." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "자원 '%1'의 DBus 인터페이스를 가져올 수 없음" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "모음집 속성 동기화 시간이 초과되었습니다." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "잘못된 복사할 모음집" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "잘못된 대상 모음집" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "잘못된 부모" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "응답에서 모음집을 처리할 수 없음" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "잘못된 모음집" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "잘못된 모음집이 지정되었습니다." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "이동할 객체가 지정되지 않음" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "올바른 대상이 지정되지 않음" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "잘못된 모음집입니다." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "잘못된 부모 모음집" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Akonadi 서비스에 연결할 수 없습니다." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi 서버 프로토콜 버전이 호환되지 않습니다. 호환되는 버전이 설치되어 있는" +"지 확인하십시오." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "사용자가 작업을 취소했습니다." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "알 수 없는 오류입니다." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "예상하지 못한 응답" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "관계를 만들 수 없습니다." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "자원 동기화 시간이 초과되었습니다." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "자원 %1의 루트 모음집을 가져올 수 없습니다." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "자원 ID가 주어지지 않았습니다." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "잘못된 자원 식별자 '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "DBus를 통하여 기본 자원을 설정할 수 없습니다." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "자원 모음집을 가져올 수 없습니다." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "잠금을 시도하는 중 시간이 초과되었습니다." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "태그를 만들 수 없습니다." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "휴지통으로 모음집을 옮길 수 없음, 휴지통 버리기를 중단함" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "잘못된 항목이 전달됨" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "잘못된 모음집이 전달됨" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "올바른 모음집이 없거나 항목 목록이 비어 있음" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "복원 모음집을 찾을 수 없으며 복원 자원을 사용할 수 없음" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "이름" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "불러오는 중..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "오류" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"모음집 '%1'이(가) 이미 하위 모음집\n" +"'%2'을(를) 포함합니다." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "이름" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "항목을 복사할 수 없음: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "모음집을 복사할 수 없음: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "항목을 이동할 수 없음: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "모음집을 이동할 수 없음: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "엔티티를 링크할 수 없음: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "오류" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "즐겨찾는 폴더" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "총 메시지" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "읽지 않은 메시지" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "할당량" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "저장소 크기" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "하위 폴더 저장소 크기" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "읽지 않음" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "합계" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "크기" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "태그" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "색인에 추가할 항목을 가져올 수 없음" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "인덱스를 사용할 수 없음" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "이 인덱스의 내용 부분 '%1'을(를) 사용할 수 없음" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "인덱스에 해당하는 세션 없음" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "인덱스에 해당하는 항목 없음" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "이름 없는 플러그인" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "설명 없음" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi 서버의 프로토콜과 이 프로그램에서 사용하는 프로토콜의 버전이 서로 다" +"릅니다.\n" +"시스템을 최근에 업데이트했다면 모든 프로그램이 올바른 프로토콜 버전을 사용하" +"도록 로그아웃한 후 다시 로그인하십시오." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"사용 가능한 Akondai 에이전트가 없습니다. KDE PIM 설치 상태를 확인하십시오." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"프로토콜 버전이 일치하지 않습니다. 서버 쪽 버전(%1)이 클라이언트 쪽 버전(%2)" +"보다 낮습니다. 시스템을 업데이트했다면 Akonadi 서버를 다시 시작하십시오." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"프로토콜 버전이 일치하지 않습니다. 서버 쪽 버전(%1)이 클라이언트 쪽 버전(%2)" +"보다 높습니다. 시스템을 업데이트했다면 KDE PIM 프로그램을 다시 시작하십시오." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi 자가 진단" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi 서버 상태 점검 및 보고" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "새 에이전트 인스턴스(&N)..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "에이전트 인스턴스 삭제(&D)" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "에이전트 인스턴스 설정(&C)" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "새 에이전트 인스턴스" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "에이전트 인스턴스를 만들 수 없음: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "에이전트 인스턴스 생성 실패" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "에이전트 인스턴스를 삭제하시겠습니까?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "선택한 에이전트 인스턴스를 삭제하시겠습니까?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 설정" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 도움말" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "%1 정보" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "다른 창에서 설정 대화 상자를 열었음" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "다른 곳에서 %1 설정을 열었습니다." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "%1 설정 대화 상자를 등록할 수 없습니다." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "분" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "가져오기" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "부모 폴더나 계정 설정 사용" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "이 폴더를 선택할 때 동기화" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "다음 시간 이후 자동 동기화:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "하지 않음" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "분" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "로컬에 캐시된 부분" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "가져오기 옵션" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "항상 전체 메시지 가져오기(&M)" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "필요한 때 메시지 본문 가져오기(&R)" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "로컬 메시지 본문 유지 시간:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "영구" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "찾기" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "기본으로 사용할 폴더" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "새 하위 폴더(&N)..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "현재 선택된 폴더 아래에 새 하위 폴더 만들기" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "새 폴더" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "이름" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "폴더를 만들 수 없음: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "폴더 생성 실패" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "일반" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "객체 %1개" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "이름(&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "사용자 정의 아이콘 사용(&U):" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "폴더" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "통계" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "내용:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "객체 0개" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "크기:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0바이트" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "색인 작업은 시간이 걸릴 수도 있습니다." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "관리" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "색인된 항목 개수를 가져오는 중 오류 발생" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "이 폴더의 항목 %1개를 색인에 추가함" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "색인된 항목 계산 중..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "파일" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "폴더 종류:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "알 수 없음" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "항목" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "총 항목:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "읽지 않은 항목:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "색인 작업 중" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "전문 색인 사용" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "색인된 항목 개수 가져오는 중..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "폴더 색인 재생성" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "폴더 없음" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "모음집 대화 상자 열기" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "모음집 선택" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "여기로 이동(&M)" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "여기로 복사(&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "취소" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "수정 시간" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "플래그" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "속성: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "충돌 해결" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "내 버전 사용" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "상대방 버전 사용" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "둘 다 유지" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"다른 사람이 변경한 사항과 내 변경 사항이 충돌합니다.
하나의 버전을 포" +"기하지 않는다면, 충돌하는 변경 사항을 수동으로 합쳐야 합니다.
\"텍스트 편집기 열기\"를 눌러서 텍스트의 복사본을 유지" +"한 다음, 더 정확한 내용이 들어 있는 버전을 선택하고, 다시 열어서 빠진 내용을 " +"수정하십시오." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "데이터" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi 서버 시작 중..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi 서버 정지 중..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "여기로 이동(&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "여기로 복사(&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "여기로 링크(&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "취소(&A)" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"개인 정보 관리 서비스에 연결할 수 없습니다.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "개인 정보 관리 서비스를 시작하는 중..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "개인 정보 관리 서비스를 종료하는 중..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "개인 정보 관리 서비스에서 데이터베이스를 업그레이드하고 있습니다." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"개인 정보 관리 서비스에서 데이터를 업그레이드하고 있습니다.\n" +"소프트웨어 업데이트 이후에 진행되며 성능 최적화에 필요합니다.\n" +"저장된 개인 정보의 양에 따라 시간이 걸릴 수도 있습니다." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi 개인 정보 관리 서비스가 실행 중이 아닙니다. 이 프로그램은 해당 서비" +"스 없이 사용할 수 없습니다." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "시작" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi 개인 정보 관리 프레임워크가 작동하지 않습니다.\n" +"\"자세히...\"를 누르면 문제의 자세한 정보를 볼 수 있습니다." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi 개인 정보 관리 서비스가 작동하지 않습니다." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "자세히..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "'%1' 계정을 삭제하시겠습니까?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "계정을 삭제하시겠습니까?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "수신 계정(하나 이상 필요함):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "추가(&D)..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "수정(&M)..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "삭제(&E)" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "다시 시작" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "최근 폴더" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "책갈피 이름 바꾸기" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "이름:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi 서버 자가 진단" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "보고서 저장..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "보고서를 클립보드에 복사" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Akonadi 서버 설정에 QtSQL 드라이버 '%1'이(가) 필요하며 시스템에서 찾았습니다." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Akonadi 서버 설정에 QtSQL 드라이버 '%1'이(가) 필요합니다.\n" +"다음 드라이버가 설치되어 있습니다: %2\n" +"드라이버 설치 상태를 확인하십시오." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "데이터베이스 드라이버를 찾았습니다." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "데이터베이스 드라이버를 찾지 못했습니다." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL 서버 실행 파일이 시험되지 않았습니다." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "현재 설정에는 내부 MySQL 서버가 필요하지 않습니다." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Akonadi에서 MySQL 서버 '%1'을(를) 사용하도록 설정했습니다.\n" +"MySQL 서버를 설정했고, 올바른 경로를 지정했으며 서버 실행 파일에 필요한 읽기 " +"및 실행 권한이 있는지 확인하십시오. 서버 실행 파일은 대개 'mysqld'로 불리며 " +"배포판에 따라서 설치 위치가 달라질 수 있습니다." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL 서버를 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL 서버를 읽을 수 없습니다." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL 서버를 실행할 수 없습니다." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL 서버의 이름이 예상한 것과 다릅니다." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL 서버를 찾았습니다." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL 서버 찾음: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL 서버를 실행할 수 있습니다." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"MySQL 서버 '%1'을(를) 실행한 결과 다음 오류 메시지를 반환했습니다: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL 서버를 실행할 수 없습니다." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL 서버 오류 기록이 확인되지 않았습니다." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "MySQL 오류 기록이 없습니다." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL 서버를 시작하는 중 오류가 없었습니다. '%1'에 기록이 저장되어 있습니다." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL 오류 기록을 읽을 수 없습니다." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "MySQL 오류 기록 파일을 찾았지만 읽을 수 없습니다: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL 서버 로그에 오류가 들어 있습니다." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL 서버 오류 로그 파일 '%1'에 오류가 들어 있습니다." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL 서버 로그에 경고가 들어 있습니다." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL 서버 로그 파일 '%1'에 경고가 들어 있습니다." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL 서버 기록에 오류가 없습니다." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL 서버 기록 파일 '%1'에 오류나 경고가 없습니다." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL 서버 설정이 시험되지 않았습니다." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL 서버 기본 설정을 찾았습니다." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "기본 MySQL 서버 설정을 찾았고 %1에 있습니다." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL 서버 기본 설정을 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL 서버 기본 설정 파일을 찾거나 읽을 수 없습니다. Akonadi 설치 상태 및 필" +"요한 권한이 모두 있는지 확인하십시오." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL 서버 사용자 정의 설정이 없습니다." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "추가적인 MySQL 서버 사용자 정의 설정을 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL 서버 사용자 정의 설정을 찾았습니다." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "사용자 정의 MySQL 서버 설정을 찾았고 %1에 있습니다." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL 서버 사용자 정의 설정을 읽을 수 없습니다." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"사용자 정의 MySQL 서버 설정이 %1에 있지만 읽을 수 없습니다. 접근 권한을 확인" +"하십시오." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL 서버 설정을 찾거나 읽을 수 없습니다." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL 서버 설정을 찾거나 읽을 수 없습니다." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL 서버 설정을 사용할 수 있습니다." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL 서버 설정이 %1에 있으며 읽을 수 있습니다." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL 서버에 연결할 수 없습니다." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL 서버를 찾았습니다." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL 서버를 찾았고 연결할 수 있습니다." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl을 찾을 수 없음" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"$PATH에서 프로그램 'akonadictl'을 찾을 수 있어야 합니다. Akonadi 서버 설치 상" +"태를 확인하십시오." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl을 찾았고 사용할 수 있음" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi 서버를 제어하는 프로그램 '%1'을(를) 찾았고 실행할 수 있습니다.\n" +"결과:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl을 찾았지만 사용할 수 없음" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi 서버를 제어하는 프로그램 '%1'을(를) 찾았지만 실행할 수 없습니다.\n" +"결과:\n" +"%2\n" +"Akonadi 서버 설치 상태를 확인하십시오." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi 제어 프로세스가 DBus에 등록되었습니다." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "Akonadi 제어 서버가 DBus에 등록되었으며 작동함을 나타냅니다." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi 제어 프로세스가 DBus에 등록되지 않았습니다." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi 제어 서버가 DBus에 등록되지 않았으며 시작되지 않았거나 시작 중 오류" +"가 발생했음을 나타냅니다." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi 서버 프로세스가 DBus에 등록되었습니다." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "Akonadi 서버 프로세스가 DBus에 등록되었으며 작동함을 나타냅니다." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi 서버 프로세스가 DBus에 등록되지 않았습니다." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi 서버 프로세스가 DBus에 등록되지 않았으며 시작되지 않았거나 시작 중 오" +"류가 발생했음을 나타냅니다." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "프로토콜 버전을 확인할 수 없습니다." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "서버에 연결할 수 없어서 프로토콜 버전이 호환되는지 확인할 수 없습니다." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "서버 프로토콜 버전이 오래되었습니다." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"현재 서버 프로토콜 버전이 %1이지만 최소한 버전 %2이(가) 클라이언트에서 필요합" +"니다. KDE PIM을 업그레이드했다면 Akonadi와 KDE PIM 프로그램을 모두 다시 시작" +"하십시오." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "서버 프로토콜 버전이 너무 높습니다." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "서버 프로토콜 버전이 일치합니다." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "현재 프로토콜 버전은 %1입니다." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "자원 에이전트를 찾았습니다." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "최소한 하나의 자원 에이전트를 찾았습니다." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "자원 에이전트를 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"자원 에이전트를 찾을 수 없습니다. Akonadi가 작동하려면 최소한 하나의 자원 에" +"이전트가 필요합니다. 자원 에이전트가 설치되지 않았거나 설치 상황에 문제가 있" +"습니다. 다음 경로를 찾았습니다: '%1'. XDG_DATA_DIRS 환경 변수가 '%2'(으)로 설" +"정되어 있으며, Akonadi 에이전트가 설치되어 있는 경로가 들어 있는지 확인하십시" +"오." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "현재 Akonadi 서버 오류 기록을 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "현재 시작된 Akonadi 서버가 오류를 보고하지 않았습니다." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "현재 Akonadi 서버 오류 기록을 찾았습니다." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi 서버가 시작되는 중 오류를 보고했습니다. 오류 기록은 %1에 있습니다." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "이전 Akonadi 서버 오류 기록을 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "이전에 시작된 Akonadi 서버가 오류를 보고하지 않았습니다." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "이전 Akonadi 서버 오류 기록을 찾았습니다." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi 서버가 이전에 시작되는 중 오류를 보고했습니다. 오류 기록은 %1에 있습" +"니다." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "현재 Akonadi 제어 오류 기록을 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "현재 시작된 Akonadi 제어 프로세스가 오류를 보고하지 않았습니다." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "현재 Akonadi 제어 오류 기록을 찾았습니다." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi 제어 프로세스가 시작되는 중 오류를 보고했습니다. 오류 기록은 %1에 있" +"습니다." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "이전 Akonadi 제어 오류 기록을 찾을 수 없습니다." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "이전에 시작된 Akonadi 제어 프로세스가 오류를 보고하지 않았습니다." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "이전 Akonadi 제어 오류 기록을 찾았습니다." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi 제어 프로세스가 이전에 시작되는 중 오류를 보고했습니다. 오류 기록은 " +"%1에 있습니다." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi가 루트로 시작됨" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"인터넷에 접근할 수 있는 프로그램을 루트/관리자 권한으로 시작하면 보안 문제에 " +"노출될 수 있습니다. 이 Akonadi 설치본에 사용된 MySQL은 루트로 시작되지 않으므" +"로 이러한 위험에서 보호될 수 있습니다." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi가 루트로 시작되지 않음" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi가 루트/관리자로 시작되지 않았으며, 안전한 시스템용 권장 설정입니다." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "보고서 저장" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "오류" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "파일 '%1'을(를) 열 수 없음" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi 서버 시작 중 오류가 발생했습니다. 다음 자가 진단을 통하여 문제점을 발" +"견하고 해결할 수 있습니다. 버그를 보고하거나 지원을 요청할 때 다음 보고서를 " +"첨부하십시오." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "자세히" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

더 많은 문제 해결 정보를 보려면 userbase.kde.org/Akonadi를 방문하십시오.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "새 폴더(&N)..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "새로 만들기" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "폴더 삭제(&D)" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "삭제" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "폴더 동기화(&S)" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "동기화" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "폴더 속성(&P)..." + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "속성" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "붙여넣기(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "붙여넣기" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "로컬 구독 관리(&S)..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "로컬 구독 관리" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "즐겨찾는 폴더에 추가" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "책갈피에 추가" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "즐겨찾는 폴더에서 삭제" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "책갈피에서 삭제" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "책갈피 이름 바꾸기..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "이름 바꾸기" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "다음으로 폴더 복사..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "다음으로 복사" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "다음으로 항목 복사..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "다음으로 항목 이동..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "다음으로 이동" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "다음으로 폴더 이동..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "항목 잘라내기(&C)" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "잘라내기" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "폴더 잘라내기(&C)" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "자원 만들기" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "자원 삭제" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "자원 속성(&R)" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "자원 동기화" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "오프라인으로 작업" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "하위 폴더도 동기화(&S)" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "하위 폴더도 동기화" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "휴지통으로 폴더 이동(&M)" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "휴지통으로 폴더 이동" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "휴지통으로 항목 이동(&M)" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "휴지통으로 항목 이동" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "휴지통에서 폴더 복원(&R)" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "휴지통에서 폴더 복원" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "휴지통에서 항목 복원(&R)" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "휴지통에서 항목 복원" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "휴지통에서 모음집 복원(&R)" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "휴지통에서 모음집 복원" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "즐겨찾는 폴더 동기화(&S)" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "즐겨찾는 폴더 동기화" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "폴더 트리 동기화" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "폴더 %1개 복사(&C)" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "항목 %1개 복사(&C)" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "항목 %1개 잘라내기(&C)" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "폴더 %1개 잘라내기(&C)" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "항목 %1개 삭제(&D)" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "폴더 %1개 삭제(&D)" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "폴더 %1개 동기화(&S)" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "자원 %1개 삭제(&D)" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "자원 %1개 동기화(&S)" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "폴더 %1개 복사" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "항목 %1개 복사" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "항목 %1개 잘라내기" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "폴더 %1개 잘라내기" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "항목 %1개 삭제" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "폴더 %1개 삭제" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "폴더 %1개 동기화" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "자원 %1개 삭제" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "자원 %1개 동기화" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "이름" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "폴더 %1개와 하위 폴더를 삭제하시겠습니까?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "폴더 삭제?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "폴더를 삭제할 수 없음: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "폴더 삭제 실패" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "폴더 %1 속성" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "항목 %1개를 삭제하시겠습니까?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "항목 삭제?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "항목을 삭제할 수 없음: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "항목 삭제 실패" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "책갈피 이름 바꾸기" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "이름:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "새 자원" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "자원을 만들 수 없음: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "자원 생성 실패" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "자원 %1개를 삭제하시겠습니까?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "자원 삭제?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "데이터를 붙여넣을 수 없음: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "붙여넣기 실패" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "폴더 이름에 \"/\"를 추가할 수 없습니다." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "새 폴더 만들기 오류" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "폴더 이름의 시작이나 끝에 \".\"를 추가할 수 없습니다." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"폴더 \"%1\"을(를) 동기화하기 전 해당하는 자원을 온라인으로 전환해야 합니다. " +"온라인으로 전환하시겠습니까?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "계정 \"%1\"이(가) 오프라인임" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "온라인으로 전환" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "이 폴더로 이동" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "이 폴더로 복사" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "구독을 업데이트할 수 없음: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "구독 오류" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "로컬 구독" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "찾기:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "구독 중인 항목만(&S)" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "구독(&B)" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "구독 해제(&U)" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "새 태그를 만들 수 없음" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "새 태그를 만드는 중 오류가 발생함" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "태그 %1을(를) 삭제하시겠습니까?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "태그 삭제" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "태그 삭제" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "적용할 태그를 선택합니다." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "새 태그 만들기" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "태그 관리" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "태그 선택..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "태그 선택" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "지우기" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "태그를 추가하려면 누르십시오" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi To XML 변환기" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Akonadi 모음집 하위 트리를 XML 파일로 변환합니다." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "데이터를 불러오지 않았습니다." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "파일 이름이 지정되지 않음" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "데이터 파일 '%1'을(를) 열 수 없습니다." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "파일 %1이(가) 없습니다." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "데이터 파일 '%1'을(를) 처리할 수 없습니다." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "스키마 정의를 불러와서 처리할 수 없습니다." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "스키마 파서 콘텍스트를 만들 수 없습니다." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "스키마를 만들 수 없습니다." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "스키마 검증 콘텍스트를 만들 수 없습니다." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "파일 형식이 잘못되었습니다." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "데이터 파일을 처리할 수 없음: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "모음집 %1을(를) 찾을 수 없음" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "원격 ID" + +#~ msgid "MimeType" +#~ msgstr "MIME 형식" + +#~ msgid "Default Name" +#~ msgstr "기본 이름" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "삭제" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "취소" + +#~ msgid "Take left one" +#~ msgstr "왼쪽 사용" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "두 업데이트가 서로 충돌합니다.적용할 업데이트를 선택하십시오." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "읽지 않음" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "합계" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "크기" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi 자원" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "이름" + +#~ msgid "Invalid collection specified" +#~ msgstr "잘못된 모음집 지정됨" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "프로토콜 버전 %1을(를) 사용하고 있지만 최소한 %2이(가) 필요함" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "서버 프로토콜 버전이 충분합니다." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "현재 서버 프로토콜 버전이 %1이며, 필요한 버전 %2보다 높거나 같습니다." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "일관성 없는 로컬 모음집 트리를 감지하였습니다." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "원격 모음집의 루트에서 끝나는 부모 연결이 올바르지 않습니다. 자원이 깨졌습" +#~ "니다." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE 테스트 프로그램" + +#~ msgid "Cannot list root collection." +#~ msgstr "루트 모음집의 목록을 볼 수 없습니다." diff --git a/po/lt/akonadi_knut_resource.po b/po/lt/akonadi_knut_resource.po new file mode 100644 index 0000000..6b28d9d --- /dev/null +++ b/po/lt/akonadi_knut_resource.po @@ -0,0 +1,85 @@ +# Lithuanian translations for akonadi_knut_resource package. +# This file is distributed under the same license as the akonadi_knut_resource package. +# +# Jonas Česnauskas , 2011. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2011-07-21 16:02+0300\n" +"Last-Translator: Jonas Česnauskas \n" +"Language-Team: Lithuanian \n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n%10>=2 && (n%100<10 || n" +"%100>=20) ? 1 : n%10==0 || (n%100>10 && n%100<20) ? 2 : 3);\n" +"X-Generator: Lokalize 1.1\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Neparinktas duomenų failas." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Failas „%1“ įkrautas pilnai." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Parinkite duomenų failą" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut duomenų failas" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nutolusiam id %1 elementas nerastas" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM medyje viršutinė kolekcija nerasta." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Kolekcijos įrašyti negalima." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM medyje pakeista kolekcija nerasta." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM medyje pašalinta kolekcija nerasta." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM medyje viršutinė kolekcija „%1“ nerasta." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Elemento įrašyti negalima." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM medyje pakeistas elementas nerastas." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM medyje pašalintas elementas nerastas." diff --git a/po/lt/libakonadi5.po b/po/lt/libakonadi5.po new file mode 100644 index 0000000..9037d39 --- /dev/null +++ b/po/lt/libakonadi5.po @@ -0,0 +1,2631 @@ +# Lithuanian translations for libakonadi package. +# This file is distributed under the same license as the libakonadi package. +# +# Andrius Štikonas , 2009. +# Mindaugas Baranauskas , 2010. +# Remigijus Jarmalavičius , 2011. +# Liudas Ališauskas , 2011. +# Liudas Alisauskas , 2013, 2015. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2015-01-27 22:40+0200\n" +"Last-Translator: Liudas Ališauskas \n" +"Language-Team: Lithuanian \n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n%10>=2 && (n%100<10 || n" +"%100>=20) ? 1 : n%10==0 || (n%100>10 && n%100<20) ? 2 : 3);\n" +"X-Generator: Lokalize 1.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Mindaugas Baranauskas" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "embar@users.berlios.de" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Nepavyko registruoti objekto dbus sistemoje: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 su tipu %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agento identifikatorius" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi agentas" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pasiruošęs" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Atjungtas" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sinchronizuojama..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Klaida." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nesukonfigūruota" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Ištekliaus identifikatorius" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonodi išteklius" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Gautas neteisingas elementas" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Klaida kuriant elementą: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Klaida atnaujinant kolekciją: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Atnaujinimas vietinės kolekcijos nepavyko: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Atnaujinimas vietinių elementų nepavyko: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Nepavyko gauti elemento veiksenoje „Nepsisijungus“." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sinchronizuojamas aplankas „%1“" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nėra tokios kolekcijos." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Nepavyksta pasiekti sukurto agento D-Bus sąsajos." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Netinkanti kolekcija" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Netinkanti kolekcija" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Restore Collection From Trash" +msgid "Failed to parse Collection from response" +msgstr "Atkurti rinkinį iš šiukšlinės" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Netinkanti kolekcija" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Pateikta netinkanti kolekcija." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Nėra tokios kolekcijos." + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Netinkanti kolekcija" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Nepavyksta prisijungti prie Akonadi paslaugos." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Naudotojas nutraukė veiksmą." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Nežinoma klaida." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Failed to create tag." +msgid "Failed to create relation." +msgstr "Nepavyko sukurti žymės." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Nepavyko sukurti žymės." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Pavadinimas" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Įkeliama..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Klaida." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Pavadinimas" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Nepavyko nukopijuoti elemento:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Nepavyko nukopijuoti kolekcijos:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Nepavyko perkelti elemento:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Nepavyko perkelti kolekcijos:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Klaida." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Mėgstami aplankai" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Viso laiškų" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Neskaitytų laiškų" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Atmintinės dydis" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Sub aplanko saugyklos dydis" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Neskaityta" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Iš viso" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Dydis" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Žymė" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Bevardis papildinys" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Aprašo nėra" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi savi testas" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Patikrina ir praneša apie Akonadi serverio būseną" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Naujas agento objektas" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Ša&linti agento objektą" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&konfigūruoti agento objektą" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Naujas agento objektas" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Nepavyksta sukurti agento objekto: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Nepavyksta sukurti agento objekto" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Šalinti agento objektą?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Ar iš tikro pašalintį pažymėtą agento objektą?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Failed to create tag." +msgid "Failed to register %1 configuration dialog." +msgstr "Nepavyko sukurti žymės." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minutė" +msgstr[1] "minutės" +msgstr[2] "minučių" +msgstr[3] "minutė" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Atsiuntimas" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Naudoti pasirinktis iš aukštesnio lygmens aplanko ar sąskaitos" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sinchronizuoti kai pasirenkamas šis aplankas" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatiškai sinchronizuoti po:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Niekada" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "min" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Atsiuntimo parinktys" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "Visada gauti pilnus laiškus" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "Gauti laiško turinį pareikalavus" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Laikyti laiško turinį vietoje:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Nuolatos" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Ieškoti" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Naujas poaplankis..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Sukurti naują paaplankį šiuo metu pažymėtama aplanke" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Naujas aplankas" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Pavadinimas" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Nepavyko sukurti aplanko: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Nepavyko sukurti aplanko" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Bendri" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Vienas objektas" +msgstr[1] "%1 objektai" +msgstr[2] "%1 objektų" +msgstr[3] "%1 objektas" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Pavadinimas:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Naudoti savitą ženkliuką:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "aplankas" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Turinys:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "Nėra objektų" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Dydis:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 baitų" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "Klaida kuriant elementą: %1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "Aplanko sa&vybės" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "Iškirpti elementą" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Viso laiškų" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Neskaitytų laiškų" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "Dabartinis aplankas" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Jokio aplanko" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Atverti kolekcijos dialogą" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Parinkite kolekciją" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Perkelti čia" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopijuoti čia" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Atšaukti" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Pakeitimo laikas" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Žymės" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atributas: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "Rinktis dešinįjį" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Išlaikyti abu" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Duomenys" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Paleidžiamas Akonadi serveris..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stabdomas Akonadi serveris..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Perkelti čia" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopijuoti čia" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Padaryti &nuorodą čia" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Atšaukti" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Nepavyksta prisijungti prie Akonadi paslaugos." + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Paleidžiama asmeninės informacijos tvarkymo paslauga..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Stabdoma asmeninės informacijos tvarkymo paslauga..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Asmeninės informacijos tvarkymo paslauga vykdo duomenų bazės atnaujinimą." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi asmeninės informacijos tvarkymo paslauga nepaleista. Programa be jos " +"negali veikti." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Paleisti" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management service is not running. This " +#| "application cannot be used without it." +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi asmeninės informacijos tvarkymo paslauga nepaleista. Programa be jos " +"negali veikti." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, fuzzy, kde-format +#| msgid "Personal information management service is starting..." +msgid "The Akonadi personal information management service is not operational." +msgstr "Paleidžiama asmeninės informacijos tvarkymo paslauga..." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Išsamiau..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "Paleisti" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Dabartinis aplankas" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Pervadinti mėgstamąjį" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Pavadinimas:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi serverio savi testas" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Įrašyti ataskaitą..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Duombazės tvarkyklė rasta." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Duombazės tvarkyklė nerasta." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL serverio vykdomasis failas ne testuotas." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL serveris nerastas." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL serveris neskaitomas." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL serveris nevykdomas." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL rastas su netikėtu pavadinimu." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL serveris rastas." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL serveris rastas: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL serveris yra vykdomas." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi nepaleistas naudotojo „root“ teisėmis" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Klaida." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Nepavyko atverti failo „%1“" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Išsamiau" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Naujas aplankas..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Naujas" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Trinti aplanką" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Šalint?" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sinchronizuoti" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Aplanko sa&vybės" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Savybės" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Padėti" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Padėti" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Pridėti prie mėgstamų aplankų" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Pridėti prie mėgstamų aplankų" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Pašalinti iš mėgstamųjų aplankų" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Pašalinti iš mėgstamųjų aplankų" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Pervadinti mėgstamąjį" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Pervadinti" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopijuoti aplanką į..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopijuoti į" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopijuoti elementą į..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Perkelti elementą į..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Perkelti į" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Perkelti aplanką į..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Iškirpti elementą" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Iškirpti" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Iškirpti aplanką" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Sukurti išteklių" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Šalinti išteklį" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Ištekliaus sa&vybės" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Dirbti neprisijungus" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Perkelti aplanką į šiukšlinę" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Perkelti aplanką į šiukšlinę" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Perkelti elementą į šiukšlinę" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Perkelti elementą į šiukšlinę" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Atkurti aplanką iš šiukšlinės" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Atkurti aplanką iš šiukšlinės" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Atkurti elementą iš šiukšlinės" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Atkurti elementą iš šiukšlinės" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Atkurti rinkinį iš šiukšlinės" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Atkurti rinkinį iš šiukšlinės" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgid "Synchronize Folder Tree" +msgstr "Sinchronizuoti" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopijuoti aplanką" +msgstr[1] "&Kopijuoti %1 aplankus" +msgstr[2] "&Kopijuoti %1 aplankų" +msgstr[3] "&Kopijuoti %1 aplanką" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopijuoti elementą" +msgstr[1] "&Kopijuoti %1 elementus" +msgstr[2] "&Kopijuoti %1 elementų" +msgstr[3] "&Kopijuoti %1 elementą" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Iškirpti elementą" +msgstr[1] "&Iškirpti %1 elementus" +msgstr[2] "&Iškirpti %1 elementų" +msgstr[3] "&Iškirpti %1 elementą" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Iškirpti aplanką" +msgstr[1] "&Iškirpti %1 aplankus" +msgstr[2] "&Iškirpti %1 aplankų" +msgstr[3] "&Iškirpti %1 aplanką" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Ša&linti elementą" +msgstr[1] "Ša&linti %1 elementus" +msgstr[2] "Ša&linti %1 elementų" +msgstr[3] "Ša&linti %1 elementą" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Trinti aplanką" +msgstr[1] "&Trinti %1 aplankus" +msgstr[2] "&Trinti %1 aplankų" +msgstr[3] "&Trinti %1 aplanką" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Sinchronizuoti" +msgstr[1] "Sinchronizuoti" +msgstr[2] "Sinchronizuoti" +msgstr[3] "Sinchronizuoti" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Šalinti išteklį" +msgstr[1] "&Šalinti %1 išteklius" +msgstr[2] "&Šalinti %1 išteklių" +msgstr[3] "&Šalinti %1 išteklį" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopijuoti aplanką" +msgstr[1] "Kopijuoti %1 aplankus" +msgstr[2] "Kopijuoti %1 aplankų" +msgstr[3] "Kopijuoti %1 aplanką" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopijuoti elementą" +msgstr[1] "&Kopijuoti %1 elementus" +msgstr[2] "&Kopijuoti %1 elementų" +msgstr[3] "&Kopijuoti %1 elementą" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Iškirpti elementą" +msgstr[1] "Iškirpti %1 elementus" +msgstr[2] "Iškirpti %1 elementų" +msgstr[3] "Iškirpti %1 elementą" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Iškirpti aplanką" +msgstr[1] "Iškirpti %1 aplankus" +msgstr[2] "Iškirpti %1 aplankų" +msgstr[3] "Iškirpti %1 aplanką" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Trinti elementą" +msgstr[1] "Trinti %1 elementus" +msgstr[2] "Trinti %1 elementų" +msgstr[3] "Trinti %1 elementą" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Trinti aplanką" +msgstr[1] "Trinti %1 aplankus" +msgstr[2] "Trinti %1 aplankų" +msgstr[3] "Trinti %1 aplanką" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Šalinti išteklį" +msgstr[1] "Šalinti %1 išteklius" +msgstr[2] "Šalinti %1 išteklių" +msgstr[3] "Šalinti %1 išteklį" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sinchronizuoti" +msgstr[1] "Sinchronizuoti" +msgstr[2] "Sinchronizuoti" +msgstr[3] "Sinchronizuoti" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Pavadinimas" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Ar tikrai norite šalinti šį aplanką ir visus jo poaplankius?" +msgstr[1] "Ar tikrai norite šalinti %1 aplankus ir visus jų poaplankius?" +msgstr[2] "Ar tikrai norite šalinti %1 aplankų ir visus jų poaplankius?" +msgstr[3] "Ar tikrai norite šalinti %1 aplanką ir visus jų poaplankius?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Pašalinti aplanką?" +msgstr[1] "Pašalinti aplankus?" +msgstr[2] "Pašalinti aplankus?" +msgstr[3] "Pašalinti aplankus?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Nepavyksta pašalinti aplanko %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Aplanko pašalinti nepavyko" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Aplanko „%1“ savybės" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Trinti elementą?" +msgstr[1] "Trinti elementus?" +msgstr[2] "Trinti elementus?" +msgstr[3] "Trinti elementus?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Nepavyksta pašalinti: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Pervadinti mėgstamąjį" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Pavadinimas:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Naujas išteklius" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Nepavyko sukurti ištekliaus: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Nepavyko sukurti ištekliaus" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: widgets/standardactionmanager.cpp:396 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Delete Resource?" +#| msgid_plural "Delete Resources?" +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Trinti išteklį?" +msgstr[1] "Trinti %1 išteklius?" +msgstr[2] "Trinti %1 išteklių?" +msgstr[3] "Trinti %1 išteklį?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Įdėti nepavyko" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Paskyra „%1“ yra atsijungusi" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Prisijungti" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Perkelti į šį aplanką" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopijuoti į šį aplanką" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Failed to create tag." +msgid "Failed to update subscription: %1" +msgstr "Nepavyko sukurti žymės." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Vietinės prenumeratos" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Vietinės prenumeratos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Ieškoti:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Tik užsakyti" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Prenumeruoti" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Nebeprenumeruoti" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Ar tikrai norite pašalinti žymė %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Trinti žymę" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Trinti žymę" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "Nustatyti kurias žymas pritaikyti." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "Sukurti naują žymę" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Trinti žymę" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nenurodytas failo pavadinimas" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Nepavyko atverti duomenų failo „%1“." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Failas %1 neegzistuoja." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Netinkamas failo formatas." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Nepavyko rasti kolekcijos %1" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Nuotolinis Id" + +#~ msgid "MimeType" +#~ msgstr "MIME tipas" + +#~ msgid "Default Name" +#~ msgstr "Numatytas pavadinimas" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Trinti" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Atšaukti" + +#~ msgid "Take left one" +#~ msgstr "Rinktis kairįjį" diff --git a/po/lv/akonadi_knut_resource.po b/po/lv/akonadi_knut_resource.po new file mode 100644 index 0000000..0f49b62 --- /dev/null +++ b/po/lv/akonadi_knut_resource.po @@ -0,0 +1,95 @@ +# translation of akonadi_knut_resource.po to Latvian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Nauris , 2009. +# Maris Nartiss , 2009. +# Viesturs Zariņš , 2009. +# Einars Sprugis , 2012. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2012-07-07 23:24+0300\n" +"Last-Translator: Einars Sprugis \n" +"Language-Team: Latvian \n" +"Language: lv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.4\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " +"2);\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nav izvēlēts datu fails." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Fails '%1' veiksmīgi ielādēts." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Izvēlieties datu failu" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut datu fails" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Vienība priekš attālinātā id %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Vecākkolekcija DOM kokā netika atrasta." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Neizdevās ierakstīt kolekciju." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Izmainītā kolekcija DOM kokā netika atrasta." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Dzēstā kolekcija DOM kokā netika atrasta." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Vecākkolekcija '%1' DOM kokā netika atrasta." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Neizdevās ierakstīt vienību." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Izmainītā vienība DOM kokā netika atrasta." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Dzēstā vienība DOM kokā netika atrasta." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Knut datu faila ceļš." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Nemainīt aizmugures datus." diff --git a/po/lv/libakonadi5.po b/po/lv/libakonadi5.po new file mode 100644 index 0000000..daef4c7 --- /dev/null +++ b/po/lv/libakonadi5.po @@ -0,0 +1,2969 @@ +# translation of libakonadi.po to Latvian +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Viesturs Zarins , 2008, 2010. +# Viesturs Zariņš , 2009. +# Maris Nartiss , 2009. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2010-01-01 15:34+0200\n" +"Last-Translator: Viesturs Zarins \n" +"Language-Team: Latvian \n" +"Language: lv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : " +"2);\n" +"X-Generator: Lokalize 1.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Viesturs Zariņš" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "viesturs.zarins@mii.lu.lv" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Aģenta identifikators" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi aģents" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Gatavs" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Nesaistē" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sinhronizē..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Kļūda." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label, commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Resursa iderntifikators" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title, application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Akonadi resurss" + +#: agentbase/resourcebase.cpp:579 +#, fuzzy, kde-format +#| msgid "Invalid parent" +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Nederīgs vecāks" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Neizdevās atjaunināt lokālo kolekciju: %1." + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Neizdevās atjaunināt lokālo kolekciju: %1." + +#: agentbase/resourcebase.cpp:718 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Neizdevās atjaunināt lokālo kolekciju: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Nevar ielādēt vienību bezsaistes režīmā." + +#: agentbase/resourcebase.cpp:916 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Syncing collection '%1'" +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sinhronizē kolekciju '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for sync." +msgstr "Neizdevās ielādēt resursu kolekciju." + +#: agentbase/resourcebase.cpp:983 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for attribute sync." +msgstr "Neizdevās ielādēt resursu kolekciju." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nav tādas kolekcijas." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Atrastas neatpazītas bāreņu kolekcijas" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, fuzzy, kde-format +#| msgid "Unable to access dbus interface of created agent." +msgid "Unable to access D-Bus interface of created agent." +msgstr "Neizdevās piekļūt izveidotā aģenta DBus saskarnei." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Aģenta instances izveidošanas noildze." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to obtain agent type '%1'." +msgstr "Neizdevās izveidot aģenta instanci." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Neizdevās izveidot aģenta instanci." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, fuzzy, kde-format +#| msgid "Invalid collection given." +msgid "Invalid collection instance." +msgstr "Iedota nederīga kolekcija." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Nederīga resursa instance." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Neizdevās iegūt D-Bus saskarni resursam '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, fuzzy, kde-format +#| msgid "Resource synchronization timed out." +msgid "Collection attributes synchronization timed out." +msgstr "Reursu sinhronizācijas noildze." + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Nederīga kolekcija" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Nederīga kolekcija" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Nederīgs vecāks" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to parse Collection from response" +msgstr "Neizdevās ielādēt resursu kolekciju." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Nederīga kolekcija" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Iedota nederīga kolekcija." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nav norādīti pārvietojamie objekti" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nav norādīts derīgs mērķis" + +#: core/jobs/invalidatecachejob.cpp:58 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection." +msgstr "Nederīga kolekcija" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Nederīga kolekcija" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Neizdevās pieslēgties Akonadi servisam." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi servera protokola versija nav savietojama. Pārliecinieties vai jums " +"ir savietojama Akonadi versija." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Lietotājs pārtrauca darbību." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Nezināma kļūda." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "Neizdevās izveidot aģenta instanci." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Reursu sinhronizācijas noildze." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Neizdevās ielādēt resursa %1 saknes kolekciju." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Mav dots resursa ID." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Nederīgs resursa identifikators '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Neizdevās konfigurēt noklusēto resursu caur D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Neizdevās ielādēt resursu kolekciju." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Noildze mēģinot iegūt slēgu." + +#: core/jobs/tagcreatejob.cpp:49 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create tag." +msgstr "Neizdevās izveidot aģenta instanci." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, fuzzy, kde-format +#| msgid "Invalid parent" +msgid "Invalid items passed" +msgstr "Nederīgs vecāks" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection passed" +msgstr "Nederīga kolekcija" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, fuzzy, kde-format +#| msgid "Invalid collection given." +msgid "No valid collection or empty itemlist" +msgstr "Iedota nederīga kolekcija." + +#: core/jobs/trashrestorejob.cpp:90 +#, fuzzy, kde-format +#| msgid "Could not fetch root collection of resource %1." +msgid "Could not find restore collection and restore resource is not available" +msgstr "Neizdevās ielādēt resursa %1 saknes kolekciju." + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nosaukums" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Syncing..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Sinhronizē..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Kļūda." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nosaukums" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Neizdevās atvērt failu '%1'" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "no collection" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "nav kolekcijas" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Neizdevās atvērt failu '%1'" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "no collection" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "nav kolekcijas" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Kļūda." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Iecienītās mapes" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Vēstules kopā" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Nelasītas vēstules" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Glabāšanas izmērs" + +#: core/models/statisticsproxymodel.cpp:111 +#, fuzzy, kde-format +#| msgid "Storage Size" +msgid "Subfolder Storage Size" +msgstr "Glabāšanas izmērs" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Nelasīti" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Kopā" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Izmērs" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Neizdevās ielādēt ierakstu pēc indeksa" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indekss vairs nav pieejams" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Satura daļa '%1' nav pieejama šim indeksam" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Nav pieejama sesija šim indeksam" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Nav pieejams ieraksts šim indeksam" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Nenosaukts spraudnis" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Apraksts nav pieejams" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "Akonadi servera iekšējā pārbaude" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "Neizdevās pieslēgties Akonadi servisam." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgid "&Delete Agent Instance" +msgstr "&Dzēst %1 vienību" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Could not create agent instance: %1" +msgstr "Neizdevās izveidot aģenta instanci." + +#: widgets/agentactionmanager.cpp:56 +#, fuzzy, kde-format +#| msgid "Agent instance creation timed out." +msgid "Agent instance creation failed" +msgstr "Aģenta instances izveidošanas noildze." + +#: widgets/agentactionmanager.cpp:58 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Neizdevās izveidot aģenta instanci." + +#: widgets/agentactionmanager.cpp:62 +#, fuzzy, kde-format +#| msgid "Do you really want to delete all selected items?" +msgid "Do you really want to delete the selected agent instance?" +msgstr "Vai tiešām vēlaties dzēst visas iezīmētās vienības?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "Neizdevās izveidot aģenta instanci." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minūte" +msgstr[1] "minūtes" +msgstr[2] "minūtes" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, fuzzy, kde-format +#| msgid "Subscribe to selected folder" +msgid "Synchronize when selecting this folder" +msgstr "Pierakstīties uz izvēlēto mapi" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nekad" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minūtes" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokāli pieglabātās daļas" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, fuzzy, kde-format +#| msgctxt "no cache timeout" +#| msgid "Never" +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Nekad" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "search folder" +#| msgid "Search:" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Meklēt:" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, fuzzy, kde-format +#| msgid "&New Folder..." +msgid "&New Subfolder..." +msgstr "&Jauna mape..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Jauna mape" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nosaukums" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Neizdevās izveidot mapi: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Neveiksmīga mapes izveidošana" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Pamata" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 objekts" +msgstr[1] "%1 objekti" +msgstr[2] "%1 objektu" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nosaukums:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Izmantot pielāgotu ikonu:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "mape" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Saturs:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objektu" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Izmērs:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 baitu" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "Mapes ī&pašības" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "Items" +msgstr "&Izgriezt %1 vienību" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Vēstules kopā" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Nelasītas vēstules" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Reindex folder" +msgstr "&Dzēst mapi" + +#: widgets/collectionrequester.cpp:113 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "New Folder" +msgid "No Folder" +msgstr "Jauna mape" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Atvērt kolekciju logu" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Nederīga kolekcija" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Pārvietot šeit" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopēt šeit" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Atcelt" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Palaiž Akonadi serveri..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Aptur Akonadi serveri..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Pārvietot šeit" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopēt šeit" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Saitēt šeit" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Atcelt" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Neizdevās pieslēgties Akonadi servisam." + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Akonadi personīgās informācijas pārvaldības ietvars nestrādā.\n" +"Lai iegūtu papildus informāciju, nospiediet uz \"Detaļas...\"." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi personīgās informācijas pārvaldības ietvars nestrādā.\n" +"Lai iegūtu papildus informāciju, nospiediet uz \"Detaļas...\"." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi personīgās informācijas pārvaldības ietvars nestrādā.\n" +"Lai iegūtu papildus informāciju, nospiediet uz \"Detaļas...\"." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi personal information management framework is not " +#| "operational.\n" +#| "Click on \"Details...\" to obtain detailed information on this problem." +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Akonadi personīgās informācijas pārvaldības ietvars nestrādā.\n" +"Lai iegūtu papildus informāciju, nospiediet uz \"Detaļas...\"." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, fuzzy, kde-format +#| msgid "Details" +msgid "Details..." +msgstr "Detaļas" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "" + +#: widgets/recentcollectionaction.cpp:43 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Recent Folder" +msgstr "&Dzēst mapi" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Pārdēvēt iecienīto" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox New name of the folder." +#| msgid "Name:" +msgid "Name:" +msgstr "Nosaukums:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi servera iekšējā pārbaude" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Saglabāt atskaiti..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopēt atskaiti uz starpliktuvi" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Esošajai Akonadi servera konfigurācijai nepieciešams QtSQL draiveris '%1' un " +"tas tika atrasts." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Esošajai Akonadi servera konfigurācijai nepieciešams QtSQL draiveris '%1'.\n" +"Ir instalēti šādi draiveri: %2.\n" +"Pārliecinietes, ka ir insalēts nepieciešamais draiveris." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Nav atrasts datubāzes draiveris." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Nav atrasts datubāzes draiveris." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL servera izpildfails nav pārbaudīts." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Aktīvā konfigurācija nepieprasa iekšēju MySQL serveri." + +#: widgets/selftestdialog.cpp:216 +#, fuzzy, kde-format +#| msgid "" +#| "You currently have configured Akonadi to use the MySQL server '%1'.\n" +#| "Make sure you have the MySQL server installed, set the correct path and " +#| "ensure you have the necessary read and execution rights on the server " +#| "executable. The server executable is typically called 'mysqld', its " +#| "locations varies depending on the distribution." +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Šobrīd Akonari ir konfigurēts izmantot MySQL serveri '%1'.\n" +"Pārliecinieties, ka jums ir instalēts MySQL serveris, ir iestatīts korekts " +"ceļš un pārliecinieties ka servera izpildfailam ir nepieciešamās lasīšanas " +"un izpildes atļaujas . Servera izpildfailu parasti sauc 'mysqld', tā " +"atrašanās vieta ir atkarīga no jūsu distribūcijas." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL serveris nav atrasts." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL serveris nav lasāms." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL serveris nav izpildāms." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL atrasts ar negaidītu nosaukumu." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL serveris atrasts." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL serveris atrasts: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL serveris ir izpildāms." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "MySQL servera '%1' palaišana neveiksmīga, kļūdas ziņojums: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Neizdevās palaist MySQL serveri." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL servera kļūdu žurnāls nav pārbaudīts." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Nav atrasts aktīvs MySQL kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:258 +#, fuzzy, kde-format +#| msgid "" +#| "The MySQL server did not report any errors during this startup into '%1'." +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "MySQL serveris neziņoja nevienu kļūdu veicot palaišanu failā '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL kļūdu žurnāls nav lasāms." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Tika atrasts MySQL servera kļūdu žurnāls, bet tas nav nolasāms: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL servera žurnāls satur kļūdas." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL servera žurnāla fails '%1' satur kļūdas." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL servera žurnāls satur brīdinājumus." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL servera žurnāla fails '%1' satur brīdinājumus." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL servera žurnālā nav kļūdu." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL servera žurnāla failā '%1' nav kļūdu vai brīdinājumu." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL servera konfigurācija nav pārbaudīta." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Atrasta noklusētā MySQL servera konfigurācija." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Tika atrasta noklusētā MySQL servera konfigurācija tun tā ir lasāma vietā %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Nav atrasta MySQL servera noklusētā konfigurācija." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Netika atrasta noklusētā MySQL servera konfigurācija vai to nevarēja " +"nolasīt. Pārbaudiet vai Akonadi instalācija ir pabeigta un vai jums ir " +"nepieciešamās piekļuves atļaujas." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL servera pielāgotā konfigurācija nav pieejama." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Netika atrasta pielāgotā MySQL severa konfigurācija, bet tā nav obligāta." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Atrasta MySQL servera pielāgotā konfigurācija." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"MySQL servera pielāgotā konfigurācija tika atrasta un ir lasāma iekš %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL servera pielāgotā konfigurācija nav lasāma." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL pielāgotā konfigurācija tika atrasta vietā %1, bet nav lasāma. " +"Pārbaudiet savas piekļuves tiesības." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL servera konfigurācija nav atrasta vai nav lasāma." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL servera konfigurācija netika atrasta vai nav lasāma." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL servera konfigurācija ir lietojama." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL servera konfigurācija tika atasta iekš %1 un ir lasāma." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Neizdevās pieslēgties PostgreSQL serverim." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL serveris atrasts." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL serveris atrasts un savienojums strādā." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "nav atrasta akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Nepieciešams, lai programma 'akonadictl' ir pieejama iekš $PATH. " +"Pārliecinieties ka ir instalēts Akonadi serveris." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl atrasta un ir lietojama" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Programma '%1' Akonadi servera kontrolēšanai tikai atrasta un to izdevās " +"veiksmīgi palaist.\n" +"Rezultāts:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl atrasta, bet nav lietojama" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Programma '%1' Akonadi servera kontrolēšanai tikai atrasta, bet to neizdevās " +"palaist.\n" +"Rezultāts:\n" +"%2\n" +"Pārliecinieties ka Akonadi serveris ir korekti instalēts." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi kontroles process reģistrēts pie D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi kontroles process ir reģistrēts pie D-Bus, tas parasti nozīmē ka tas " +"darbojas." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi kontroles process nav reģistrēts pie D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi kontroles process nav reģistrēts pie D-Bus, tas parasti nozīmē ka " +"tas nav palaists vai to palaižot gadījusies kļūme." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi servera process reģistrēts pie D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi servera process ir reģistrēts pie D-Bus, tas parasti nozīmē ka tas " +"darbojas." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi servera process nav reģistrēts pie D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi servera process nav reģistrēts pie D-Bus, tas parasti nozīmē ka tas " +"nav palaists vai to palaižot gadījusies kļūme." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Nav iespējams veikt protokola versijas pārbaudi." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Bez savienojuma ar serveri nevar pārbaudīt vai protokola versija atbilst " +"prasībām." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Servera protokola versija ir par vecu" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Servera protokola versija ir %1, bet nepieciešama vismaz versija %2. Lūdzu " +"instalējiet jaunāku Akonadi servera versiju." + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "Servera protokola versija ir par vecu" + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "Servera protokola versija ir par vecu" + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "Servera protokola versija ir par vecu" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Atrasti resursu aģenti." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Ir atrasts vismaz viens resursu aģents." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Nav atrasts neviens resursu aģents." + +#: widgets/selftestdialog.cpp:482 +#, fuzzy, kde-format +#| msgid "" +#| "No resource agents have been found, Akonadi is not usable without at " +#| "least one. This usually means that no resource agents are installed or " +#| "that there is a setup problem. The following paths have been searched: " +#| "'%1'. The XDG_DATA_DIRS environment variable is set to '%2', make sure " +#| "this includes all paths where Akonadi agents are installed to." +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Neizdevās atrast nevienu resursu aģentu. Akonadi nevar lietot bez vismaz " +"viena aģenta. Tas parasti nozīmē ka nav instalēts neviens resursu aģents vai " +"par konfigurācijas kļūdu. Tika pārmeklētas šādas mapes: '%1'. XDG_DATA_DIRS " +"mainīgais ir iestatīts uz '%2', pārliecinieties, ka tas satur visus ceļus, " +"kuros instalēti Akonadi aģenti." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Nav pašreizēja Akonadi servera kļūdu žurnāla." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi serveris neziņoja nevienu kļūdu veicot palaišanu." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Atrasts pašreizējs Akonadi servera kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:504 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi server did not report any errors during its current startup." +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "Akonadi serveris neziņoja nevienu kļūdu veicot palaišanu." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Nav atrasts iepriekšējais Akonadi servera kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi serveris neziņoja nevienu kļūdu iepriekšējās palaišanas laikā." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Atrasts iepriekšējais Akonadi servera kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:518 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi server did report error during its previous startup into %1." +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi serveris ziņoja par kļūdām iepriekšējās palaišanas laikā failā %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Nav pašreizēja Akonadi kontroles kļūdu žurnāla." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "Akonadi kontroles process neziņoja nevienu kļūdu veicot palaišanu." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Atrasts pašreizējs Akonadi kontroles kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:535 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi control process did not report any errors during its current " +#| "startup." +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "Akonadi kontroles process neziņoja nevienu kļūdu veicot palaišanu." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Nav atrasts iepriekšējais Akonadi kontroles kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi kontroles process neziņoja nevienu kļūdu iepriekšējās palaišanas " +"laikā." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Atrasts iepriekšējais Akonadi kontroles kļūdu žurnāls." + +#: widgets/selftestdialog.cpp:549 +#, fuzzy, kde-format +#| msgid "" +#| "The Akonadi control process did report error during its previous startup " +#| "into %1." +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi kontroles process ziņoja par kļūdām iepriekšējās palaišanas laikā " +"failā %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Saglabāt pārbaudes atskaiti" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Kļūda." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Neizdevās atvērt failu '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Gadījās kļūda palaižot Akonadi serveri. Šīs iebūvētās pārbaudes ir " +"paredzētas problēmu atrašanai un risināšanai. Lūdzot palīdzību vai nosūtot " +"kļūdas ziņojumus, vienmēr iekļaujiet šo atskaiti." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaļas" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Vairāk risināšanas padomus meklējiet userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Jauna mape..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Delete Folder" +msgstr "&Dzēst mapi" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "Delete?" +msgid "Delete" +msgstr "Dzēst?" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Folder" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Mapes ī&pašības" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Properties" +msgstr "Mapes ī&pašības" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Ielīmēt" + +#: widgets/standardactionmanager.cpp:96 +#, fuzzy, kde-format +#| msgid "&Paste" +msgid "Paste" +msgstr "&Ielīmēt" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Pārvaldīt lokālos &pierakstus..." + +#: widgets/standardactionmanager.cpp:100 +#, fuzzy, kde-format +#| msgid "Manage Local &Subscriptions..." +msgid "Manage Local Subscriptions" +msgstr "Pārvaldīt lokālos &pierakstus..." + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Pievienot iecienītajām mapēm" + +#: widgets/standardactionmanager.cpp:108 +#, fuzzy, kde-format +#| msgid "Add to Favorite Folders" +msgid "Add to Favorite" +msgstr "Pievienot iecienītajām mapēm" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Izņemt no iecienītajām mapēm" + +#: widgets/standardactionmanager.cpp:116 +#, fuzzy, kde-format +#| msgid "Remove from Favorite Folders" +msgid "Remove from Favorite" +msgstr "Izņemt no iecienītajām mapēm" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Pārdēvēt iecienīto..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopēt mapi uz..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, fuzzy, kde-format +#| msgid "&Copy Folder" +#| msgid_plural "&Copy %1 Folders" +msgid "Copy To" +msgstr "&Kopēt %1 mapi" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopēt vienību uz..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Pārvietot vienību uz..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "Move To" +msgstr "Pārvietot vienību uz..." + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Pārvietot mapi uz..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Izgriezt %1 vienību" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Izgriezt %1 mapi" + +#: widgets/standardactionmanager.cpp:150 +#, fuzzy, kde-format +#| msgctxt "@title, application name" +#| msgid "Akonadi Resource" +msgid "Create Resource" +msgstr "Akonadi resurss" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Resource" +msgstr "&Dzēst mapi" + +#: widgets/standardactionmanager.cpp:153 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "&Resource Properties" +msgstr "Mapes ī&pašības" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Resource" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:168 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Offline" +msgid "Work Offline" +msgstr "Nesaistē" + +#: widgets/standardactionmanager.cpp:188 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Folder Recursively" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:189 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Recursively" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:196 +#, fuzzy, kde-format +#| msgid "Move Folder To..." +msgid "&Move Folder To Trash" +msgstr "Pārvietot mapi uz..." + +#: widgets/standardactionmanager.cpp:197 +#, fuzzy, kde-format +#| msgid "Move Folder To..." +msgid "Move Folder To Trash" +msgstr "Pārvietot mapi uz..." + +#: widgets/standardactionmanager.cpp:204 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "&Move Item To Trash" +msgstr "Pārvietot vienību uz..." + +#: widgets/standardactionmanager.cpp:205 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "Move Item To Trash" +msgstr "Pārvietot vienību uz..." + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Favorite Folders" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:247 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Favorite Folders" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Folder Tree" +msgstr "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopēt %1 mapi" +msgstr[1] "&Kopēt %1 mapes" +msgstr[2] "&Kopēt %1 mapes" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopēt %1 vienību" +msgstr[1] "&Kopēt %1 vienības" +msgstr[2] "&Kopēt %1 vienību" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Izgriezt %1 vienību" +msgstr[1] "&Izgriezt %1 vienības" +msgstr[2] "&Izgriezt %1 vienības" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Izgriezt %1 mapi" +msgstr[1] "&Izgriezt %1 mapes" +msgstr[2] "&Izgriezt %1 mapes" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Dzēst %1 vienību" +msgstr[1] "&Dzēst %1 vienības" +msgstr[2] "&Dzēst %1 vienību" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Dzēst mapi" +msgstr[1] "&Dzēst mapi" +msgstr[2] "&Dzēst mapi" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sinhronizēt mapi" +msgstr[1] "&Sinhronizēt mapi" +msgstr[2] "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:347 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Dzēst mapi" +msgstr[1] "&Dzēst mapi" +msgstr[2] "&Dzēst mapi" + +#: widgets/standardactionmanager.cpp:348 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sinhronizēt mapi" +msgstr[1] "&Sinhronizēt mapi" +msgstr[2] "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:350 +#, fuzzy, kde-format +#| msgid "&Copy Folder" +#| msgid_plural "&Copy %1 Folders" +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "&Kopēt %1 mapi" +msgstr[1] "&Kopēt %1 mapes" +msgstr[2] "&Kopēt %1 mapes" + +#: widgets/standardactionmanager.cpp:351 +#, fuzzy, kde-format +#| msgid "&Copy Item" +#| msgid_plural "&Copy %1 Items" +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "&Kopēt %1 vienību" +msgstr[1] "&Kopēt %1 vienības" +msgstr[2] "&Kopēt %1 vienību" + +#: widgets/standardactionmanager.cpp:352 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "&Izgriezt %1 vienību" +msgstr[1] "&Izgriezt %1 vienības" +msgstr[2] "&Izgriezt %1 vienības" + +#: widgets/standardactionmanager.cpp:353 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "&Izgriezt %1 mapi" +msgstr[1] "&Izgriezt %1 mapes" +msgstr[2] "&Izgriezt %1 mapes" + +#: widgets/standardactionmanager.cpp:354 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "&Dzēst %1 vienību" +msgstr[1] "&Dzēst %1 vienības" +msgstr[2] "&Dzēst %1 vienību" + +#: widgets/standardactionmanager.cpp:355 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "&Dzēst mapi" +msgstr[1] "&Dzēst mapi" +msgstr[2] "&Dzēst mapi" + +#: widgets/standardactionmanager.cpp:356 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "&Sinhronizēt mapi" +msgstr[1] "&Sinhronizēt mapi" +msgstr[2] "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "&Dzēst mapi" +msgstr[1] "&Dzēst mapi" +msgstr[2] "&Dzēst mapi" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "&Sinhronizēt mapi" +msgstr[1] "&Sinhronizēt mapi" +msgstr[2] "&Sinhronizēt mapi" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nosaukums" + +#: widgets/standardactionmanager.cpp:368 +#, fuzzy, kde-format +#| msgid "Do you really want to delete folder '%1' and all its sub-folders?" +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Vai tiešām vēlaties dzēst mapi '%1' un visas tās apakšmapes?" +msgstr[1] "Vai tiešām vēlaties dzēst mapi '%1' un visas tās apakšmapes?" +msgstr[2] "Vai tiešām vēlaties dzēst mapi '%1' un visas tās apakšmapes?" + +#: widgets/standardactionmanager.cpp:371 +#, fuzzy, kde-format +#| msgid "Delete folder?" +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Dzēst mapi?" +msgstr[1] "Dzēst mapi?" +msgstr[2] "Dzēst mapi?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Neizdevās izdzēst mapi: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Neveiksmīga mapes dzēšana" + +#: widgets/standardactionmanager.cpp:375 +#, fuzzy, kde-format +#| msgid "Properties of Folder %1" +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Mapes %1 īpašības" + +#: widgets/standardactionmanager.cpp:379 +#, fuzzy, kde-format +#| msgid "Do you really want to delete all selected items?" +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Vai tiešām vēlaties dzēst visas iezīmētās vienības?" +msgstr[1] "Vai tiešām vēlaties dzēst visas iezīmētās vienības?" +msgstr[2] "Vai tiešām vēlaties dzēst visas iezīmētās vienības?" + +#: widgets/standardactionmanager.cpp:380 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "&Dzēst %1 vienību" +msgstr[1] "&Dzēst %1 vienības" +msgstr[2] "&Dzēst %1 vienību" + +#: widgets/standardactionmanager.cpp:381 +#, fuzzy, kde-format +#| msgid "Could not delete folder: %1" +msgid "Could not delete item: %1" +msgstr "Neizdevās izdzēst mapi: %1" + +#: widgets/standardactionmanager.cpp:382 +#, fuzzy, kde-format +#| msgid "Folder deletion failed" +msgid "Item deletion failed" +msgstr "Neveiksmīga mapes dzēšana" + +#: widgets/standardactionmanager.cpp:384 +#, fuzzy, kde-format +#| msgid "Rename Favorite" +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Pārdēvēt iecienīto" + +#: widgets/standardactionmanager.cpp:385 +#, fuzzy, kde-format +#| msgctxt "@label:textbox New name of the folder." +#| msgid "Name:" +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nosaukums:" + +#: widgets/standardactionmanager.cpp:387 +#, fuzzy, kde-format +#| msgctxt "@title, application name" +#| msgid "Akonadi Resource" +msgctxt "@title:window" +msgid "New Resource" +msgstr "Akonadi resurss" + +#: widgets/standardactionmanager.cpp:388 +#, fuzzy, kde-format +#| msgid "Could not create folder: %1" +msgid "Could not create resource: %1" +msgstr "Neizdevās izveidot mapi: %1" + +#: widgets/standardactionmanager.cpp:389 +#, fuzzy, kde-format +#| msgid "Folder creation failed" +msgid "Resource creation failed" +msgstr "Neveiksmīga mapes izveidošana" + +#: widgets/standardactionmanager.cpp:393 +#, fuzzy, kde-format +#| msgid "Do you really want to delete the search view '%1'?" +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Vai tiešām vēlaties dzēst meklēšanas skatu '%1'?" +msgstr[1] "Vai tiešām vēlaties dzēst meklēšanas skatu '%1'?" +msgstr[2] "Vai tiešām vēlaties dzēst meklēšanas skatu '%1'?" + +#: widgets/standardactionmanager.cpp:396 +#, fuzzy, kde-format +#| msgid "Delete folder?" +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Dzēst mapi?" +msgstr[1] "Dzēst mapi?" +msgstr[2] "Dzēst mapi?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Neizdevās ielīmēt datus: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Neizdevās ielīmēt" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Offline" +msgctxt "@action:button" +msgid "Go Online" +msgstr "Nesaistē" + +#: widgets/standardactionmanager.cpp:1592 +#, fuzzy, kde-format +#| msgid "Copy to This Folder" +msgid "Move to This Folder" +msgstr "Kopēt uz šo mapi" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopēt uz šo mapi" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to update subscription: %1" +msgstr "Neizdevās izveidot aģenta instanci." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Manage Local &Subscriptions..." +msgctxt "@title" +msgid "Subscription Error" +msgstr "Pārvaldīt lokālos &pierakstus..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Manage Local &Subscriptions..." +msgid "Local Subscriptions" +msgstr "Pārvaldīt lokālos &pierakstus..." + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, fuzzy, kde-format +#| msgctxt "search folder" +#| msgid "Search:" +msgid "Search:" +msgstr "Meklēt:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgctxt "@title:column" +#| msgid "Subscribe To" +msgid "&Subscribed only" +msgstr "Pierakstīties uz" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgctxt "@title:column" +#| msgid "Subscribe To" +msgid "Su&bscribe" +msgstr "Pierakstīties uz" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgctxt "@title:column" +#| msgid "Unsubscribe From" +msgid "&Unsubscribe" +msgstr "Atrakstīties no" + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create a new tag" +msgstr "Neizdevās izveidot aģenta instanci." + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete the search view '%1'?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Vai tiešām vēlaties dzēst meklēšanas skatu '%1'?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "&Dzēst %1 vienību" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "&Dzēst %1 vienību" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Create new tag" +msgstr "Neizdevās izveidot aģenta instanci." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "&Dzēst %1 vienību" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, fuzzy, kde-format +#| msgid "No valid destination specified" +msgid "No filename specified" +msgstr "Nav norādīts derīgs mērķis" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to open data file '%1'." +msgstr "Neizdevās izveidot aģenta instanci." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to parse data file '%1'." +msgstr "Neizdevās izveidot aģenta instanci." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "Neizdevās izveidot aģenta instanci." + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "Neizdevās izveidot aģenta instanci." + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "Neizdevās izveidot aģenta instanci." + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgid "Invalid parent" +msgid "Invalid file format." +msgstr "Nederīgs vecāks" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "Neizdevās ielīmēt datus: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "Nederīga kolekcija" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Attālinātais Id" + +#~ msgid "MimeType" +#~ msgstr "Mime tips" + +#, fuzzy +#~| msgid "Delete?" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Dzēst?" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Atcelt" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Nelasīts" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Kopā" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Izmērs" + +#, fuzzy +#~| msgctxt "@title, application name" +#~| msgid "Akonadi Resource" +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi resurss" + +#, fuzzy +#~| msgctxt "@title:column, name of a thing" +#~| msgid "Name" +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nosaukums" + +#~ msgid "Invalid collection specified" +#~ msgstr "Norādīta nederīga kolekcija" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Atrasta protokola versija %1, nepieciešama vismaz %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Servera protokola versija ir gana jauna." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Servera protokola versija ir %1, tā sakrīt vai ir jaunāka par " +#~ "nepieciešamo versiju %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Lokālo kolekciju kokā atrastas pretrunas." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Dota attālināta kolekcija bez vecāku ķēdes ar sakni sākumā, resurss " +#~ "bojāts." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE izmēģināšanas programma" + +#~ msgid "Cannot list root collection." +#~ msgstr "Neizdevās nolasīt saknes kolekciju." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk meklēšanas serviss reģistrēts pie D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Nepomuk meklēšanas serviss ir reģistrēts pie D-Bus, tas parasti nozīmē ka " +#~ "tas darbojas." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk meklēšanas serviss nav reģistrēts pie D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Nepomuk meklēšanas serviss nav reģistrēts pie D-Bus, tas parasti nozīmē " +#~ "ka tas nav palaists vai to palaižot gadījusies kļūme." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuk meklēšanas serviss lieto nepiemērotu aizmuguri." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Nepomuk meklēšanas serviss izmanto aizmuguri '%1'. kas nav piemērota " +#~ "Akonadi vajadzībām." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuk meklēšanas serviss lieto nepiemērotu aizmuguri. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Nepomuk meklēšanas serviss lieto vienu no rekomendētajām aizmugurēm." + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Jauna mape..." + +#, fuzzy +#~| msgid "Folder &Properties" +#~ msgid "Resource Properties" +#~ msgstr "Mapes ī&pašības" + +#~ msgid "Cache" +#~ msgstr "Kešatmiņa" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Mantot pieglabāšanas politiku no vecāka" + +#~ msgid "Cache Policy" +#~ msgstr "Pieglabāšanas politika" + +#~ msgid "Interval check time:" +#~ msgstr "Pārbaudes intervāls:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Lokālās glabātuves noildze:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Sinhronizēt pēc vajadzības" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Pārvaldīt, kuras mapes ir redzamas mapju kokā" + +#, fuzzy +#~| msgctxt "search folder" +#~| msgid "Search:" +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Meklēt:" + +#~ msgid "Available Folders" +#~ msgstr "Pieejamās mapes" + +#~ msgid "Current Changes" +#~ msgstr "Šobrīdējās izmaiņas" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Atrakstīties no izvēlētās mapes" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi nedarbojas.
Detaļas...

" + +#~ msgid "TODO" +#~ msgstr "TODO" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi resurss" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Akonadi serveris ziņoja par kļūdām veicot palaišanu failā %1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Akonadi kontroles process ziņoja par kļūdām veicot palaišanu failā %1." diff --git a/po/mr/akonadi_knut_resource.po b/po/mr/akonadi_knut_resource.po new file mode 100644 index 0000000..cbe54d8 --- /dev/null +++ b/po/mr/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Chetan Khona , 2013. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2013-03-20 15:19+0530\n" +"Last-Translator: Chetan Khona \n" +"Language-Team: Marathi \n" +"Language: mr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "डेटा फाइल निवडली नाही." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "डेटा फाईल निवडा" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "" diff --git a/po/mr/libakonadi5.po b/po/mr/libakonadi5.po new file mode 100644 index 0000000..af15890 --- /dev/null +++ b/po/mr/libakonadi5.po @@ -0,0 +1,2531 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Chetan Khona , 2013. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2013-03-08 10:11+0530\n" +"Last-Translator: Chetan Khona \n" +"Language-Team: Marathi \n" +"Language: mr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"X-Generator: Lokalize 1.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "चेतन खोना" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "chetan@kompkin.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, fuzzy, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "तयार" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "त्रुटी." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "अपरिचीत त्रुटी." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "नाव" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "दाखल करत आहे..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "त्रुटी." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "नाव" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "त्रुटी." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "एकूण" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "आकार" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "निनावी प्लगइन" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "वर्णन उपलब्ध नाही" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "मिनिट" +msgstr[1] "मिनिटे" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "कधीही नाही" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "मिनिटे" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "शोधा" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "नवीन संचयीका" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "नाव" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, fuzzy, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "सामान्य" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "" +msgstr[1] "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "नाव (&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, fuzzy, kde-format +msgid "folder" +msgstr "संचयीका" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "आकडेवारी" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, fuzzy, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "मजकूर" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, fuzzy, kde-format +msgid "Size:" +msgstr "आकार :" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, fuzzy, kde-format +msgid "0 Byte" +msgstr "बाइट" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Delete folder?" +#| msgid_plural "Delete folders?" +msgid "Reindex folder" +msgstr "संचयीका काढून टाकायची का?" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "" + +#: widgets/collectionview.cpp:220 +#, fuzzy, kde-format +msgid "&Move here" +msgstr "येथे हलवा" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "येथे प्रत करा (&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "रद्द करा" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, fuzzy, kde-format +msgid "Data" +msgstr "डेटा" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "येथे हलवा (&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "येथे प्रत करा (&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "येथे लिंक करा (&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "रद्द करा (&A)" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, fuzzy, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "सुरु करा" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "तपशील..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +msgid "Restart" +msgstr "सुरु करा" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +msgid "Name:" +msgstr "नाव :" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "त्रुटी." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "फाईल '%1' उघडू शकत नाही" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "तपशील" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "नवीन संचयीका (&N)..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "नवीन" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "संचयीका काढून टाका (&D)" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "काढून टाका" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "सिंक्रोनाईझ" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "गुणधर्म" + +#: widgets/standardactionmanager.cpp:96 +#, fuzzy, kde-format +msgid "&Paste" +msgstr "चिकटवा (&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "चिटकवा" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "नाव बदला" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "येथे प्रत करा" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, fuzzy, kde-format +msgid "Move To" +msgstr "येथे हलवा" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "घटक कापा (&C)" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "कापा" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "संसाधन काढून टाका" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgid "Synchronize Folder Tree" +msgstr "सिंक्रोनाईझ" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "" +msgstr[1] "घटकांची प्रत करा" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "घटक कापा (&C)" +msgstr[1] "%1 घटक कापा (&C)" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "संचयीका काढून टाका (&D)" +msgstr[1] "%1 संचयीका काढून टाका (&D)" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "" +msgstr[1] "घटक नष्ट करा" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "संचयीका काढून टाका (&D)" +msgstr[1] "%1 संचयीका काढून टाका (&D)" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "सिंक्रोनाईझ" +msgstr[1] "सिंक्रोनाईझ" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "संसाधन काढून टाका (&D)" +msgstr[1] "%1 संसाधने काढून टाका (&D)" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "" +msgstr[1] "घटकांची प्रत करा" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "" +msgstr[1] "घटक कापा" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "" +msgstr[1] "घटक नष्ट करा" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "संचयीका काढून टाका" +msgstr[1] "%1 संचयीका काढून टाका" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "संसाधन काढून टाका" +msgstr[1] "%1 संसाधने काढून टाका" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "सिंक्रोनाईझ" +msgstr[1] "सिंक्रोनाईझ" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "नाव" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "संचयीका काढून टाकायची का?" +msgstr[1] "संचयीका काढून टाकायच्या का?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "" +msgstr[1] "घटक नष्ट करा" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:385 +#, fuzzy, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "नाव :" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:396 +#, fuzzy, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "संसाधन काढून टाका" +msgstr[1] "संसाधने काढून टाका" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, fuzzy, kde-format +msgid "Search:" +msgstr "शोधा :" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "&Subscribed only" +msgstr "सबस्क्राइब" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "सबस्क्राइब" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "&Unsubscribe" +msgstr "सबस्क्राइब" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete" +msgctxt "@title" +msgid "Delete tag" +msgstr "काढून टाका" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete" +msgctxt "@info" +msgid "Delete tag" +msgstr "काढून टाका" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "काढून टाका" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgid "Unable to open data file '%1'." +msgstr "फाईल '%1' उघडू शकत नाही" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "" + +#~ msgid "Id" +#~ msgstr "Id" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "काढून टाका" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "रद्द करा" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "एकूण" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "आकार" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "नाव" + +#~ msgid "KDE Test Program" +#~ msgstr "केडीई चाचणी कार्यक्रम" diff --git a/po/nb/akonadi_knut_resource.po b/po/nb/akonadi_knut_resource.po new file mode 100644 index 0000000..c1d0d78 --- /dev/null +++ b/po/nb/akonadi_knut_resource.po @@ -0,0 +1,86 @@ +# Translation of akonadi_knut_resource to Norwegian Bokmål +# +# Bjørn Steensrud , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-08-16 15:23+0200\n" +"Last-Translator: Bjørn Steensrud \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Ingen datafil valgt." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Fil «%1» vellykket lastet inn." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Velg datafil" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut datafil" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Fant ikke noe element for fjern-id %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Fant ikke foreldersamlingen i DOM-treet." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Klarte ikke skrive samlingen." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Fant ikke endret samling i DOM-treet." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Fant ikke slettet samling i DOM-treet." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Fant ikke foreldersamlingen «%1» i DOM-treet." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Klarte ikke skrive elementet." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Fant ikke endret element i DOM-treet." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Fant ikke slettet element i DOM-treet." diff --git a/po/nb/libakonadi5.po b/po/nb/libakonadi5.po new file mode 100644 index 0000000..9979058 --- /dev/null +++ b/po/nb/libakonadi5.po @@ -0,0 +1,2554 @@ +# Translation of libakonadi5 to Norwegian Bokmål +# +# Bjørn Steensrud , 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2014-09-23 21:56+0200\n" +"Last-Translator: Bjørn Steensrud \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.5\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Klaus Ade Johnstad" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "klaus@skolelinux.no" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Klarte ikke å registrere objeket ved dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 av type %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agentidentifikasjon" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Klar" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Frakoblet" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synkroniserer …" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Feil." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Ikke satt opp" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadiressurs" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Ugyldig element hentet" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Feil ved oppretting av element: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Feil mens samling blir oppdatert: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Klarte ikke å oppdatere lokal samling: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Klarte ikke å oppdatere lokale elementer: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Kan ikke hente element når frakoblet" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synkroniserer mappe «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Forespurte element finnes ikke lenger" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Jobb avbrutt." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Ingen slik samling." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Fant foreldreløse samlinger uten løsning" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Fant ikke det andre elementet for konflikthåndtering" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Klarte ikke kontakte D-Bus-grensesnittet for den opprettede agenten." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Tidsavbrudd ved start av en agentinstans." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Klarte ikke skaffe agent type «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Klarte ikke starte en instans av agenten." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Ugyldig samlingsinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Ugyldig ressursinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Klarte ikke skaffe D-Bus-grensesnitt for ressurs «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Tidsavbrudd ved synkronisering av samlingsattributter." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Ugyldig forelder" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Ugyldig samling" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Ugyldig samling oppgitt." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Ingen objekter oppgitt for flytting" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Det er ikke oppgitt noe gyldig mål" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Ugyldig samling." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Kan ikke koble til Akonadi-tjenesten." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Protokollversjonen til Akonadi-tjeneren er ikke kompatibel. Pass på at du " +"har en kompatibel versjon installert." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Bruker avbrøt handlinga." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Ukjent feil." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tidsavbrudd ved ressurs-synkronisering." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Kan ikke hente rotsamlingen for ressurs %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Ingen ressurs-ID gitt." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Ugyldig ressursidentifikasjon «%1»" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Klarte ikke å sette opp standardressurs via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Klarte ikke å hente ressurssamlingen." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tidsavbrudd ved forsøk på å låse." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Klarte ikke opprette etikett." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Klarte ikke å flytte til søppelsamling, avbryter søppelhandling" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Ugyldige elementet sendt" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Ugyldig samling sendt" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Ingen gyldig samling eller tom elementliste" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Klarte ikke å finne gjenopprettingssamlingen og gjenopprettingsressurs er " +"ikke tilgjengelig" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Navn" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Målsamlingen «%1» inneholdder fra før\n" +"en samling som heter «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Navn" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Favorittmapper" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Totalt antall meldinger" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Uleste meldinger" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvote" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Lagerstørrelse" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Størrelse for undermapper" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Ulest" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Totalt" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Størrelse" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etikett" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Kan ikke hente element for indeks" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indeks ikke lenger tilgjengelig" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Nyttelastdelen «%1» er ikke tilgjengelig for denne indeksen" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Ingen økt tilgjengelig for denne indeksen" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Ikke noe element tilgjengelig for denne indeksen" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Programtillegg uten navn." + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Ingen beskrivelse tilgjengelig" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi selvtest" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Kontrollerer og rapporterer status for Akonaditjeneren" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Ny agentinstans …" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Slett agentinstans" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Sett opp agentinstans" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Ny agentinstans" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Klarte ikke å opprette en instans av agenten: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Opprette en agentinstans mislyktes" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Slett agentinstans?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Er du sikker på at du vil slette den valgte agentinstansen?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minutt" +msgstr[1] "minutter" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Henting" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Bruk valg fra foreldre-mappe eller konto" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synkroniser når denne mappa velges" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synkroniser automatisk etter:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Aldri" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutter" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokalt mellomlagrede deler" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Valg for framhenting" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Behold meldingskropper lokalt i:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "For alltid" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Søk" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Bruk mappe som standard" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Ny undermappe …" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Lag ny undermappe under den valgte mappa" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Ny mappe" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Navn" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Klarte ikke opprette mappe: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Mappeoppretting mislyktes" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Generelt" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Ett objekt" +msgstr[1] "%1 objekter" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Navn:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Br&uk selvvalgt ikon:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "mappe" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistikk" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Innhold:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objekter" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Størrelse:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Mappetype:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "ukjent" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementer" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ingen mappe" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Åpne samlingsdialog" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Flytt hit" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopier hit" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Avbryt" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Endringstidspunkt" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flagg" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attributt: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Konfliktløsning" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Data" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Starter Akonadi-tjener …" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stopper Akonadi-tjener …" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Flytt hit" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopier hit" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Lag lenke hit" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Avbryt" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Tjenesten for håndtering av personlig informasjon starter …" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Tjenesten for personlig informasjonsbehandling avslutter …" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Tjenesten for håndtering av personlig informasjon kjører en database-" +"oppgradering." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Tjenesten for håndtering av personlig informasjon kjører en database-" +"oppgradering.\n" +" Dette hender etter en programvareoppdatering og er nødvendig for å sikre " +"god ytelse.\n" +" Dette kan ta noen minutter, avhengig av mengden personlige opplysninger." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi rammeverk for håndtering av personlig informasjon er ikke i drift. " +"Dette programmet kan ikke brukes uten det." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Start" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi rammeverk for håndtering av personlig informasjon er ikke i drift.\n" +" Trykk på «Detaljer …» for å få detaljerte opplysninger om hva som er galt." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Akonadi-tjenesten for håndtering av personlig informasjon er ikke i drift." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detaljer …" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Vil du fjerne kontoen «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Fjern konto?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Innkommende kontoer (legg til minst en):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Endre …" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "Fj&ern" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Nylig brukt mappe" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Lagre rapport …" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopier rapport til utklippstavla" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Ditt gjeldende oppsett for Akonadi-tjeneren krever QtSQL-driveren «%1» som " +"ble funnet på systemet." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Ditt gjeldende oppsett for Akonadi-tjeneren krever QtSQL-driveren «%1».\n" +"Følgende drivere er installert: %2.\n" +"Se etter at den nødvendige driveren er installert." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Fant database-driver." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Fant ikke database-driver." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL-tjenerprogrammet er ikke testet." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Det gjeldende oppsettet krever ikke en intern MySQL-tjener." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"For tiden har du satt opp Akonadi til å bruke MySQL-tjeneren «%1».\n" +"Pass på at du har MySQL-tjeneren installert, satt riktig søkesti og se etter " +"at du har nødvendig lese- og kjørerettighet til tjenerprogrammet. " +"Tjenerprogrammet heter typisk «mysqld», hvor det bor avhenger av " +"distribusjonen." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Fant ikke MySQL-tjener." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-tjener kan ikke leses." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-tjener kan ikke kjøres." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL funnet med uventet navn." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Fant MySQL-tjener." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Fant MySQL-tjener: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-tjener kan kjøres." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Det lyktes ikke å kjøre MySQL-tjeneren «%1», med denne feilmeldinga: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Det lyktes ikke å kjøre MySQL-tjeneren." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL-tjenerens feillogg ikke testet." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Fant ingen gjeldende feillogg for MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL-tjeneren meldte ikke om noen feil under denne oppstarten. Loggen kan " +"finnes i «%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-feilloggen kan ikke leses." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Det fantes en feillogg fra MySQL-tjeneren, men den er ikke lesbar: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-tjenerens logg inneholder feil." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL-tjenerens feillogg-fil «%1» inneholder feil." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-tjenerens logg inneholder advarsler." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL-tjenerens loggfil «%1» inneholder advarsler." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-tjenerens loggfil inneholder ingen feil." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL-tjenerens loggfil «%1» inneholder ingen feil eller advarsler." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-tjenerens oppsett er ikke testet." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Fant ikke MySQL-tjenerens standardoppsett." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Standardoppsettet for MySQL-tjeneren ble funnet ved %1 og er lesbart." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Fant ikke MySQL-tjenerens standardoppsett." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Standardoppsettet for MySQL-tjeneren ble ikke funnet eller var ikke lesbart. " +"Se etter at Akonadi-installasjonen er fiullstendig og at du har alle " +"nødvendige tilgangsrettigheter." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL-tjenerens tilpassede oppsett er ikke tilgjengelig." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "MySQL-tjenerens tilpassede oppsett ble ikke funnet, men er valgfritt." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL-tjenerens tilpassede oppsett ble funnet." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "MySQL-tjenerens tilpassede oppsett ble funnet på %1 og er lesbart." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL-tjenerens tilpassede oppsett er ikke lesbart." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL-tjenerens tilpassede oppsett ble funnet på %1 men kan ikke leses. " +"Sjekk tilgangsrettene." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL-tjenerens oppsett ble ikke funnet eller er ikke lesbart." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL-tjenerens oppsett ble ikke funnet eller er ikke lesbart." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL-tjenerens oppsett kan brukes." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL-tjenerens oppsett ble funnet på %1 og er lesbart." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Kan ikke koble til PostgreSQL-tjener" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL-tjener funnet" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL-tjeneren ble funnet og tilkoblingen virker." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl ikke funnet" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Programmet «akonadictl» må være tilgjengelig i $PATH. Pass på at du har " +"Akonadi-tjeneren installert." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl funnet og brukbar" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Programmet «%1» som skal kontrollere Akonadi-tjeneren ble funnet og kan " +"kjøres.\n" +"Resultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl funnet men ikke brukbar" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Programmet «%1» som skal kontrollere Akonadi-tjeneren ble funnet men kan " +"ikke kjøres.\n" +"Resultat:\n" +"%2\n" +"Pass på at Akonadi-tjeneren er riktig installert." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Kontrollprosessen for Akonadi er registrert hos D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Kontrollprosessen for Akonadi er registrert hos D-Bus, det betyr som regel " +"at den virker." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Kontrollprosessen for Akonadi er ikke registrert hos D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Kontrollprosessen for Akonadi er ikke registrert hos D-Bus, det betyr som " +"regel at den ikke ble startet eller støtte på en kritisk feil under " +"oppstarten." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi tjenerprosessen er registrert hos D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-tjenerprosessen er registrert hos D-Bus, det betyr som regel at den " +"virker." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi tjenerprosessen er ikke registrert hos D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-tjenerprosessen er ikke registrert hos D-Bus, det betyr som regel at " +"den ikke er startet eller støtte på en kritisk feil under oppstarten." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Ikke mulig å sjekke protokollversjon" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Uten forbindelse til tjeneren er det ikke mulig å undersøke om " +"protokollversjonen fyller kravene." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Tjenerens protokollversjon er for gammel." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Ressursagenter funnet." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Minst én ressursagent er funnet." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Ingen ressursagenter funnet." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Ingen ressursagenter er funnet. Akonadi kan ikke brukes uten minst én. Dette " +"betyr som regel at det ikke er installert noen ressursagenter, eller at det " +"er et problem i oppsettet. Følgende stier er blitt undersøkt: «%1». " +"Miljøvariablen XDG_DATA_DIRS er satt til «%2», pass på at dette inkluderer " +"alle stier der Akonadi-agenter er installert." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Fant ingen feillogg for Akonaditjener." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonaditjeneren ga ingen feilmeldinger i løpet av denne oppstarten." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Ingen gjeldende feillogg funnet for Akonadi-tjeneren." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-tjeneren ga feilmeldinger i løpet av denne oppstarten. Loggen kan " +"finnes i %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Fant ingen tidligere feillogg fra Akonadi-tjeneren." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi-tjeneren meldte ikke om noen feil under forrige oppstart." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Fant tidligere feillogg for Akonadi-tjeneren." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-tjeneren ga feilmeldinger da den sist ble startet. Loggen kan " +"finnes i %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Fant ingen gjeldende feillogg fra kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Kontrollprosessen for Akonadi ga ingen feilmeldinger under gjeldende " +"oppstart." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Fant gjeldende feillogg fra kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Kontrollprosessen for Akonadi ga feilmeldinger under gjeldende oppstart. " +"Loggen kan finnes i %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Fant ingen tidligere feillogg fra kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Kontrollprosessen for Akonadi ga ingen feilmeldinger da den sist ble startet." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Fant feillogg fra siste kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Kontrollprosessen for Akonadi ga feilmeldinger da den sist ble startet. " +"Loggen kan finnes i %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonati ble startet som root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Det åpner for mange sikkerhetsrisikoer å kjøre programmer som root når de " +"vender mot Internett. Denne Akonadi-installasjonen bruker MySQL, som ikke " +"tillater kjøring som root, for å beskytte mot slik risiko." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi kjører ikke som root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi kjører ikke som root/administrator, og det er det anbefalte " +"oppsettet for et sikkert system." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Lagre testrapport" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Feil" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Klarte ikke åpne fila «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Det oppsto en feil under oppstart av Akonadi-tjeneren. Følgende selvtester " +"bør være til hjelp for å spore opp og løse dette problemet. Når du ber om " +"hjelp eller sender inn feilrapport, så ta alltid med denne rapporten." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaljer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Ny mappe …" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Ny" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +msgid "&Delete Folder" +msgstr "&Slett mappe" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Slett?" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +msgid "&Synchronize Folder" +msgstr "&Synkroniser mappe" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synkroniser" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Mappe&egenskaper" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Egenskaper" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Lim inn" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Lim inn" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Håndter lokale &abonnementer …" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Håndter lokale abonnementer" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Legg til favorittmapper" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Legg til favoritt" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Fjern fra Favorittmapper" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Fjern fra Favoritt" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Endre navn på favoritt …" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Endre navn" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopier mappe til …" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopier til" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopier element til …" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Flytt element til …" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Flytt til" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Flytt mappe til …" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +msgid "&Cut Item" +msgstr "&Klipp ut element" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Klipp ut" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +msgid "&Cut Folder" +msgstr "&Klipp ut mappe" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Opprett ressurs" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +msgid "Delete Resource" +msgstr "Slett ressurs" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Ressursegenskaper" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +msgid "Synchronize Resource" +msgstr "Synkroniser ressurs" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Arbeid frakoblet" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synkroniser mappe rekursivt" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synkroniser rekursivt" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Flytt mappa til papirkurven" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Flytt mappa til papirkurven" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Flytt element til papirkurven" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Flytt element til papirkurven" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Gjenopp&rett mappe fra papirkurven" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Gjenopprett mappe fra papirkurven" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Gjenopp&rett element fra papirkurven" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Gjenopprett element fra papirkurven" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Gjenopp&rett samling fra papirkurven" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Gjenopprett samling fra papirkurven" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synkroniser favorittmappes" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synkroniser favorittmappes" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopier mappe" +msgstr[1] "&Kopier %1 mapper" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopier element" +msgstr[1] "&Kopier %1 elementer" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Slett element" +msgstr[1] "&Slett %1 elementer" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Slett ressurs" +msgstr[1] "&Slett %1 ressurser" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synkroniser ressurs" +msgstr[1] "&Synkroniser %1 ressurser" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopier mappe" +msgstr[1] "Kopier %1 mapper" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopier element" +msgstr[1] "Kopier %1 elementer" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Klipp ut element" +msgstr[1] "Klipp ut %1 elementer" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Klipp ut mappe" +msgstr[1] "Klipp ut %1 mapper" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Slett element" +msgstr[1] "Slett %1 elementer" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Slett mappe" +msgstr[1] "Slett %1 mapper" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synkroniser mappe" +msgstr[1] "Synkroniser %1 mapper" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Er du sikker på at du vil slette denne mappa og alle undermapper i den?" +msgstr[1] "" +"Er du sikker på at du vil slette «%1» mapper og alle undermapper i dem?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Slett mappe?" +msgstr[1] "Slett mapper?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Klarte ikke slette mappe: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Mappesletting mislyktes" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Egenskaper for mappa %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Er du sikker på at du vil slette det markerte elementet?" +msgstr[1] "Er du sikker på at du vil slette %1 elementer?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Slett element?" +msgstr[1] "Slett elementer?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Klarte ikke slette element: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Elementsletting mislyktes" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Endre navn på favoritt" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Navn:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Ny ressurs" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Klarte ikke opprette ressurs: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Ressursoppretting mislyktes" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Er du sikker på at du vil slette denne ressursen?" +msgstr[1] "Er du sikker på at du vil slette %1 ressurser?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Slett ressurs?" +msgstr[1] "Slett ressurser?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Klarte ikke lime inn data: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Innliming mislyktes" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Vi kan ikke legge til «/» i mappenavn." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Feil ved oppretting av ny mappe." + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Vi kan ikke legge til «.» i begynnelsen eller slutten av mappenavn." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Før mappa «%1» kan synkroniseres må ressursen være tilkoblet. Vil du koble " +"den til nå?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Konto «%1» er frakoblet" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Koble til" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Flytt til denne mappa" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopier til denne mappa" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Søk:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Klarte ikke opprette en ny etikett" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Det oppstod en feil mens en ny etikett ble opprettet" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Er du sikker på at du vil fjerne etiketten %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Slett etikett" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Slett etikett" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "…" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi til XML-konverterer" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Gjør om et under-tre av en Akonadi-samling til en XML-fil." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Ingen data lastet inn." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Filnavn ikke oppgitt." + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Kan ikke åpne datafil «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Fila «%1» finnes ikke." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Klarer ikke tolke datafil «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Klarte ikke laste inn og tolke skjemadefinisjonen." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Klarte ikke å opprette kontekst for skjematolker." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Klarte ikke å opprette skjema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Klarte ikke å opprette kontekst for skjemavalidering." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Ugyldig filformat." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Klarer ikke tolke datafil: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Klarer ikke finne samling %1" diff --git a/po/nds/akonadi_knut_resource.po b/po/nds/akonadi_knut_resource.po new file mode 100644 index 0000000..1d6583e --- /dev/null +++ b/po/nds/akonadi_knut_resource.po @@ -0,0 +1,92 @@ +# translation of akonadi_knut_resource.po to Low Saxon +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Manfred Wiese , 2009, 2010. +# Sönke Dibbern , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-07-29 15:51+0200\n" +"Last-Translator: Manfred Wiese \n" +"Language-Team: Low Saxon \n" +"Language: nds\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.1\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Keen Datendatei utsöcht" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Datei \"%1\" mit Spood laadt." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Datendatei utsöken" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi-Knut-Datendatei" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "För feern ID \"%1\" lett sik keen Indrag finnen." + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Överornt Sammeln nich binnen DOM-Boom funnen." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Sammeln lett sik nich schrieven." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Ännert Sammeln nich binnen DOM-Boom funnen." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Wegdaan Sammeln nich binnen DOM-Boom funnen." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Överornt Sammeln \"%1\" nich binnen DOM-Boom funnen." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Indrag lett sik nich schrieven." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Ännert Indrag nich binnen DOM-Boom funnen." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Wegdaan Indrag nich binnen DOM-Boom funnen." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Padd na de Knut-Datendatei" + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Bitte de aktuellen Hülpprogramm-Daten nich ännern." diff --git a/po/nds/libakonadi5.po b/po/nds/libakonadi5.po new file mode 100644 index 0000000..49263db --- /dev/null +++ b/po/nds/libakonadi5.po @@ -0,0 +1,2739 @@ +# Translation of libakonadi.po to Low Saxon +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Sönke Dibbern , 2007, 2008, 2009, 2014. +# Manfred Wiese , 2009, 2010, 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2014-09-18 22:53+0200\n" +"Last-Translator: Sönke Dibbern \n" +"Language-Team: Low Saxon \n" +"Language: nds\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.4\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Sönke Dibbern, Manfred Wiese" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "s_dibbern@web.de, m.j.wiese@web.de" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Objekt lett sik bi D-Bus nich utmellen: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 vun'n Typ %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Hölperkennen" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-Hölper" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Praat" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Afkoppelt" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Bi to synkroniseren..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Fehler." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nich instellt" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Ressourcekennen" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Akonadi-Ressource" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Leeg Element haalt" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Fehler bi't Opstellen vun en Indrag: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Fehler bi't Opfrischen vun en Sammeln: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Opfrischen vun en lokaal Sammeln is fehlslaan: %1" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Opfrischen vun lokaal Indrääg is fehlslaan: %1" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Element lett sik bi Afkokppelbedrief nich halen" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Orner \"%1\" warrt synkroniseert" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for sync." +msgstr "De Ressource-Sammeln lett sik nich halen." + +#: agentbase/resourcebase.cpp:983 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for attribute sync." +msgstr "De Ressource-Sammeln lett sik nich halen." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Anfraagt Objekt nich mehr vörhannen." + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Opgaav afbraken" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Dat gifft disse Sammeln nich." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Nich oplööst \"verlaten\" Sammeln opdeckt" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Keen anner Indrag för Konfliktenbedoon funnen." + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "D-Bus-Koppelsteed vun opstellt Hölper lett sik nich faatkriegen." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Tietafloop bi't Opstellen vun den Hölper" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Hölper-Typ \"%1\" lett sik nich halen." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Hölper lett sik nich opstellen." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Leeg Sammelnutgaav" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Leeg Ressource-Utgaav." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "D-Bus-Koppelsteed na Ressource \"%1\" lett sik nich faatkriegen" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Tietafloop bi't Synkroniseren vun Sammelnattributen" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Leeg Sammeln" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Leeg Sammeln" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Leeg Överornen" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to parse Collection from response" +msgstr "De Ressource-Sammeln lett sik nich halen." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Leeg Sammeln" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Leeg Sammeln angeven" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Keen Objekten för't Verschuven angeven" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Keen gellen Teel angeven" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Leeg Sammeln" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Leeg Sammeln" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Tokoppeln na den Akonadi-Deenst nich mööglich" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"De Protokollverschoon vun den Akonadi-Server is nich kompatibel. Beseker " +"bitte, Du hest en kompatibel Verschoon installeert." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Bruker hett Akschoon afbraken." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Nich begäng Fehler." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Failed to create tag." +msgid "Failed to create relation." +msgstr "Slötelwoort lett sik nich opstellen." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tietafloop bi't Synkroniseren vun en Ressource" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Wörtel-Sammeln vun Ressource \"%1\" lett sik nich halen." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Keen Ressource-ID angeven." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Leeg Ressourcekennen \"%1\"" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Standard-Ressource lett sik nich över D-Bus instellen." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "De Ressource-Sammeln lett sik nich halen." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tiet bi't Halen vun Slott aflopen." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Slötelwoort lett sik nich opstellen." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Verschuven na Affalltünn fehlslaan. Akschoon warrt afbraken." + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Leeg Elementen övergeven" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Leeg Sammeln övergeven" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Keen tolaten Sammeln oder de Elementenlist bargt keen Indrääg" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Sammeln lett sik nich wedderherstellen, de Ressource för't Wedderherstellen " +"lett sik nich finnen." + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Naam" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "An't Laden…" + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Fehler." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"De Teelsammeln \"%1\" bargt al\n" +"en Sammeln mit den Naam \"%2\"." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Naam" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Indrag lett sik nich koperen:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Sammeln lett sik nich koperen:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Indrag lett sik nich verschuven:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Sammeln lett sik nich verschuven:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Link na Entiteet lett sik nich opstellen:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Fehler." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Vörtrocken Ornern" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Narichten tosamen" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Nich leest Narichten" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Bruukgrenz" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Spiekergrött" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Ünnerorner-Spiekergrött" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Nich leest" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Tosamen" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Grött" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Slötelwoort" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Indrag för dissen Index lett sik nich halen." + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index is nich mehr verföögbor" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Bruukdatendeel \"%1\" is för dissen Index nich verföögbor." + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Keen Törn för dissen Index verföögbor" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Keen Indrag för dissen Index verföögbor" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Moduul ahn Naam" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Keen Beschrieven verföögbor" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi-Egenprööv" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Pröövt den Akonadi-Server sien Tostand un gifft dat ut" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nieg Hölper..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Indrag &wegdoon" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Hölper &inrichten" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nieg Hölper" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Hölper lett sik nich opstellen: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Opstellen vun den Hölper fehlslaan" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Hölper wegdoon?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Wullt Du den utsöchten Hölper redig wegdoon?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Failed to create tag." +msgid "Failed to register %1 configuration dialog." +msgstr "Slötelwoort lett sik nich opstellen." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "Minuut" +msgstr[1] "Minuten" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Halen" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Optschonen vun överornt Orner oder Konto bruken" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Bi't Utsöken vun dissen Orner synkroniseren" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automaatsch synkroniseren na:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nienich" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "Minuten" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokaal wohrt Delen" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Afhaal-Optschonen" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "Jümmers hele Narichten halen" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "Hööftdelen op Nafraag halen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Hööftdelen lokaal wohren för:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Duerhaftig" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Söken" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Standardwies Orner bruken" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nieg Ünnerorner..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "En niegen Ünnerorner binnen den opstunns utsöchten Orner opstellen" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nieg Orner" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Naam" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Orner \"%1\" lett sik nich opstellen" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Opstellen vun den Orner fehlslaan" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Allgemeen" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Een Objekt" +msgstr[1] "%1 Objekten" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Naam:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Egen Lüttbild &bruken:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "Orner" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistik" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Inholt:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 Objekten" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Grött:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bytes" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "Fehler bi't Opstellen vun en Indrag: %1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "Orner-&Egenschappen" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "Indrag knippen" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Narichten tosamen" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Nich leest Narichten" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "Verleden Orner" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Keen Orner" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Sammeln-Dialoog opmaken" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "En Sammeln utsöken" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Hierhen &verschuven" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Hierhen &koperen" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Afbreken" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Tiet vun de Ännern" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Marken" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Konfliktlösen" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "Den rechten Indrag wohren" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Bede wohren" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Daten" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi-Server warrt opropen..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi-Server warrt anhollen..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Hierhen &verschuven" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "Hierhen &koperen" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Hierhen en &Link maken" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Afbreken" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Tokoppeln na den Akonadi-Deenst nich mööglich" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "De Deenst för de Pleeg vun persöönlich Informatschonen warrt start..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" +"De Deenst för de Pleeg vun persöönlich Informatschonen warrt utmaakt..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"De Deenst för de Pleeg vun persöönlich Informatschonen föhrt en " +"Datenbankopfrischen dör." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"De Deenst för de Pleeg vun persöönlich Informatschonen föhrt en " +"Datenbankopfrischen dör.\n" +"Dit deit na en Software-Opfrischen för't Verbetern vun de Leisten noot.\n" +"Jüst dor na, wo veel persöönlich Daten Du hest, mag dat en poor Minuten " +"bruken." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"De Akonadi-Deenst för de Pleeg vun persöönliche Informatschonen löppt nich. " +"Ahn em lett sik dit Programm man nich bruken." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Start" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Dat Rahmenwark för de Pleeg vun persöönlich Informatschonen - Akonadi - " +"funkscheneert nich.\n" +"Klick op \"Enkelheiten\", wenn Du mehr över dat Problem weten wullt." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"De Akonadi-Deenst för de Pleeg vun persöönlich Informatschonen löppt nich " +"propper" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Enkelheiten..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "Start" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Verleden Orner" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Leesteken ümnömen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Naam:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi-Server-Egenprööv" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Bericht sekern..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Bericht na Twischenaflaag koperen" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"För Dien aktuelle Akonadi-Serverinstellen deit de Qt-SQL-Driever \"%1\" " +"noot, un he ok wöör op Dien Systeem funnen." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Dien aktuelle Akonadi-Serverinstellen bruukt den Qt-SQL-Driever \"%1\".\n" +"Disse Drievers sünd installeert: %2\n" +"Beseker bitte, de nödige Driever is installeert." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Datenbankdriever funnen." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Datenbankdriever nich funnen." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL-Serverprogramm nich utprobeert." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "De aktuellen Instellen maakt keen intern MySQL-Server nödig." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Opstunns is Akonadi so instellt, dat he den MySQL-Server \"%1\" bruukt.\n" +"Beseker bitte, Du hest den MySQL-Server installeert, legg de PATH-Variabel " +"propper fast un prööv, wat Du de nödigen Lees- un Utföhrverlöven för dat " +"Serverprogramm hest. Normalerwies heet dat Serverprogramm \"mysqld\", sien " +"Steed hangt vun de Distributschoon af." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-Server nich funnen." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-Server lett sik nich lesen." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-Server lett sik nich utföhren." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL mit nich verwacht Naam funnen" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL-Server funnen." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL-Server funnen: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-Server lett sik utföhren." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Dat Opropen vun den MySQL-Server \"%1\" is mit disse Fehlermellen fehlslaan: " +"%2" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Oproop vun den MySQL-Server fehlslaan." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Fehlerlogbook vun den MySQL-Server nich utprobeert." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Keen aktuell Fehlerlogbook för den MySQL-Server funnen" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"De MySQL-Server hett bi dissen Start keen Fehlers meldt. Dat Logbook lett " +"sik in \"%1\" finnen." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-Fehlerlogbook lett sik nich lesen." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"En Fehlerlogbook vun den MySQL-Server wöör funnen, man lett sik nich lesen: " +"%1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Dat MySQL-Server-Fehlerlogbook bargt Fehlers." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Dat MySQL-Server-Fehlerlogbook \"%1\" bargt Fehlers." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Dat MySQL-Server-Fehlerlogbook bargt Wohrschoen." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Dat MySQL-Server-Fehlerlogbook \"%1\" bargt Wohrschoen." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Dat MySQL-Server-Fehlerlogbook bargt keen Fehlers." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Dat MySQL-Server-Fehlerlogbook \"%1\" bargt keen Fehlers oder Wohrschoen." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-Server-Instellen nich utprobeert." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Standardinstellen för den MySQL-Server funnen." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"De Standardinstellen för den MySQL-Server wöörn funnen (\"%1\") un laat sik " +"lesen." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "De Standardinstellen för den MySQL-Server laat sik nich finnen." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"De Standardinstellen för den MySQL-Server laat sik nich finnen oder nich " +"lesen. Prööv bitte, wat Akonadi propper installeert is un wat Du all de " +"nödigen Togriepverlöven hest." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Keen topasst Instellen för den MySQL-Server verföögbor." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"De topassten Instellen för den MySQL-Server laat sik nich finnen, man sünd " +"so un so köörwies." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Topasst Instellen för den MySQL-Server funnen." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"De topassten Instellen för den MySQL-Server wöörn funnen (\"%1\") un laat " +"sik lesen" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "De topassten Instellen för den MySQL-Server laat sik nich lesen." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"De topassten Instellen för den MySQL-Server wöörn funnen (\"%1\"), man laat " +"sik nich lesen. Prööv bitte Dien Togriepverlöven." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" +"De Instellen för den MySQL-Server laat sik nich finnen oder nich lesen." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"De Instellen för den MySQL-Server laat sik nich finnen oder nich lesen." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "De Instellen för den MySQL-Server laat sik bruken." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"De Instellen för den MySQL-Server wöörn funnen (\"%1\") un laat sik lesen." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Tokoppeln na den PostgreSQL-Server nich mööglich" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL-Server funnen." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "De PostgreSQL-Server wöör funnen, de Verbinnen funkscheneert." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "\"akonadictl\" nich funnen" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Dat Programm \"akonadictl\" mutt sik över Dien PATH-Variabel finnen laten. " +"Beseker bitte, Du hest den Akonadi-Server installeert." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "\"akonadictl\" funnen un lett sik bruken" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Dat Programm \"%1\", dat den Akonadi-Server stüert, wöör funnen un mit Spood " +"opropen.\n" +"Resultaat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "\"akonadictl\" funnen, man lett sik nich bruken" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Dat Programm \"%1\", dat den Akonadi-Server stüert, wöör funnen, man lett " +"sik nich mit Spood opropen.\n" +"Resultaat:\n" +"%2\n" +"Beseker bitte, de Akonadi-Server is propper installeert" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi-Stüerperzess bi den D-Bus inmeldt." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"De Akonadi-Stüerperzess is bi den D-Bus inmeldt, normalerwies funkscheneert " +"he denn." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi-Stüerperzess nich bi den D-Bus inmeldt" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"De Akonadi-Stüerperzess is bi den D-Bus nich inmeldt, normalerwies wöör he " +"denn gor nich opropen oder dat geev bi't Starten en swoor Fehler." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi-Serverperzess bi den D-Bus inmeldt" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"De Akonadi-Serverperzess is bi den D-Bus inmeldt, normalerwies funkscheneert " +"he denn." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi-Serverperzess bi den D-Bus nich inmeldt" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"De Akonadi-Serverperzess is bi den D-Bus nich inmeldt, normalerwies wöör he " +"denn gor nich opropen oder dat geev bi't Starten en swoor Fehler." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protokoll-Verchoon lett sik nich pröven." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Ahn en Verbinnen na den Server lett sik dat nich pröven, wat dat Protokoll " +"sien Verschoon groot noog is." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Server-Protokollverschoon is to oolt." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Den Server sien Protokollverschoon is %1, man tominnst Verschoon %2 deit " +"noot. Installeer bitte en nieger Verschoon vun den Akonadi-Server." + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "Server-Protokollverschoon is to oolt." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "Server-Protokollverschoon is to oolt." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "Server-Protokollverschoon is to oolt." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Ressource-Hölpers funnen" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Tominnst een Ressource-Hölper lett sik finnen." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Keen Ressource-Hölpers funnen" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Keen Ressource-Hölpers laat sik finnen. Akonadi lett sik nich bruken, wenn " +"dat nich tominnst een gifft. Normalerwies sünd denn keen Ressource-Hölpers " +"installeert, oder dat gifft en Problem mit de Instellen. Disse Padden wöörn " +"dörkeken: %1. De Ümgevenvariabel \"XDG_DATA_DIRS\" hett den Weert \"%2\". " +"Beseker bitte, dat all Akonadi-Hölpers na Ornern dor binnen installeert sünd." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Keen aktuell Fehlerloogbook för en Akonadi-Server funnen" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "De Akonadi-Server hett bi dissen Start keen Fehlers meldt." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Aktuell Fehlerloogbook för Akonadi-Server funnen" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"De Akonadi-Server hett bi dissen Start Fehlers meldt. Dat Logbook lett sik " +"in \"%1\" finnen." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Keen verleden Fehlerloogbook för Akonadi-Server funnen" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "De Akonadi-Server hett bi sien verleden Start keen Fehlers meldt." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Verleden Fehlerloogbook för Akonadi-Server funnen" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"De Akonadi-Server hett bi sien verleden Start Fehlers meldt. Dat Logbook " +"lett sik in \"%1\" finnen." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Keen aktuell Fehlerlogbook för Akonadi-Stüern funnen" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "De Akonadi-Stüerperzess hett bi sien Start keen Fehlers meldt." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Aktuell Fehlerlogbook för Akonadi-Stüern funnen" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"De Akonadi-Stüerperzess hett bi sien Start Fehlers meldt. Dat Logbook lett " +"sik in \"%1\" finnen." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Keen verleden Fehlerlogbook för Akonadi-Stüern funnen" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"De Akonadi-Stüerperzess hett bi sien verleden Start keen Fehlers meldt." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Verleden Fehlerlogbook för Akonadi-Stüern funnen" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"De Akonadi-Stüerperzess hett bi sien verleden Start Fehlers meldt. Dat " +"Logbook lett sik in \"%1\" finnen." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi wöör in'n Systeemplegerbedrief start." + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Warrt en Programmen mit Internet-Togriep in'n Systeemplegerbedrief utföhrt, " +"kann dat en Riskanz för de Sekerheit wesen. För Dien Sekerheit lett sik dat " +"vun disse Akonadi-Installatschoon bruukt MYSQL nich as Systempleger utföhren." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi löppt nich in'n Systeemplegerbedrief." + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi löppt nich in'n Systeemplegerbedrief. Disse Instellen warrt för en " +"seker Systeem anraadt." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Pröövbericht sekern" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Fehler." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Datei \"%1\" lett sik nich opmaken." + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Bi't Starten vun den Akonadi-Server hett dat en Fehler geven. De nakamen " +"Egenpröven schöölt dat Problem ingrenzen un bi't Lösen hölpen. Wenn Du " +"Fehlers künnig maken oder na Ünnerstütten fragen wullt, legg dissen Bericht " +"bitte bi." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Enkelheiten" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Mehr Tipps för't Problemlösen gifft dat op userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nieg Orner..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nieg" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "Orner &wegmaken" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Wegmaken" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "Orner &synkroniseren" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synkroniseren" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Orner-&Egenschappen" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Egenschappen" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Infögen" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Infögen" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "&Lokale Bestellen plegen" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Lokale Bestellen plegen" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Na Leestekenornern tofögen" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Na Leestekens tofögen" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Ut Leestekenornern wegmaken" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Ut Leestekens wegmaken" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Vörtrocken Orner ümnömen..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Ümnömen" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Orner koperen na..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Koperen na" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Indrag koperen na..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Indrag verschuven na..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Verschuven na" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Orner verschuven na..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "Indrag &knippen" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Knippen" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "Orner &knippen" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Ressource opstellen" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Ressource wegdoon" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Ressource-Egenschappen" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Ressource synkroniseren" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Afkoppelt arbeiden" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Ok Ünnerornern &synkroniseren" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Rekursiev synkroniseren" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Orner na &Affalltünn verschuven" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Orner na Affalltünn verschuven" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Indrag na &Affalltünn verschuven" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Indrag na Affalltünn verschuven" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Orner ut Affall &wedderherstellen" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Orner ut Affall wedderherstellen" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Indrag ut Affall &wedderherstellen" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Indrag ut Affall wedderherstellen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Sammeln ut Affall &wedderherstellen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Sammeln ut Affall wedderherstellen" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Vörtrocken Ornern &synkroniseren" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Vörtrocken Ornern synkroniseren" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize Folder Tree" +msgstr "Orner synkroniseren" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Orner &koperen" +msgstr[1] "%1 Ornern &koperen" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Indrag &koperen" +msgstr[1] "%1 Indrääg &koperen" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Indrag &knippen" +msgstr[1] "%1 Indrääg &knippen" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Orner &knippen" +msgstr[1] "%1 Ornern &knippen" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Indrag &wegdoon" +msgstr[1] "%1 Indrääg &wegdoon" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Orner &wegmaken" +msgstr[1] "%1 Ornern &wegmaken" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Orner &synkroniseren" +msgstr[1] "%1 Ornern &synkroniseren" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Ressource &wegmaken" +msgstr[1] "%1 Ressourcen &wegmaken" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Ressource &synkroniseren" +msgstr[1] "%1 Ressourcen &synkroniseren" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Orner koperen" +msgstr[1] "%1 Ornern koperen" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Indrag koperen" +msgstr[1] "%1 Indrääg koperen" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Indrag knippen" +msgstr[1] "%1 Indrääg knippen" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Orner knippen" +msgstr[1] "%1 Ornern knippen" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Indrag wegdoon" +msgstr[1] "%1 Indrääg wegdoon" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Orner wegdoon" +msgstr[1] "%1 Ornern wegdoon" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Orner synkroniseren" +msgstr[1] "%1 Ornern synkroniseren" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Ressource wegdoon" +msgstr[1] "%1 Ressourcen wegdoon" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Ressource synkroniseren" +msgstr[1] "%1 Ressourcen synkroniseren" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Naam" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Wullt Du dissen Orner un all sien Ünnerornern redig wegdoon?" +msgstr[1] "Wullt Du disse %1 Ornern un all ehr Ünnerornern redig wegdoon?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Orner wegmaken?" +msgstr[1] "Orner wegdoon?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Orner \"%1\" lett sik nich wegdoon" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Wegdoon vun den Orner fehlslaan" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Egenschappen vun Orner \"%1\"" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Wullt Du den utsöchten Indrag redig wegdoon?" +msgstr[1] "Wullt Du redig %1 Indrääg wegdoon?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Indrag wegmaken?" +msgstr[1] "Indrääg wegmaken?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Indrag \"%1\" lett sik nich wegdoon" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Wegdoon vun den Indrag fehlslaan" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Leesteken ümnömen" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Naam:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nieg Ressource" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Ressource \"%1\" lett sik nich opstellen." + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Opstellen vun de Ressource fehlslaan" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Wullt Du disse Ressource redig wegdoon?" +msgstr[1] "Wullt Du redig %1 Ressourcen wegdoon?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Ressource wegmaken?" +msgstr[1] "Ressourcen wegmaken?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Daten laat sik nich infögen: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Infögen fehlslaan" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "En Ornernaam lett keen Dwarsstreek (\"/\") to." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Fehler bi't Opstellen vun en niegen Orner" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "En Ornernaam lett sik keen \".\" vör- oder achteranstellen." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Ehr Du den Orner \"%1\" synkroniseren kannst, muttst Du de Ressource " +"tokoppeln. Wullt Du ehr nu tokoppeln?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Konto \"%1\" is afkoppelt" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Tokoppeln" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Na dissen Orner verschuven" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Na dissen Orner koperen" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Failed to create tag." +msgid "Failed to update subscription: %1" +msgstr "Slötelwoort lett sik nich opstellen." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Lokale Bestellen" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Lokale Bestellen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Söken:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Bloots bestellte" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Bestellen" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Afbestellen" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Slötelwoort lett sik nich opstellen" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Dat geev en Fehler bi't Opstellen vun en nieg Slötelwoort" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete this resource?" +#| msgid_plural "Do you really want to delete %1 resources?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Wullt Du disse Ressource redig wegdoon?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Slötelwoort wegdoon" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Slötelwoort wegdoon" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "De Slötelwöör fastleggen, de Du bruken wullt" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "Nieg Slötelwoort opstellen" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Manage Tags" +msgid "Manage Tags" +msgstr "Slötelwöör plegen" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Slötelwoort wegdoon" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi-na-XML-Ümwanneln" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Wannelt en Akonadisammeln-Ünnerboom na en XML-Datei üm." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Keen Daten laadt." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Keen Dateinaam angeven" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Datendatei „%1“ lett sik nich opmaken." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Dat gifft de Datei „%1“ nich." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Datendatei „%1“ lett sik nich inlesen." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Schemadefinitschoon lett sik nich laden un inlesen." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Schemainleser-Ümgeven lett sik nich opstellen." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Schema lett sik nich opstellen." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Schemaprööv-Ümgeven lett sik nich opstellen." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Leeg Dateiformaat." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Datendatei lett sik nich inlesen: „%1“" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Sammeln „%1“ lett sik nich finnen" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "Feern ID" + +#~ msgid "MimeType" +#~ msgstr "MIME-Typ" + +#~ msgid "Default Name" +#~ msgstr "Standardnaam" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Wegdoon" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Afbreken" + +#~ msgid "Take left one" +#~ msgstr "Den linken Indrag wohren" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Nich leest" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Tosamen" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Grött" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-Ressource" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Naam" + +#~ msgid "Invalid collection specified" +#~ msgstr "Leeg Sammeln angeven" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Protokollverschoon %1 funnen, tominnst %2 wöör verwacht" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "De Server-Protokollverschoon is nieg noog." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Den Server sien Protokollverschoon is %1, wat liek is oder nieger as de " +#~ "Verschoon %2, de tominnst noot deit." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Nich passen lokaal Sammelnboom opdeckt" + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Feern Sammeln ahn wörtel-afslaten Stammkeed angeven, Ressource is leeg." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE-Testprogramm" diff --git a/po/nl/akonadi_knut_resource.po b/po/nl/akonadi_knut_resource.po new file mode 100644 index 0000000..75b1768 --- /dev/null +++ b/po/nl/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Freek de Kruijf , 2016. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2016-01-19 10:31+0100\n" +"Last-Translator: Freek de Kruijf \n" +"Language-Team: Dutch \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Geen gegevensbestand geselecteerd." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Bestand '%1' succesvol geladen." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Gegevensbestand selecteren" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut-gegevensbestand" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Geen item gevonden voor remote-id %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Moederverzameling niet gevonden in DOM-boomstructuur." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Verzameling kan niet worden geschreven." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Gewijzigde verzameling niet gevonden in DOM-boomstructuur." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Gewiste verzameling niet gevonden in DOM-boomstructuur." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Moederverzameling '%1' niet gevonden in DOM-boomstructuur." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Schrijven van item niet mogelijk." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Gewijzigd item niet gevonden in DOM-boomstructuur." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Gewist item niet gevonden in DOM-boomstructuur." diff --git a/po/nl/libakonadi5.po b/po/nl/libakonadi5.po new file mode 100644 index 0000000..640746e --- /dev/null +++ b/po/nl/libakonadi5.po @@ -0,0 +1,2819 @@ +# translation of libakonadi.po to Dutch +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Bram Schoenmakers , 2007, 2008. +# Rinse de Vries , 2007, 2008, 2010. +# Antoon Tolboom , 2008. +# Freek de Kruijf , 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 10:25+0100\n" +"Last-Translator: Freek de Kruijf \n" +"Language-Team: Dutch \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.12.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" +"Bram Schoenmakers - 2007; 2008,Rinse de Vries - 2007; 2008,Antoon Tolboom - " +"2008,Freek de Kruijf - 2009 t/m 2021" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr ",rinsedevries@kde.nl,,freekdekruijf@kde.nl" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Er is nu geen account geconfigureerd." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integratie van accounts wordt niet ondersteund" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Kan object niet registreren in dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 van type %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agent-ID" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Gereed" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Offline" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Bezig met synchroniseren..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Fout." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Niet ingesteld" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Hulpbronidentificatie" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-hulpbron" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Ongeldig item opgehaald" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Fout bij aanmaken van item: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Fout bij bijwerken van verzameling: %1." + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Bijwerken van lokale verzameling is mislukt: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Bijwerken van lokale items is mislukt: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Kan het item niet ophalen in offline-modus." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Map '%1' wordt gesynchroniseerd" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Het ophalen van de verzameling voor synchronisatie is mislukt." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" +"Het ophalen van de verzameling voor synchronisatie van attributen is mislukt." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Het gevraagde item bestaat niet langer" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Taak geannuleerd." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Deze verzameling bestaat niet." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Een niet opgeloste wees-verzameling gevonden" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Kon het andere item voor de behandeling van een conflict niet vinden" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" +"Niet in staat om toegang te krijgen tot het D-Bus-interface van de " +"aangemaakte agent." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Tijdslimiet bij aanmaken exemplaar van agent overschreden." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Kon agenttype '%1' niet verkrijgen." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Aanmaken exemplaar van agent is mislukt." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Ongeldige verzameling." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Ongeldige hulpbron." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Het verkrijgen van de DBus-interface voor hulpbron '%1' lukt niet" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" +"Synchronisatie van attributen van verzameling heeft een tijdsoverschrijding." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Ongeldige te kopiëren verzameling" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Ongeldige doelverzameling" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Ongeldige ouder" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Verzameling ontlenen aan antwoord is mislukt" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Ongeldige verzameling" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Ongeldige verzameling gegeven." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Geen objecten gespecificeerd voor de verplaatsing" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Geen geldige bestemming gespecificeerd" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Ongeldige verzameling." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Ongeldige ouderverzameling" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Kan niet met de Akonadi-dienst verbinden." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"De protocolversie van de Akonadi-server is incompatibel. Zorg ervoor dat er " +"een compatibele versie geïnstalleerd is." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Door gebruiker geannuleerde actie." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Onbekende fout." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Onverwacht antwoord" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Een relatie aanmaken is mislukt." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tijdslimiet bij hulpbronsynchronisatie overschreden." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Kan de hoofdverzameling van hulpbron %1 niet ophalen." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Geen hulpbron-ID gegeven." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Hulpbronidentificatie '%1' ongeldig" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "De standaard hulpbron via DBus configureren is mislukt." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Het ophalen van de hulpbronverzameling is mislukt." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tijd verlopen bij een poging om een vergrendeling te verkrijgen." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Een tag aanmaken is mislukt." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Naar prullenbak verplaatsen is mislukt, prullenbakhandeling wordt afgebroken" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Ongeldige items doorgegeven" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Ongeldige verzameling doorgegeven" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Ongeldige verzameling of lege lijst met items" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Kon de te herstellen verzameling niet vinden en herstelhulpbron is niet " +"beschikbaar" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Naam" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Bezig met laden..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Fout" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"De doelverzameling '%1' bevat al\n" +"een verzameling met de naam '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Naam" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Kon item niet kopiëren: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kon verzameling niet kopiëren: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Kon item niet verplaatsen: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Kon verzameling niet verplaatsen: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Kon eenheid niet koppelen: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Fout" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Bladwijzers" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Totaal aantal berichten" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Ongelezen berichten" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Grootte van de opslag" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Grootte van de opslag in de submap" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Ongelezen" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Totaal" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Grootte" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Tag" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Ophalen van items voor de index lukt niet" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index is niet langer beschikbaar" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Ladingdeel '%1' is niet beschikbaar voor deze index" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Geen sessie beschikbaar voor deze index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Geen item beschikbaar voor deze index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Naamloze plug-in" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Geen beschrijving beschikbaar" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"De versie van het Akonadi-serverprotocol verschilt van de protocolversion " +"gebruikt door deze toepassing.\n" +"Als u recent uw systeem hebt bijgewerkt meldt u dan af en weer aan om er " +"zeker van te zijn dat alle toepassingen de juiste versie van het protocol " +"gebruiken." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Er zijn geen Akonadi-agents beschikbaar. Controleer uw KDE PIM installatie." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Geen overeenkomende protocolversie. Serverversie is ouder (%1) dan onze " +"(%2). Als u uw systeem recent hebt bijgewerkt start de Akonadi-server dan " +"opnieuw." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Geen overeenkomende protocolversie. Serverversie is nieuwer (%1) dan onze " +"(%2). Als u uw systeem recent hebt bijgewerkt start dan alle KDE-PIM-" +"toepassingen opnieuw." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi zelftest" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Controleert de Akonadi-server en rapporteert status." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nieuw exemplaar van agent..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Agentinstantie verwij&deren" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Agentinstantie aanmaken" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nieuw exemplaar van agent" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Aanmaken exemplaar van agent is mislukt: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Aanmaken exemplaar van agent is mislukt" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Agentinstantie verwijderen?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Wilt u de geselecteerde exemplaar van agent verwijderen?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 configuratie" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 Handboek" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Info over %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "De configuratiedialoog is geopend in een ander venster" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Configuratie voor %1 is al elders geopend." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Registreren van %1 configuratiedialoog is mislukt." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuut" +msgstr[1] "minuten" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Ophalen" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Opties van de bovenliggende map of account gebruiken" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchroniseren bij selectie van deze map" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automatisch synchroniseren na:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nooit" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuten" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokaal gebufferde delen" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opties voor ophalen" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Altijd volledige &berichten ophalen" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Inhoud van berichten op verzoek &ophalen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Inhoud van berichten lokaal houden voor:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Altijd" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Zoeken" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Standaard een map gebruiken" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nieuwe submap..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Een nieuwe submap aanmaken in de huidig geselecteerde map" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nieuwe map" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Naam" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Kon de map niet aanmaken: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Aanmaken van map mislukt" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Algemeen" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Een object" +msgstr[1] "%1 objecten" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Naam:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Aangepaste pictogram gebruiken:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "map" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistieken" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Inhoud:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objecten" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Grootte:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Bedenk dat indexering enige minuten kan duren." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Onderhoud" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Fout bij ophalen van aantal geïndexeerde items" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 item geïndexeerd in deze map" +msgstr[1] "%1 items geïndexeerd in deze map" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Geïndexeerde items berekenen..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Bestanden" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Maptype:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "onbekend" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Items" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Totaal aantal items:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Ongelezen items:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Bezig te indexeren" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Indexeren van volledige tekst inschakelen" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Ophalen van aantal geïndexeerde items ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Map opnieuw indexeren" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Geen map" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Dialoogvenster voor verzameling openen" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Een verzameling selecteren" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Hier naar toe verplaat&sen" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Hier naar toe &kopiëren" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Annuleren" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Tijdstip van wijziging" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Vlaggen" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attribuut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Conflictoplossing" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Mijn versie nemen" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Hun versie nemen" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Beide versies behouden" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Uw wijzigingen zijn intussen in conflict met die gemaakt door iemand " +"anders.
Tenzij een versie gewoon weggegooid kan worden, moet u deze " +"wijzigingen handmatig integreren.
Klik op " +"om een kopie van de teksten te behouden, selecteer daarna welke versie het " +"meest juist is, open het daarna opnieuw en wijzig het opnieuw om toe te " +"voegen wat er ontbreekt." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Gegevens" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi-server starten..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi-server stoppen..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Hierheen verplaat&sen" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "Hierheen &kopiëren" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Hierheen een koppe&ling maken" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Annuleren" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Kan niet verbinden met de service Persoonlijke informatie beheerder.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Service voor beheer van persoonlijke informatie start..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Service voor beheer van persoonlijke informatie stopt..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"De service voor beheer van persoonlijke informatie is bezig met een " +"opwaardering van de database." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"De service voor beheer van persoonlijke informatie is bezig met een " +"opwaardering van de database.\n" +"Dit vindt plaats na het bijwerken van de software en is noodzakelijk om de " +"prestaties te optimaliseren.\n" +"Afhankelijk van de hoeveelheid persoonlijke informatie kan dit enkele " +"minuten duren." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"De Akonadi persoonlijke informatiebeheer-service draait niet. Deze " +"applicatie kan niet worden gebruikt zonder dat." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Starten" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Het Akonadi raamwerk voor beheer van persoonlijke informatie is niet " +"operationeel.\n" +"Klik op \"Details...\" om meer gedetailleerde informatie over dit probleem " +"te verkrijgen." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi persoonlijke informatiebeheer-service draait niet." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Details..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Wilt u het account '%1' verwijderen?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Account verwijderen?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Inkomende accounts (voeg tenminste één account toe):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Toevoegen..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Wijzigen..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "V&erwijderen" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Herstarten" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Recente map" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Favoriet hernoemen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Naam:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi-server zelftest" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Rapport opslaan..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Rapport kopiëren naar klembord" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Het QtSQL-stuurprogramma '%1' is voor de huidige Akonadi-serverconfiguratie " +"vereist en is op uw systeem gevonden." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Het QtSQL-stuurprogramma %1 is voor de huidige Akonadi-serverconfiguratie " +"vereist.\n" +"De volgende stuurprogramma's zijn geïnstalleerd: %2.\n" +"Zorg ervoor dat het vereiste stuurprogramma geïnstalleerd is." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Databasestuurprogramma gevonden." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Databasestuurprogramma niet gevonden." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Uitvoerbare MySQL-server bestand niet getest." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "De huidige configuratie heeft geen interne MySQL-server nodig." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"De huidige geconfigureerde Akonadi gebruikt de MySQL-server '%1'.\n" +"Controleer of de MySQL-server geïnstalleerd is, zet het juiste pad en " +"controleer de noodzakelijke lees- en uitvoeringsrechten van het " +"serverprogramma. Het serverprogramma is normaliter 'mysqld' en de locatie is " +"afhankelijk van de distributie." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-server niet gevonden." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-server niet leesbaar." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-server niet uitvoerbaar." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL met onverwachte naam gevonden." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL-server gevonden." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL-server gevonden: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-server is uitvoerbaar." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Het uitvoeren van MySQL-server '%1' is mislukt met de volgende foutmelding: " +"'%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Het uitvoeren van MySQL-server is mislukt." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Foutlog van MySQL-server is niet getest." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Huidige MySQL-foutlog niet gevonden." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"De MySQL-server heeft tijdens het starten geen fouten gerapporteerd. De log " +"kan gevonden worden in '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-foutlog is niet leesbaar." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Een foutlog-bestand van MySQL-server gevonden maar is niet leesbaar: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-serverlog bevat fouten." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Het foutlog-bestand '%1' van MySQL-server bevat fouten." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-serverlog bevat waarschuwingen." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Het MySQL-serverlogbestand '%1' bevat waarschuwingen." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-serverlog bevat geen fouten." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Het MySQL-serverlogbestand '%1' bevat geen enkele fout of waarschuwing." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-serverconfiguratie niet getest." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Standaard MySQL-serverconfiguratie gevonden." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"De standaard configuratie voor de MySQL-server is gevonden en is leesbaar op " +"%1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Standaard MySQL-serverconfiguratie niet gevonden." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"De standaard configuratie voor de MySQL-server is niet gevonden of is niet " +"leesbaar. Controleer of de Akonadi-installatie kompleet is en de " +"toegangrechten voldoende zijn." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Aangepaste MySQL-serverconfiguratie niet beschikbaar." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"De aangepaste configuratie voor de MsSQL-server is niet gevonden maar is " +"optioneel." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Aangepaste MySQL-serverconfiguratie gevonden." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"De aangepaste configuratie voor de MySQL-server is gevonden en is leesbaar " +"op %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Aangepaste MySQL-serverconfiguratie niet leesbaar." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"De aangepaste configuratie voor de MySQL-server is op %1 gevonden maar is " +"niet leesbaar. Controleer de toegangsrechten." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL-serverconfiguratie niet gevonden of niet leesbaar." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "De MySQL-serverconfiguratie is niet gevonden of is niet leesbaar." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL-serverconfiguratie is bruikbaar." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "De MySQL-serverconfiguratie is op %1 gevonden en is leesbaar." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Kan niet met de PostgresSQL-server verbinden." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgresSQL-server gevonden." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "De PostgresSQL-server is gevonden en werkt." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl niet gevonden" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Het programma 'akonadictl' dient toegankelijk te zijn in $PATH. Zorg ervoor " +"dat de Akonadi-server geïnstalleerd is." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl gevonden en bruikbaar" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Het programma '%1' om de Akonadi-server te beheren is gevonden en kon " +"succesvol uitgevoerd worden.\n" +"Resultaat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl gevonden maar niet bruikbaar" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Het programma '%1' om de Akonadi-server te beheren is gevonden maar kon niet " +"succesvol uitgevoerd worden.\n" +"Resultaat:\n" +"%2\n" +"Zorg ervoor dat de Akonadi-server correct geïnstalleerd is." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi-beheerproces geregistreerd op D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Het Akonadi-beheerproces is geregistreerd op D-Bus wat meestal aangeeft dat " +"het operationeel is." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi-beheerproces niet geregistreerd op D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Het Akonadi-beheerproces is niet geregistreerd op D-Bus wat meestal betekent " +"dat het niet gestart is of dat er een fatale fout opgetreden is tijdens het " +"opstarten." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi-serverproces geregistreerd op D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-serverproces is geregistreerd op D-Bus wat meestal betekent dat het " +"operationeel is." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi-serverproces niet geregistreerd op D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-serverproces is niet geregistreerd op D-Bus wat meestal betekent dat " +"het niet gestart is of dat er een fatale fout opgetreden is tijdens het " +"opstarten." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protocol versietest niet mogelijk." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Zonder een verbinding met de server is het niet mogelijk om te testen of de " +"protocolversie aan de eisen voldoet." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Server protocolversie is te oud." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"De server protocolversie is %1, maar versie %2 is vereist door de client. " +"Als u recent KDE-PIM hebt bijgewerkt, ga dan na dat zowel de Akonadi-server " +"als de KDE-PIM-applicaties opnieuw zijn opgestart." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Server protocolversie is te new." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Server protocolversie komt overeen." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "De huidige protocolversie is %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Hulpbronagenten gevonden." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Tenminste een hulpbronagent is gevonden." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Geen hulpbronagent gevonden." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Geen hulpbronagenten zijn gevonden, Akonadi is niet bruikbaar zonder " +"minstens één hulpbronagent. Dit houdt meestal in dat geen hulpbronagenten " +"geïnstalleerd zijn of dat er een probleem met de instellingen is. Er is in " +"de volgende paden gezocht: '%1'. De XDG_DATA_DIRS-omgevingsvariabele is " +"ingesteld op '%2', zorg ervoor dat deze alle paden bevat waar Akonadi-" +"agenten geïnstalleerd zijn." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Geen huidige foutlog van Akonadi-server gevonden." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"De Akonadi-server heeft geen fouten tijdens het huidige opstarten " +"gerapporteerd." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Huidige foutlog van Akonadi-server gevonden." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"De Akonadi-server heeft fouten tijdens het huidige opstarten gerapporteerd. " +"De log kan worden gevonden in %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Geen vorige foutlog van Akonadi-server gevonden." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"De Akonadi-server heeft geen fouten tijdens het vorige opstarten " +"gerapporteerd." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Vorige foutlog van Akonadi-server gevonden." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"De Akonadi-server heeft tijdens het vorige opstarten fouten gerapporteerd. " +"De log kan worden gevonden in %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Geen huidige foutlog van Akonadi-beheer gevonden." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Het Akonadi-beheerproces heeft geen fouten tijdens het huidige opstarten " +"gerapporteerd." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Huidige foutlog van Akonadi-beheer gevonden." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Het Akonadi-besturingsproces heeft fouten tijdens het huidige opstarten " +"gerapporteerd. De log kan worden gevonden in %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Geen vorige foutlog van Akonadi-beheer gevonden." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Het Akonadi-beheerproces heeft geen fouten tijdens het vorige opstarten " +"gerapporteerd." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Vorige foutlog van Akonadi-beheer gevonden." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Het Akonadi-besuringsproces heeft tijdens het vorige opstarten fouten " +"gerapporteerd. De log kan worden gevonden in %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi is gestart als root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Applicaties met een directe verbinding met het internet draaien als root/" +"systeembeheerder stelt u bloot aan vele beveiligingsrisico's. MySQL, dat " +"wordt gebruikt door deze Akonadi-installatie staat zichzelf niet toe om als " +"root te draaien om u beschermen tegen deze risico's." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi draait niet als root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi draait niet als root/systeembeheerder, wat de aanbevolen manier is " +"voor de opzet van een veilig systeem." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Testrapport opslaan" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Fout" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Kon het bestand '%1' niet openen" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Tijdens het starten van de Akonadi-server is er een fout opgetreden. De " +"volgende zelftesten zijn behulpzaam bij het opsporen en verhelpen van dit " +"probleem. Voeg dit rapport altijd toe bij het vragen van ondersteuning of " +"het indienen van een bug." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Details" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Voor meer probleemoplossingtips kijk naar userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nieuwe map..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nieuw" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Map verwij&deren" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Verwijderen" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "Map &synchroniseren" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchroniseren" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Ma&peigenschappen" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Eigenschappen" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "P&lakken" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Plakken" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Lokale in&schrijvingen beheren..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Lokale inschrijvingen beheren" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Aan bladwijzers toevoegen" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Aan favorieten toevoegen" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Uit bladwijzers verwijderen" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Uit favorieten verwijderen" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Bladwijzer hernoemen..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Hernoemen" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Map kopiëren naar..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopiëren naar" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Item kopiëren naar..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Item verplaatsen naar..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Verplaatsen naar" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Map verplaatsen naar..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Item &knippen" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Knippen" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Map &knippen" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Hulpbron aanmaken" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Hulpbron verwijderen" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Hulpbroneigenschappen" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Hulpbron synchroniseren" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Offline werken" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Map recursief &synchroniseren" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Recursief synchroniseren" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Map naar prullenbak verplaatsen" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Map naar prullenbak verplaatsen" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Ite&m naar prullenbak verplaatsen" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Item naar prullenbak verplaatsen" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Map uit prullenbak he&rstellen" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Map uit prullenbak herstellen" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Item uit prullenbak he&rstellen" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Item uit prullenbak herstellen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Verzameling uit prullenbak he&rstellen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Verzameling uit prullenbak herstellen" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Favoriete mappen &synchroniseren" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Favoriete mappen synchroniseren" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Alles in deze map synchroniseren" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Map &kopiëren" +msgstr[1] "%1 mappen &kopiëren" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Item &kopiëren" +msgstr[1] "%1 items &kopiëren" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Item &knippen" +msgstr[1] "%1 items &knippen" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Map &knippen" +msgstr[1] "%1 mappen &knippen" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Item verwij&deren" +msgstr[1] "%1 items verwij&deren" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Map verwij&deren" +msgstr[1] "%1 mappen verwijderen" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Map &synchroniseren" +msgstr[1] "%1 mappen &synchroniseren" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Hulpbron verwij&deren" +msgstr[1] "%1 hulpbronnen verwij&deren" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Hulpbron &synchroniseren" +msgstr[1] "%1 hulpbronnen &synchroniseren" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Map kopiëren" +msgstr[1] "%1 mappen &kopiëren" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Item kopiëren" +msgstr[1] "%1 items kopiëren" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Item knippen" +msgstr[1] "%1 items &knippen" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Map knippen" +msgstr[1] "%1 mappen knippen" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Item verwijderen" +msgstr[1] "%1 items verwij&deren" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Map verwijderen" +msgstr[1] "%1 mappen verwijderen" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Map synchroniseren" +msgstr[1] "%1 mappen synchroniseren" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Hulpbron verwijderen" +msgstr[1] "%1 hulpbronnen verwijderen" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Hulpbron synchroniseren" +msgstr[1] "%1 hulpbronnen synchroniseren" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Naam" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Wilt u deze map en al haar submappen verwijderen?" +msgstr[1] "Wilt u deze %1 mappen en al hun submappen verwijderen?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Map verwijderen?" +msgstr[1] "Map verwijderen?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Kon de map niet verwijderen: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Verwijderen van de map is mislukt" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Eigenschappen voor map %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Wilt u het geselecteerde item verwijderen?" +msgstr[1] "Wilt u de %1 geselecteerde items verwijderen?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Item verwijderen?" +msgstr[1] "Items verwijderen?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Kon item niet verwijderen: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Verwijderen van item is mislukt" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Favoriet hernoemen" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Naam:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nieuwe hulpbron" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Kon de hulpbron niet aanmaken: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Aanmaken van hulpbron is mislukt" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Wilt u deze hulpbron verwijderen?" +msgstr[1] "Wilt u deze %1 hulpbronnen verwijderen?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Hulpbron verwijderen?" +msgstr[1] "Hulpbronnen verwijderen?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Kon gegevens niet plakken: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Plakken is mislukt" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Er kan geen \"/\" aan de mapnaam worden toegevoegd." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Fout bij aanmaken nieuwe map" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Er kan geen \".\" aan het begin of eind van de mapnaam worden toegevoegd." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Alvorens map \"%1\" te kunnen synchroniseren is het nodig om de hulpbron " +"online te hebben. Wilt u het online brengen?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Account \"%1\" is offline" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Ga online" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Naar deze map verplaatsen" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Naar deze map kopiëren" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Bijwerken van inschrijving is mislukt: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Fout bij inschrijven" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Lokale inschrijvingen" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Zoeken:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Alleen &ingeschreven" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Inschrijven" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Uitschrijven" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Een nieuwe tag aanmaken is mislukt" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Fout bij aanmaken van een nieuwe tag" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Wilt u de tag %1 verwijderen?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Tag verwijderen" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Tag verwijderen" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Tags selecteren om toegepast te worden." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Nieuwe tag aanmaken" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Tags beheren" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Tags selecteren..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Tags selecteren" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Wissen" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Klik om tags toe te voegen" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi naar XML converter" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" +"Converteert een subboomstructuur van een verzameling in Akonadi in een XML-" +"bestand." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Geen gegevens geladen." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Geen bestandsnaam opgegeven" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Gegevensbestand '%1' kon niet worden geopend." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Bestand %1 bestaat niet." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Gegevensbestand '%1' kon niet worden ontleden." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Definitie van schema kon niet geladen en ontleed worden." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Niet in staat context van schema-ontleder aan te maken." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Niet in staat schema aan te maken." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Niet in staat context van schema-validatie aan te maken." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Ongeldig bestandsformaat." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Gegevensbestand kon niet worden ontleden: '%1'" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Verzameling %1 kan niet gevonden worden" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Externe Id" + +#~ msgid "MimeType" +#~ msgstr "Mimetype" + +#~ msgid "Form" +#~ msgstr "Formulier" + +#~ msgid "Default Name" +#~ msgstr "Standaard naam" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Verwijderen" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Annuleren" + +#~ msgctxt "@action:button" +#~ msgid "Open text editor" +#~ msgstr "Tekstbewerker openen" + +#~ msgid "Take left one" +#~ msgstr "Linker nemen" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Twee exemplaren voor bijwerken zijn met elkaar in conflict.Kies " +#~ "welke bijwerking toegepast moet worden." + +#~ msgid "uknown" +#~ msgstr "onbekend" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Ongelezen" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Totaal" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Grootte" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-hulpbron" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Naam" + +#~ msgid "Invalid collection specified" +#~ msgstr "Ongeldige verzameling gespecificeerd" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Protocolversie %1 gevonden, tenminste %2 verwacht" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Server protocolversie is voldoende recent." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "De server protocolversie is %1, wat gelijk of nieuwer is dan de vereiste " +#~ "versie %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Inconsistente lokale boomstructuur van verzameling gedetecteerd." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Verzameling op afstand zonder voorganger die eindigt in de " +#~ "hoofdmapketting aangeleverd, hulpbron is gebroken." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE Testprogramma" + +#~ msgid "Cannot list root collection." +#~ msgstr "Kan geen lijst maken van de hoofdverzameling." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk-zoekdienst geregistreerd op DBus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "De Nepomuk-zoekdienst is geregistreerd op DBus wat meestal betekent dat " +#~ "het operationeel is." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk-zoekdienst niet geregistreerd op DBus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "De Nepomuk-zoekdienst is niet geregistreerd op DBus wat meestal betekent " +#~ "dat het niet gestart is of dat er een fatale fout is opgetreden tijdens " +#~ "het opstarten." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuk-zoekdienst gebruikt een niet geschikte backend." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "De Nepomuk-zoekdienst gebruikt de '%1'-backend, die niet wordt aanbevolen " +#~ "om met Akonadi te gebruiken." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuk-zoekdienst gebruikt een geschikte backend." + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "De Nepomuk-zoekdienst gebruikt een van de aanbevolen backends." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Plug-in \"%1\" is geen statisch gebouwde built-in, gaarne deze informatie " +#~ "in het bugrapport specificeren." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Plug-in is niet statisch gebouwd" + +#~ msgid "Fetch Job Error" +#~ msgstr "Fout in job ophalen" + +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "Nieuwe map..." + +#~| msgid "&Resource Properties" +#~ msgid "Resource Properties" +#~ msgstr "Hulpbroneigenschappen" + +#~ msgid "Cache" +#~ msgstr "Buffer" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Bufferbeleid van parent overnemen" + +#~ msgid "Cache Policy" +#~ msgstr "Bufferbeleid" + +#~ msgid "Interval check time:" +#~ msgstr "Interval controletijd:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Verlooptijd lokaal buffer:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Op aanvraag synchroniseren" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Beheer welke mappen u wilt zien in de mappenlijst" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Zoeken" + +#~ msgid "Available Folders" +#~ msgstr "Beschikbare mappen" + +#~ msgid "Current Changes" +#~ msgstr "Huidige veranderingen" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Uitschrijven van geselecteerde map" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "" +#~ "De Akonadi-server heeft tijdens het opstarten fouten gerapporteerd in %1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "Het Akonadi-beheerproces heeft tijdens het opstarten fouten gerapporteerd " +#~ "in %1." + +#~ msgid "TODO" +#~ msgstr "TAAK" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi niet operationeel.
Details...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-hulpbron" diff --git a/po/nn/akonadi_knut_resource.po b/po/nn/akonadi_knut_resource.po new file mode 100644 index 0000000..2540657 --- /dev/null +++ b/po/nn/akonadi_knut_resource.po @@ -0,0 +1,87 @@ +# Translation of akonadi_knut_resource to Norwegian Nynorsk +# +# Eirik U. Birkeland , 2009. +# Karl Ove Hufthammer , 2009, 2010, 2019. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2019-08-04 09:55+0200\n" +"Last-Translator: Karl Ove Hufthammer \n" +"Language-Team: Norwegian Nynorsk \n" +"Language: nn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 19.04.3\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Inga datafil er vald." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Fila «%1» vart lasta inn." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Vel datafil" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut-datafil" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Fann inkje element for fjern-ID %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Fann ikkje foreldersamlinga i DOM-treet." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Klarte ikkje skriva samlinga." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Fann ikkje endra samling i DOM-treet." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Fann ikkje fjerna samling i DOM-treet." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Fann ikkje foreldersamlinga «%1» i DOM-treet." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Klarte ikkje skriva elementet." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Fann ikkje endra element i DOM-treet." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Fann ikkje fjerna element i DOM-treet." diff --git a/po/nn/libakonadi5.po b/po/nn/libakonadi5.po new file mode 100644 index 0000000..d8790d2 --- /dev/null +++ b/po/nn/libakonadi5.po @@ -0,0 +1,2516 @@ +# Translation of libakonadi5 to Norwegian Nynorsk +# +# Karl Ove Hufthammer , 2007, 2008, 2010, 2016, 2018, 2021. +# Eirik U. Birkeland , 2008, 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-02-13 12:56+0100\n" +"Last-Translator: Karl Ove Hufthammer \n" +"Language-Team: Norwegian Nynorsk \n" +"Language: nn\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.12.2\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Karl Ove Hufthammer,Eirik U. Birkeland" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "karl@huftis.org,eirbir@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agent-ID" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Klar" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Fråkopla" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synkroniserer …" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Feil." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-ressurs" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Klarte ikkje oppdatera den lokale samlinga: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Klarar ikkje henta element i fråkopla modus." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Det finst inga slik samling." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Fann uordna foreldrelause samlingar" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Tidsavbrot i opprettinga av agentinstans." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Klarte ikkje henta inn agenttypen «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Klarte ikkje laga agentinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Ugyldig ressursinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Fekk ikkje D-Bus-grensesnitt til ressursen «%1»." + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Ugyldig forelder" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Ugyldig samling" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Ugyldig samling oppgjeven." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Ingen objekt som skal flyttast er oppgjevne" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Ingen gyldige mål er oppgjevne" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Får ikkje tilgang til Akonadi-tenesta" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Protokollversjonen av Akonadi-tenaren er inkompatibel. Sjå til at ein " +"kompatibel versjon er installert." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Brukaren avbraut handlinga." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Ukjend feil." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tidsavbrot ved ressurssynkronisering." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Klarte ikkje henta rotsamlinga til ressursen %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Ingen ressurs-ID oppgjeven." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Ugyldig ressurs-ID «%1»" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Klarte ikkje setja opp standardressursen via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Klarte ikkje henta ressurssamlinga." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tidsavbrot ved forsøk på eksklusiv tilgang." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Namn" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Lastar …" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Feil" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Namn" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Feil" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Favorittmapper" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Meldingar i alt" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Ulesne meldingar" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvote" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Lagringsstorleik" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Storleik på undermapper" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Ulesne" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "I alt" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Storleik" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Merkelapp" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Klarte ikkje henta element til indeks" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indeksen er ikkje lenger tilgjengeleg" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Nyttlastdelen «%1» er ikkje tilgjengeleg for indeksen" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Det finst ikkje noko tilgjengeleg økt for indeksen" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Det finst ingen element tilgjengelege for indeksen" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Programtillegg utan namn" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Inga skildring tilgjengeleg" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Sjølvtest for Akonadi" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Får ikkje tilgang til Akonadi-tenesta" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1-oppsett" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1-handbok" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Om %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minutt" +msgstr[1] "minutt" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Aldri" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutt" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokalt mellomlagra delar" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Søk" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Bruk mappe som standard" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Ny undermappe …" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Lag ei ny undermappe i den valde mappa" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Ny mappe" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Namn" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Klarte ikkje oppretta mappe: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Klarte ikkje oppretta mappe" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Generelt" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Eitt objekt" +msgstr[1] "%1 objekt" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Namn:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Bruk sjølvvalt &ikon" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "mappe" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistikk" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Innhald:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objekt" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Storleik:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Vedlikehald" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Filer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Mappetype:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "ukjend" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Element" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indeksering" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Inga mappe" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Opna dialogvindauge for samling" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Flytt hit" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopier hit" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Avbryt" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Endringstidspunkt" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flagg" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Attributt: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Løysing av konflikt" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Bruk min versjon" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Bruk deira versjon" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Bruk begge versjonane" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Data" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Startar Akonadi-tenaren …" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stengjer Akonadi-tenaren …" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Flytt hit" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopier hit" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Lag lenkje hit" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Avbryt" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi-tenesta for handsaming av personleg informasjon køyrer ikkje. " +"Programmet kan derfor ikkje brukast." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Start" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi-tenesta for handsaming av personleg informasjon verkar ikkje.\n" +"Trykk på «Detaljar» for å få detaljert informasjon om kva som er gale." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi-tenesta for handtering av personleg informasjon verkar ikkje." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detaljar …" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Fjerna kontoen?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Innkommande kontoar (legg til minst éin):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Legg til …" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Endra …" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Fjern" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Start på nytt" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Endra namn på favoritt" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Namn:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Sjølvtest for Akonadi-tenaren" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Lagra rapporten …" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopier rapporten til utklippstavla" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Akonadi-oppsettet treng QtSQL-drivaren «%1», som alt finst tilgjengeleg på " +"systemet." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Akonadi-oppsettet treng QtSQL-drivaren «%1».\n" +"Desse drivarane er installerte: %2.\n" +"Sjå til at den nødvendige drivaren er installert." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Databasedrivar er funnen." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Databasedrivar er ikkje funnen." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Det er ikkje testa om MySQL-tenaren er køyrbar." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Det gjeldande oppsettet krev ingen intern MySQL-tenar." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-tenaren er ikkje funnen." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-tenaren er ikkje lesbar." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-tenaren er ikkje køyrbar." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL-tenar er funnen med eit uventa namn." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL-tenar er funnen." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL-tenar er funnen: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-tenaren er køyrbar." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "Klarte ikkje køyra MySQL-tenaren «%1». Denne grunnen vart gjeven: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Klarte ikkje køyra MySQL-tenaren." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Loggfila til MySQL-tenaren er ikkje testa." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Gjeldande MySQL-feillogg er ikkje funnen." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-feilloggen er ikkje lesbar." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Ei loggfil til MySQL-tenaren er funnen, men ho er ikkje lesbar: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-tenarloggen inneheld feil." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Loggfila til MySQL-tenaren, «%1», inneheld feil." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-tenarloggen inneheld åtvaringar." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Loggfila til MySQL-tenaren, «%1», inneheld åtvaringar." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-tenarloggen inneheld ingen feil." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Loggfila til MySQL-tenaren, «%1», inneheld ingen feil eller åtvaringar." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Oppsettet for MySQL-tenaren er ikkje testa." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Standardoppsett for MySQL-tenaren er funne." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Standardoppsett for MySQL-tenaren vart funne i %1 og er lesbart." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Standardoppsett for MySQL-tenaren er ikkje funne." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Standardoppsett for MySQL-tenaren vart ikkje funne eller er ikkje lesbart. " +"Sjå til at Akonadi er ferdig installert og at du har nok tilgang." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Tilpassa oppsett for MySQL-tenaren er ikkje tilgjengeleg." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Tilpassa oppsett for MySQL-tenaren er ikkje funne, men det er valfritt." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Tilpassa oppsett for MySQL-tenaren er funne." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Tilpassa oppsett for MySQL-tenaren vart funne i %1 og er lesbart." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Tilpassa oppsett for MySQL-tenaren er ikkje lesbart." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Tilpassa oppsett for MySQL-tenaren vart funne i %1, men er ikkje lesbart. " +"Sjå til at du har nok tilgang." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Oppsettet for MySQL-tenaren er ikkje funne eller ikkje lesbart." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Oppsett for MySQL-tenaren vart ikkje funne eller er ikkje lesbart." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Oppsettet for MySQL-tenaren er brukbart." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Oppsett for MySQL-tenaren vart funne i %1 og er lesbart." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Klarte ikkje kopla til PostgreSQL-tenaren." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Fann ikkje PostgreSQL-tenar." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Fann PostgreSQL-tenaren, og sambandet verkar." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "fann ikkje akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Programmet «akonadictl» må vera tilgjengeleg i $PATH. Sjå til at Akonadi-" +"tenaren er installert." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl er funne og brukbart" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Programmet «%1» som skal kontrollera Akonadi-tenaren er funne og køyrer " +"skikkeleg.\n" +"Resultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl er funne, men ikkje brukbart" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Programmet «%1» som skulle kontrollera Akonadi-tenaren er funne, men køyrer " +"ikkje skikkeleg.\n" +"Resultat:\n" +"%2\n" +"Sjå til at Akonadi-tenaren er installert rett." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Kontrollprosessen for Akonadi er registrert i D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Kontrollprosessen for Akonadi er registrert i D-Bus. Dette tyder vanlegvis " +"at han verkar." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Kontrollprosessen for Akonadi er ikkje registrert i D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Kontrollprosessen for Akonadi er ikkje registrert i D-Bus. Dette tyder " +"vanlegvis at han ikkje er starta eller at det vart oppdaga ein alvorleg feil " +"under oppstarten." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi-tenarprosessen er registrert i D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-tenarprosessen er registrert i D-Bus. Dette tyder vanlegvis at han " +"verkar." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi-tenarprosessen er ikkje registrert i D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-tenarprosessen er ikkje registrert i D-Bus. Dette tyder vanlegvis at " +"han ikkje er starta eller at det vart oppdaga ein alvorleg feil under " +"oppstarten." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Ikkje mogleg å sjekka protokollversjon." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Utan tilkopling til tenaren er det umogleg å sjekka om protokollversjonen er " +"ny nok." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Tenarprotokollversjonen er for gammal." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Fann ressursagentar." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Fann minst éin ressursagent." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Fann ingen ressursagentar" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Fann ikkje gjeldande feillogg frå Akonadi-tenaren." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi-tenaren rapporterte ikkje om feil under gjeldande oppstart." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Fann gjeldande feillogg frå Akonadi-tenaren." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Fann ikkje tidlegare feillogg frå Akonadi-tenaren." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi-tenaren rapporterte ikkje om feil då han sist vart starta." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Fann tidlegare feillogg frå Akonadi-tenaren." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Fann ikkje gjeldande feillogg frå kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Kontrollprosessen for Akonadi rapporterte ikkje om feil under gjeldande " +"oppstart." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Fann gjeldande feillogg frå kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Fann ikkje tidlegare feillogg frå kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Kontrollprosessen for Akonadi rapporterte ikkje om feil då han sist vart " +"starta." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Fann tidlegare feillogg frå kontroll av Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi vart starta av rotbrukar" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi køyrer ikkje som rotbrukar" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Lagra testrapport" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Feil" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Klarte ikkje opna fila «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Det oppstod ein feil då Akonadi-tenaren skulle starta. Sjølvtestane nedanfor " +"kan vera til hjelp når du skal finna og løysa problemet. Når du spør om " +"hjelp eller melder frå om feil, bør du alltid ta med denne rapporten." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaljar" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Ny mappe …" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +msgid "&Delete Folder" +msgstr "&Slett mappe" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Slett" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +msgid "&Synchronize Folder" +msgstr "&Synkroniser mappe" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synkroniser" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Mappe&eigenskapar" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Eigenskapar" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Lim inn" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Lim inn" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Handsam lokale &abonnement …" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Handsam lokale abonnement" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Legg til som favorittmappe" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Fjern frå favorittmappene" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Fjern frå favorittar" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Endra namn på favoritt …" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Endra namn" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopier mappa til …" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopier til" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopier elementet til …" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Flytt elementet til …" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Flytt til" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Flytt mappa til …" + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +msgid "&Cut Item" +msgstr "&Klipp ut elementet" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Klipp ut" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +msgid "&Cut Folder" +msgstr "&Klipp ut mappa" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +msgid "Delete Resource" +msgstr "Slett ressurs" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Ressurseigenskapar" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +msgid "Synchronize Resource" +msgstr "Synkroniser ressurs" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Arbeid fråkopla" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synkroniser mappe rekursivt" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synkroniser rekursivt" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Flytt mappe til papirkorga" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Flytt mappe til papirkorga" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Flytt element til papirkorga" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopier mappa" +msgstr[1] "&Kopier dei %1 mappene" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopier elementet" +msgstr[1] "&Kopier dei %1 elementa" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Slett elementet" +msgstr[1] "&Slett dei %1 elementa" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Klarte ikkje sletta mappe: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Klarte ikkje sletta mappe" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Klarte ikkje laga ressurs: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Klarte ikkje lima inn data: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Klarte ikkje lima inn" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Flytt til denne mappa" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopier til denne mappa" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Det er ikkje oppgjeve noko filnamn" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +msgid "Unable to open data file '%1'." +msgstr "Klarte ikkje henta inn agenttypen «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Fila %1 finst ikkje." + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Klarte ikkje henta inn agenttypen «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +msgid "Unable to create schema parser context." +msgstr "Klarte ikkje laga agentinstans." + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +msgid "Unable to create schema." +msgstr "Klarte ikkje laga agentinstans." + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +msgid "Unable to create schema validation context." +msgstr "Klarte ikkje laga agentinstans." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +msgid "Unable to parse data file: %1" +msgstr "Klarte ikkje lima inn data: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +msgid "Unable to find collection %1" +msgstr "Ugyldig samling" diff --git a/po/pa/akonadi_knut_resource.po b/po/pa/akonadi_knut_resource.po new file mode 100644 index 0000000..3fb706f --- /dev/null +++ b/po/pa/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# A S Alam , 2010. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-01-16 08:42+0530\n" +"Last-Translator: A S Alam \n" +"Language-Team: ਪੰਜਾਬੀ \n" +"Language: pa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "ਕੋਈ ਡਾਟਾ ਫਾਇਲ ਨਹੀਂ ਚੁਣੀ।" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "ਫਾਇਲ '%1' ਠੀਕ ਤਰ੍ਹਾਂ ਲੋਡ ਕੀਤੀ ਗਈ।" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "ਡਾਟਾ ਫਾਇਲ ਚੁਣੋ" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "" diff --git a/po/pa/libakonadi5.po b/po/pa/libakonadi5.po new file mode 100644 index 0000000..101685c --- /dev/null +++ b/po/pa/libakonadi5.po @@ -0,0 +1,2744 @@ +# translation of libakonadi.po to Punjabi +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Amanpreet Singh Alam , 2008. +# A S Alam , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2010-01-16 08:17+0530\n" +"Last-Translator: A S Alam \n" +"Language-Team: ਪੰਜਾਬੀ \n" +"Language: pa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 1.0\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "ਅਮਨਪਰੀਤ ਸਿੰਘ ਆਲਮ" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "aalam@users.sf.net" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "ਏਜੰਟ ਪਛਾਣ" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "ਅਕੋਂਡੀ ਏਜੰਟ" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "ਤਿਆਰ" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "ਆਫਲਾਈਨ" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "ਸੈਕਰੋ..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "ਗਲਤੀ।" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label, commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "ਸਰੋਤ ਪਛਾਣਕਰਤਾ" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title, application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "ਅਕੌਂਡੀ ਸਰੋਤ" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "ਕੋਈ ਭੰਡਾਰ ਨਹੀਂ" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, fuzzy, kde-format +#| msgid "Invalid collection given." +msgid "Invalid collection instance." +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ ਦਿੱਤਾ ਹੈ।" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Failed to parse Collection from response" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ ਦਿੱਤਾ ਹੈ।" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "ਕੋਈ ਢੁੱਕਵਾਂ ਟਿਕਾਣਾ ਨਹੀਂ ਦਿੱਤਾ" + +#: core/jobs/invalidatecachejob.cpp:58 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection." +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "ਅਕੌਂਡੀ ਸਰਵਿਸ ਨਾਲ ਕੁਨੈਕਟ ਨਹੀਂ ਹੈ।" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "ਯੂਜ਼ਰ ਨੇ ਓਪਰੇਸ਼ਨ ਰੱਦ ਕੀਤਾ।" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "ਅਣਜਾਣ ਗਲਤੀ।" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection passed" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, fuzzy, kde-format +#| msgid "Invalid collection given." +msgid "No valid collection or empty itemlist" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ ਦਿੱਤਾ ਹੈ।" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "ਨਾਂ" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Syncing..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "ਸੈਕਰੋ..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "ਗਲਤੀ।" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "ਨਾਂ" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "ਡਾਟਾ ਪਾਰਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "ਡਾਟਾ ਪਾਰਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "ਫਾਇਲ '%1' ਖੋਲ੍ਹੀ ਨਹੀਂ ਜਾ ਸਕੀ" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "no collection" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "ਕੋਈ ਭੰਡਾਰ ਨਹੀਂ" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "ਗਲਤੀ।" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "ਪਸੰਦੀਦਾ ਫੋਲਡਰ" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "ਕੁੱਲ ਸੁਨੇਹੇ" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "ਨਾ-ਪੜ੍ਹੇ ਸੁਨੇਹੇ" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "ਕੋਟਾ" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "ਸਟੋਰੇਜ਼ ਸਾਈਜ਼" + +#: core/models/statisticsproxymodel.cpp:111 +#, fuzzy, kde-format +#| msgid "Storage Size" +msgid "Subfolder Storage Size" +msgstr "ਸਟੋਰੇਜ਼ ਸਾਈਜ਼" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "ਨਾ-ਪੜ੍ਹੇ" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "ਕੁੱਲ" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "ਸਾਈਜ਼" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "ਇਸ ਇੰਡੈਕਸ ਲਈ ਕੋਈ ਸ਼ੈਸ਼ਨ ਉਪਲੱਬਧ ਨਹੀਂ" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "ਬਿਨ-ਨਾਂ ਪਲੱਗਇਨ" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "ਕੋਈ ਵੇਰਵਾ ਉਪਲੱਬਧ ਨਹੀਂ" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "ਅਕੌਂਡੀ ਸਰਵਰ ਸੈਲਫ਼-ਟੈਸਟ" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "ਅਕੌਂਡੀ ਸਰਵਿਸ ਨਾਲ ਕੁਨੈਕਟ ਨਹੀਂ ਹੈ।" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgid "&Delete Agent Instance" +msgstr "ਆਈਟਮ ਹਟਾਓ(&D)" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Could not create agent instance: %1" +msgstr "ਡਾਟਾ ਪਾਰਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ: %1" + +#: widgets/agentactionmanager.cpp:56 +#, fuzzy, kde-format +#| msgid "Folder creation failed" +msgid "Agent instance creation failed" +msgstr "ਫੋਲਡਰ ਬਣਾਉਣਾ ਫੇਲ੍ਹ" + +#: widgets/agentactionmanager.cpp:58 +#, fuzzy, kde-format +#| msgid "Delete folder?" +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "ਫੋਲਡਰ ਹਟਾਉਣਾ ਹੈ?" + +#: widgets/agentactionmanager.cpp:62 +#, fuzzy, kde-format +#| msgid "Do you really want to delete all selected items?" +msgid "Do you really want to delete the selected agent instance?" +msgstr "ਕੀ ਤੁਸੀਂ ਸਭ ਚੁਣੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "ਮਿੰਟ" +msgstr[1] "ਮਿੰਟ" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, fuzzy, kde-format +#| msgid "Subscribe to selected folder" +msgid "Synchronize when selecting this folder" +msgstr "ਚੁਣੇ ਫੋਲਡਰ ਲਈ ਮੈਂਬਰ ਬਣੋ" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "ਕਦੇ ਨਹੀਂ" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "ਮਿੰਟ" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "ਲੋਕਲ ਕੈਸ਼ ਕੀਤੇ ਭਾਗ" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, fuzzy, kde-format +#| msgctxt "no cache timeout" +#| msgid "Never" +msgctxt "no cache timeout" +msgid "Forever" +msgstr "ਕਦੇ ਨਹੀਂ" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "search folder" +#| msgid "Search:" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "ਖੋਜ:" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, fuzzy, kde-format +#| msgid "&New Folder..." +msgid "&New Subfolder..." +msgstr "ਨਵਾਂ ਫੋਲਡਰ(&N)..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "ਨਵਾਂ ਫੋਲਡਰ" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "ਨਾਂ" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "ਫੋਲਡਰ ਬਣ ਨਹੀਂ ਸਕਿਆ: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "ਫੋਲਡਰ ਬਣਾਉਣਾ ਫੇਲ੍ਹ" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "ਆਮ" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "ਇੱਕ ਆਬਜੈਕਟ" +msgstr[1] "%1 ਆਬਜੈਕਟ" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "ਨਾਂ(&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "ਕਸਟਮ ਆਈਕਾਨ ਵਰਤੋਂ(&U):" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "ਫੋਲਡਰ" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "ਅੰਕੜੇ" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "ਸਮੱਗਰੀ:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 ਆਬਜੈਕਟ" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "ਸਾਈਜ਼:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 ਬਾਈਟ" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "Items" +msgstr "ਆਈਟਮ ਕੱਟੋ(&C)" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "ਕੁੱਲ ਸੁਨੇਹੇ" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "ਨਾ-ਪੜ੍ਹੇ ਸੁਨੇਹੇ" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Reindex folder" +msgstr "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/collectionrequester.cpp:113 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "New Folder" +msgid "No Folder" +msgstr "ਨਵਾਂ ਫੋਲਡਰ" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "ਇੱਥੇ ਭੇਜੋ(&M)" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "ਇੱਥੇ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "ਰੱਦ ਕਰੋ" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "ਅਕੌਂਡ ਸਰਵਰ ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "ਅਕੌਂਡੀ ਸਰਵਰ ਰੋਕਿਆ ਜਾ ਰਿਹਾ ਹੈ..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "ਇੱਥੇ ਭੇਜੋ(&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "ਇੱਥੇ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "ਇੱਥੇ ਲਿੰਕ ਕਰੋ(&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "ਰੱਦ ਕਰੋ(&a)" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "ਅਕੌਂਡੀ ਸਰਵਿਸ ਨਾਲ ਕੁਨੈਕਟ ਨਹੀਂ ਹੈ।" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, fuzzy, kde-format +#| msgid "Details" +msgid "Details..." +msgstr "ਵੇਰਵਾ" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "" + +#: widgets/recentcollectionaction.cpp:43 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Recent Folder" +msgstr "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "ਪਸੰਦੀਦਾ ਨਾਂ ਬਦਲੋ" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox New name of the folder." +#| msgid "Name:" +msgid "Name:" +msgstr "ਨਾਂ:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "ਅਕੌਂਡੀ ਸਰਵਰ ਸੈਲਫ਼-ਟੈਸਟ" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "ਰਿਪੋਰਟ ਸੰਭਾਲੋ..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "ਰਿਪੋਰਟ ਕਲਿੱਪਬੋਰਡ 'ਚ ਕਾਪੀ ਕਰੋ" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "ਡਾਟਾਬੇਸ ਡਰਾਇਵਰ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "ਡਾਟਾਬੇਸ ਡਰਾਇਵਰ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL ਸਰਵਰ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL ਸਰਵਰ ਪੜ੍ਹਨਯੋਗ ਨਹੀਂ ਹੈ।" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL ਚੱਲਣਯੋਗ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL ਗਲਤ ਨਾਂ ਨਾਲ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL ਸਰਵਰ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL ਸਰਵਰ ਲੱਭਿਆ: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL ਸਰਵਰ ਚੱਲਣਯੋਗ ਹੈ।" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL ਸਰਵਰ ਗਲਤੀ ਲਾਗ ਟੈਸਟ ਨਹੀਂ ਕੀਤਾ ਗਿਆ।" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "ਕੋਈ ਮੌਜੂਦਾ MySQL ਗਲਤੀ ਲਾਗ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL ਗਲਤੀ ਲਾਗ ਪੜ੍ਹਨਯੋਗ ਨਹੀਂ।" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "ਇੱਕ MySQL ਸਰਵਰ ਗਲਤੀ ਲਾਗ ਫਾਇਲ ਲੱਭੀ, ਪਰ ਪੜ੍ਹਨਯੋਗ ਨਹੀਂ ਹੈ: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL ਸਰਵਰ ਵਿੱਚ ਗਲਤੀਆਂ ਹਨ।" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL ਸਰਵਰ ਲਾਗ ਫਾਇਲ '%1' ਵਿੱਚ ਗਲਤੀਆਂ ਹਨ।" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL ਸਰਵਰ ਲਾਗ ਵਿੱਚ ਚੇਤਾਵਨੀਆਂ ਹਨ।" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL ਸਰਵਰ ਲਾਗ ਫਾਇਲ '%1' ਵਿੱਚ ਚੇਤਾਵਨੀਆਂ ਹਨ।" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL ਸਰਵਰ ਲਾਗ ਵਿੱਚ ਕੋਈ ਗਲਤੀ ਨਹੀਂ ਹੈ।" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL ਸਰਵਰ ਲਾਗ ਫਾਇਲ '%1' ਵਿੱਚ ਕੋਈ ਗਲਤੀ ਜਾਂ ਚੇਤਾਵਨੀ ਨਹੀਂ ਹੈ।" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL ਸਰਵਰ ਸੰਰਚਨਾ ਟੈਸਟ ਨਹੀਂ ਕੀਤੀ।" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL ਸਰਵਰ ਡਿਫਾਲਟ ਸੰਰਚਨਾ ਮਿਲੀ ਹੈ।" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL ਸਰਵਰ ਡਿਫਾਲਟ ਸੰਰਚਨਾ ਨਹੀਂ ਲੱਭੀ।" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL ਸਰਵਰ ਨਾਲ ਕੁਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ।" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL ਸਰਵਰ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:378 +#, fuzzy, kde-format +#| msgid "The PostgreSQL server was found and connection is working." +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL ਸਰਵਰ ਲਾਗ ਫਾਇਲ '%1' ਵਿੱਚ ਚੇਤਾਵਨੀਆਂ ਹਨ।" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl ਨਹੀਂ ਲੱਭਿਆ" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "ਕੋਈ ਸਰੋਤ ਏਜੰਟ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "ਪਿਛਲਾ ਅਕੌਂਡੀ ਸਰਵਰ ਗਲਤੀ ਲਾਗ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "ਕੋਈ ਮੌਜੂਦਾ ਅਕੌਂਡੀ ਕੰਟਰੋਲ ਗਲਤੀ ਲਾਗ ਨਹੀਂ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "ਮੌਜੂਦਾ ਅਕੌਂਡੀ ਕੰਟਰੋਲ ਗਲਤੀ ਲਾਗ ਲੱਭਿਆ।" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "ਟੈਸਟ ਰਿਪੋਰਟ ਸੰਭਾਲੋ" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "ਗਲਤੀ।" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "ਫਾਇਲ '%1' ਖੋਲ੍ਹੀ ਨਹੀਂ ਜਾ ਸਕੀ" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "ਵੇਰਵਾ" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "ਨਵਾਂ ਫੋਲਡਰ(&N)..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Delete Folder" +msgstr "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "Delete?" +msgid "Delete" +msgstr "ਹਟਾਉਣਾ?" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Folder" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Properties" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "ਚੇਪੋ(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, fuzzy, kde-format +#| msgid "&Paste" +msgid "Paste" +msgstr "ਚੇਪੋ(&P)" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "ਲੋਕਲ ਮੈਂਬਰੀ ਪਰਬੰਧ(&S)...." + +#: widgets/standardactionmanager.cpp:100 +#, fuzzy, kde-format +#| msgid "Manage Local &Subscriptions..." +msgid "Manage Local Subscriptions" +msgstr "ਲੋਕਲ ਮੈਂਬਰੀ ਪਰਬੰਧ(&S)...." + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "ਪਸੰਦੀਦਾ ਫੋਲਡਰ 'ਚ ਸ਼ਾਮਲ" + +#: widgets/standardactionmanager.cpp:108 +#, fuzzy, kde-format +#| msgid "Add to Favorite Folders" +msgid "Add to Favorite" +msgstr "ਪਸੰਦੀਦਾ ਫੋਲਡਰ 'ਚ ਸ਼ਾਮਲ" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "ਪਸੰਦੀਦਾ ਫੋਲਡਰ ਤੋਂ ਹਟਾਓ" + +#: widgets/standardactionmanager.cpp:116 +#, fuzzy, kde-format +#| msgid "Remove from Favorite Folders" +msgid "Remove from Favorite" +msgstr "ਪਸੰਦੀਦਾ ਫੋਲਡਰ ਤੋਂ ਹਟਾਓ" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "ਪਸੰਦੀਦਾ ਨਾਂ ਬਦਲੋ..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, fuzzy, kde-format +#| msgid "&Copy Folder" +#| msgid_plural "&Copy %1 Folders" +msgid "Copy To" +msgstr "ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "ਆਈਟਮ ਕਾਪੀ ਕਰੋ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "ਆਈਟਮ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "Move To" +msgstr "ਆਈਟਮ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "ਫੋਲਡਰ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "ਆਈਟਮ ਕੱਟੋ(&C)" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "ਫੋਲਡਰ ਕੱਟੋ(&C)" + +#: widgets/standardactionmanager.cpp:150 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Create Resource" +msgstr "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Resource" +msgstr "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:153 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "&Resource Properties" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Resource" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:168 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Offline" +msgid "Work Offline" +msgstr "ਆਫਲਾਈਨ" + +#: widgets/standardactionmanager.cpp:188 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Folder Recursively" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:189 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Recursively" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:196 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "&Move Folder To Trash" +msgstr "ਆਈਟਮ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:197 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "Move Folder To Trash" +msgstr "ਆਈਟਮ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:204 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "&Move Item To Trash" +msgstr "ਆਈਟਮ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:205 +#, fuzzy, kde-format +#| msgid "Move Item To..." +msgid "Move Item To Trash" +msgstr "ਆਈਟਮ ਭੇਜੋ..." + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "&Restore Folder From Trash" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Restore Folder From Trash" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "&Restore Item From Trash" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Restore Item From Trash" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:235 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "&Restore Collection From Trash" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:235 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Restore Collection From Trash" +msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#: widgets/standardactionmanager.cpp:246 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Favorite Folders" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:247 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Favorite Folders" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Folder Tree" +msgstr "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ(&C)" +msgstr[1] "%1 ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "ਆਈਟਮ ਕਾਪੀ ਕਰੋ(&C)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "ਆਈਟਮ ਕੱਟੋ(&C)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਕੱਟੋ(&C)" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਕੱਟੋ(&C)" +msgstr[1] "%1 ਫੋਲਡਰ ਕੱਟੋ(&C)" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "ਆਈਟਮ ਹਟਾਓ(&D)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਹਟਾਓ(&D)" +msgstr[1] "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" +msgstr[1] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:347 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "ਫੋਲਡਰ ਹਟਾਓ(&D)" +msgstr[1] "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:348 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" +msgstr[1] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:350 +#, fuzzy, kde-format +#| msgid "&Copy Folder" +#| msgid_plural "&Copy %1 Folders" +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ(&C)" +msgstr[1] "%1 ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/standardactionmanager.cpp:351 +#, fuzzy, kde-format +#| msgid "&Copy Item" +#| msgid_plural "&Copy %1 Items" +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "ਆਈਟਮ ਕਾਪੀ ਕਰੋ(&C)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਕਾਪੀ ਕਰੋ(&C)" + +#: widgets/standardactionmanager.cpp:352 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "ਆਈਟਮ ਕੱਟੋ(&C)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਕੱਟੋ(&C)" + +#: widgets/standardactionmanager.cpp:353 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਕੱਟੋ(&C)" +msgstr[1] "%1 ਫੋਲਡਰ ਕੱਟੋ(&C)" + +#: widgets/standardactionmanager.cpp:354 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "ਆਈਟਮ ਹਟਾਓ(&D)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:355 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਹਟਾਓ(&D)" +msgstr[1] "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:356 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" +msgstr[1] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "ਫੋਲਡਰ ਹਟਾਓ(&D)" +msgstr[1] "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" +msgstr[1] "ਫੋਲਡਰ ਸੈਕਰੋਨਾਈਜ਼(&S)" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "ਨਾਂ" + +#: widgets/standardactionmanager.cpp:368 +#, fuzzy, kde-format +#| msgid "Do you really want to delete folder '%1' and all its sub-folders?" +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "ਕੀ ਤੁਸੀਂ '%1' ਫੋਲਡਰ ਅਤੇ ਇਸ ਦੇ ਸਬ-ਫੋਲਡਰਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" +msgstr[1] "ਕੀ ਤੁਸੀਂ '%1' ਫੋਲਡਰ ਅਤੇ ਇਸ ਦੇ ਸਬ-ਫੋਲਡਰਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" + +#: widgets/standardactionmanager.cpp:371 +#, fuzzy, kde-format +#| msgid "Delete folder?" +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "ਫੋਲਡਰ ਹਟਾਉਣਾ ਹੈ?" +msgstr[1] "ਫੋਲਡਰ ਹਟਾਉਣਾ ਹੈ?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "ਫੋਲਡਰ ਹਟਾਇਆ ਨਹੀਂ ਜਾ ਸਕਿਆ: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "ਫੋਲਡਰ ਹਟਾਉਣਾ ਫੇਲ੍ਹ" + +#: widgets/standardactionmanager.cpp:375 +#, fuzzy, kde-format +#| msgid "Properties of Folder %1" +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "ਫੋਲਡਰ %1 ਦੀ ਵਿਸ਼ੇਸ਼ਤਾ" + +#: widgets/standardactionmanager.cpp:379 +#, fuzzy, kde-format +#| msgid "Do you really want to delete all selected items?" +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "ਕੀ ਤੁਸੀਂ ਸਭ ਚੁਣੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" +msgstr[1] "ਕੀ ਤੁਸੀਂ ਸਭ ਚੁਣੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" + +#: widgets/standardactionmanager.cpp:380 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "ਆਈਟਮ ਹਟਾਓ(&D)" +msgstr[1] "%1 ਆਈਟਮਾਂ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:381 +#, fuzzy, kde-format +#| msgid "Could not delete folder: %1" +msgid "Could not delete item: %1" +msgstr "ਫੋਲਡਰ ਹਟਾਇਆ ਨਹੀਂ ਜਾ ਸਕਿਆ: %1" + +#: widgets/standardactionmanager.cpp:382 +#, fuzzy, kde-format +#| msgid "Folder deletion failed" +msgid "Item deletion failed" +msgstr "ਫੋਲਡਰ ਹਟਾਉਣਾ ਫੇਲ੍ਹ" + +#: widgets/standardactionmanager.cpp:384 +#, fuzzy, kde-format +#| msgid "Rename Favorite" +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "ਪਸੰਦੀਦਾ ਨਾਂ ਬਦਲੋ" + +#: widgets/standardactionmanager.cpp:385 +#, fuzzy, kde-format +#| msgctxt "@label:textbox New name of the folder." +#| msgid "Name:" +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "ਨਾਂ:" + +#: widgets/standardactionmanager.cpp:387 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +msgctxt "@title:window" +msgid "New Resource" +msgstr "ਫੋਲਡਰ ਹਟਾਓ(&D)" + +#: widgets/standardactionmanager.cpp:388 +#, fuzzy, kde-format +#| msgid "Could not create folder: %1" +msgid "Could not create resource: %1" +msgstr "ਫੋਲਡਰ ਬਣ ਨਹੀਂ ਸਕਿਆ: %1" + +#: widgets/standardactionmanager.cpp:389 +#, fuzzy, kde-format +#| msgid "Folder creation failed" +msgid "Resource creation failed" +msgstr "ਫੋਲਡਰ ਬਣਾਉਣਾ ਫੇਲ੍ਹ" + +#: widgets/standardactionmanager.cpp:393 +#, fuzzy, kde-format +#| msgid "Do you really want to delete all selected items?" +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "ਕੀ ਤੁਸੀਂ ਸਭ ਚੁਣੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" +msgstr[1] "ਕੀ ਤੁਸੀਂ ਸਭ ਚੁਣੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" + +#: widgets/standardactionmanager.cpp:396 +#, fuzzy, kde-format +#| msgid "Delete folder?" +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "ਫੋਲਡਰ ਹਟਾਉਣਾ ਹੈ?" +msgstr[1] "ਫੋਲਡਰ ਹਟਾਉਣਾ ਹੈ?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "ਡਾਟਾ ਪਾਰਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "ਚੇਪਣਾ ਫੇਲ੍ਹ" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Offline" +msgctxt "@action:button" +msgid "Go Online" +msgstr "ਆਫਲਾਈਨ" + +#: widgets/standardactionmanager.cpp:1592 +#, fuzzy, kde-format +#| msgid "Copy to This Folder" +msgid "Move to This Folder" +msgstr "ਇਹ ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "ਇਹ ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Manage Local &Subscriptions..." +msgctxt "@title" +msgid "Subscription Error" +msgstr "ਲੋਕਲ ਮੈਂਬਰੀ ਪਰਬੰਧ(&S)...." + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Manage Local &Subscriptions..." +msgid "Local Subscriptions" +msgstr "ਲੋਕਲ ਮੈਂਬਰੀ ਪਰਬੰਧ(&S)...." + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, fuzzy, kde-format +#| msgctxt "search folder" +#| msgid "Search:" +msgid "Search:" +msgstr "ਖੋਜ:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgctxt "@title:column" +#| msgid "Subscribe To" +msgid "&Subscribed only" +msgstr "ਮੈਂਬਰ ਬਣਾਓ" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgctxt "@title:column" +#| msgid "Subscribe To" +msgid "Su&bscribe" +msgstr "ਮੈਂਬਰ ਬਣਾਓ" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgctxt "@title:column" +#| msgid "Unsubscribe From" +msgid "&Unsubscribe" +msgstr "ਇਸ ਦੀ ਮੈਂਬਰੀ ਛੱਡੋ" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete all selected items?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "ਕੀ ਤੁਸੀਂ ਸਭ ਚੁਣੀਆਂ ਆਈਟਮਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "ਆਈਟਮ ਹਟਾਓ(&D)" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "ਆਈਟਮ ਹਟਾਓ(&D)" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "&Delete Item" +#| msgid_plural "&Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "ਆਈਟਮ ਹਟਾਓ(&D)" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, fuzzy, kde-format +#| msgid "No valid destination specified" +msgid "No filename specified" +msgstr "ਕੋਈ ਢੁੱਕਵਾਂ ਟਿਕਾਣਾ ਨਹੀਂ ਦਿੱਤਾ" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgid "Unable to open data file '%1'." +msgstr "ਫਾਇਲ '%1' ਖੋਲ੍ਹੀ ਨਹੀਂ ਜਾ ਸਕੀ" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid file format." +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "ਡਾਟਾ ਪਾਰਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "ਅਢੁੱਕਵਾਂ ਭੰਡਾਰ" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "ਰਿਮੋਟ Id" + +#~ msgid "MimeType" +#~ msgstr "ਮਾਈਮ ਟਾਈਪ" + +#, fuzzy +#~| msgid "Delete?" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "ਹਟਾਉਣਾ?" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "ਰੱਦ ਕਰੋ" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "ਨਾ-ਪੜ੍ਹੇ" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "ਕੁੱਲ" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "ਸਾਈਜ਼" + +#, fuzzy +#~| msgctxt "@title, application name" +#~| msgid "Akonadi Resource" +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "ਅਕੌਂਡੀ ਸਰੋਤ" + +#, fuzzy +#~| msgctxt "@title:column, name of a thing" +#~| msgid "Name" +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "ਨਾਂ" + +#~ msgid "KDE Test Program" +#~ msgstr "KDE ਟੈਸਟ ਪਰੋਗਰਾਮ" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "ਨਵਾਂ ਫੋਲਡਰ(&N)..." + +#, fuzzy +#~| msgid "Folder &Properties" +#~ msgid "Resource Properties" +#~ msgstr "ਫੋਲਡਰ ਵਿਸ਼ੇਸ਼ਤਾ(&P)" + +#~ msgid "Cache" +#~ msgstr "ਕੈਸ਼" + +#~ msgid "Cache Policy" +#~ msgstr "ਕੈਸ਼ ਪਾਲਸੀ" + +#~ msgid "Interval check time:" +#~ msgstr "ਅੰਤਰਾਲ ਚੈਕ ਟਾਈਮ:" + +#~ msgid "Local cache timeout:" +#~ msgstr "ਲੋਕਲ ਕੈਸ਼ ਟਾਈਮ-ਆਉਟ:" + +#~ msgid "Synchronize on demand" +#~ msgstr "ਲੋੜ ਪੈਣ ਉੱਤੇ ਸੈਕਰੋਨਾਈਜ਼ " + +#, fuzzy +#~| msgctxt "search folder" +#~| msgid "Search:" +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "ਖੋਜ:" + +#~ msgid "Available Folders" +#~ msgstr "ਉਪਲੱਬਧ ਫੋਲਡਰ" + +#~ msgid "Current Changes" +#~ msgstr "ਮੌਜੂਦਾ ਬਦਲਾਅ" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "ਚੁਣੇ ਫੋਲਡਰ ਲਈ ਮੈਂਬਰੀ ਹਟਾਓ" + +#~ msgid "TODO" +#~ msgstr "ਟੂ-ਡੂ" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

ਅਕੌਂਡੀ ਓਪਰੇਸ਼ਨਲ ਨਹੀਂ ਹੈ।
ਵੇਰਵਾ...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "ਅਕੌਂਡੀ ਸਰੋਤ" + +#, fuzzy +#~| msgid "No such collection." +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "ਕੋਈ ਭੰਡਾਰ ਨਹੀਂ" +#~ msgstr[1] "ਕੋਈ ਭੰਡਾਰ ਨਹੀਂ" + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "ਫੋਲਡਰ ਕਾਪੀ ਕਰੋ(&C)" diff --git a/po/pl/akonadi_knut_resource.po b/po/pl/akonadi_knut_resource.po new file mode 100644 index 0000000..f764b1b --- /dev/null +++ b/po/pl/akonadi_knut_resource.po @@ -0,0 +1,92 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Marta Rybczyńska , 2009. +# Łukasz Wojniłowicz , 2011. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2011-07-10 12:44+0200\n" +"Last-Translator: Łukasz Wojniłowicz \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.2\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nie wybrano żadnego pliku z danymi." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Plik \"%1\" wczytany poprawnie." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Wybierz plik danych" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Plik danych Akonadi Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nie znaleziono elementu dla zdalnego id %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Nie znaleziono podrzędnej kolekcji w drzewie DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Nie można zapisać kolekcji." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Nie znaleziono zmodyfikowanej kolekcji w drzewie DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Nie znaleziono usuniętej kolekcji w drzewie DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Nie znaleziono podrzędnej kolekcji '%1' w drzewie DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Nie można zapisać elementu." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Nie znaleziono zmodyfikowanego elementu w drzewie DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Nie znaleziono usuniętego elementu w drzewie DOM." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Ścieżka do pliku danych Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Nie zmieniaj rzeczywistych danych." diff --git a/po/pl/libakonadi5.po b/po/pl/libakonadi5.po new file mode 100644 index 0000000..adb2697 --- /dev/null +++ b/po/pl/libakonadi5.po @@ -0,0 +1,2689 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Łukasz Wojniłowicz , 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-19 18:18+0100\n" +"Last-Translator: Łukasz Wojniłowicz \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" +"X-Generator: Lokalize 20.12.1\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Łukasz Wojniłowicz" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "lukasz.wojnilowicz@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Obecnie żadne konto nie ma ustawionego żadnego konta." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integracja kont nie jest obsługiwana" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Nie można zarejestrować obiektu na dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 typu %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identyfikator usługi" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Usługa Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Gotowy" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Rozłączony" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synchronizowanie..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Błąd." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nieustawiony" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identyfikator zasobu" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Zasób Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Otrzymano nieprawidłowy element" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Błąd podczas tworzenia elementu: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Błąd podczas uaktualniania zbioru: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Uaktualnienie lokalnego zbioru zakończone niepowodzeniem: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Uaktualnienie lokalnych elementów zakończone niepowodzeniem: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Nie można pobrać elementu bez połączenia sieciowego." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synchronizowanie katalogu '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Nie udało się pobrać zbioru dla synchronizacji." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Nie udało się pobrać zbioru dla synchronizacji atrybutów." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Żądany element już nie istnieje" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Porzucono zadanie." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nie ma takiego zbioru." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Znaleziono osierocone zbiory" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Nie odnaleziono innego elementu dla rozwiązania konfliktu" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Brak dostępu do interfejsu D-Bus utworzonej usługi." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Przekroczony czas tworzenia wystąpienie usługi." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Nie można uzyskać rodzaju usługi '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Nie można utworzyć wystąpienia usługi." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Niepoprawne wystąpienie zbioru." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Niepoprawne wystąpienie zasobu." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Nie można uzyskać interfejsu D-Bus dla zasobu '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Przekroczony czas synchronizacji atrybutów zbioru." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Nieprawidłowy zbiór do skopiowania" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Nieprawidłowy zbiór docelowy" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Nieprawidłowy rodzic" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Nieudane przetwarzanie zbioru z odpowiedzi" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Nieprawidłowy zbiór" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Podano nieprawidłowy zbiór." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nie określono obiektów do przesunięcia" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Cel nie został poprawnie określony" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Nieprawidłowy zbiór." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Nieprawidłowy zbiór nadrzędny" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Nie można podłączyć się do usługi Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Wersja protokołu Akonadi na serwerze jest niezgodna z naszą. Upewnij się, że " +"masz wgraną zgodną wersję." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operacja przerwana przez użytkownika." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Nieznany błąd." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Nieoczekiwana odpowiedź" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Nie można utworzyć powiązania." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Przekroczony czas synchronizacji zasobu." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Nie można pobrać głównego zbioru zasobu %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Nie podano identyfikatora zasobu." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Niepoprawny identyfikator zasobu '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Nie udało się ustawić domyślnych zasobów poprzez D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Nie udało się pobrać zbioru zasobów." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Przekroczony czas pobierania blokady." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Nieudane tworzenie znacznika." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Nieudane przeniesienie zbioru do kosza, przerywanie operacji kosza" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Przekazano nieprawidłowe elementy" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Przekazano nieprawidłową zbiór" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Brak prawidłowego zbioru lub pusta lista elementów" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Nie można znaleźć zbioru do przywrócenia i zasób przywracania jest " +"niedostępny" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nazwa" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Wczytywanie..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Błąd" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Docelowy zbiór '%1' już zawiera\n" +"zbiór o nazwie '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nazwa" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Nie udało się skopiować elementu: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Nie można skopiować zbioru: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Nie udało się przenieść elementu: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Nie można przenieść zbioru: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Nie można połączyć jednostki: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Błąd" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Ulubione katalogi" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Wszystkie wiadomości" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Nieprzeczytane wiadomości" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Przydział" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Rozmiar przechowalni" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Rozmiar podkatalogu przechowalni" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Nieprzeczytane" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Suma" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Rozmiar" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Znacznik" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Nie udało się pobrać elementu dla spisu" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Spis nie jest już dostępny" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Część bloku danych '%1' nie jest dostępna dla tego spisu" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Brak sesji dla tego spisu" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Brak dostępnych elementów dla tego spisu" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Nienazwana wtyczka" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Brak opisu" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Wersja protokołu serwera Akonadi różni się od wersji protokołu używanej " +"przez tę aplikację.\n" +"Jeśli niedawno uaktualniałeś swój system, to wyloguj się i zaloguj ponownie, " +"aby być pewnym, że wszystkie apikacje używaje poprawnej wersji protokołu." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Żadna z usług Akonadi nie jeste dostępna. Sprawdź swoją instalację ZIO." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Niezgodność wersji protokołu. Wersja serwera jest starsza (%1) niż nasza " +"(%2). Jeśli ostatnio nastąpiło uaktualnienie systemu, to uruchom serwer " +"Akonadi ponownie." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Niezgodność wersji protokołu. Wersja serwera jest nowsza (%1) niż nasza " +"(%2). Jeśli ostatnio nastąpiło uaktualnienie systemu, to uruchom Programy do " +"ZIO ponownie." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Próba Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Sprawdza i zwraca stan serwera Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nowe wystąpienie usługi..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Usuń wystąpienie usługi" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Ustaw wystąpienie usługi" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nowe wystąpienie usługi" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Nie można utworzyć wystąpienia usługi: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Tworzenia wystąpienia usługi nie powiodło się" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Usunąć wystąpienie usługi?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Czy na pewno usunąć zaznaczone wystąpienie usługi?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Ustawienia %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Podręcznik %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "O %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Okno ustawień zostało otwarte w innym oknie" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Ustawienia dla %1 są już otwarte gdzieś indziej." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Nie można zarejestrować okna dialogowego ustawień %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuta" +msgstr[1] "minuty" +msgstr[2] "minuty" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Pobieranie" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Użyj ustawień nadrzędnego katalogu lub konta" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchronizuj po zaznaczeniu tego katalogu" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synchronizuj po:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nigdy" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuty" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokalnie przechowywane moduły" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Ustawienia pobierania" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Zawsze pobieraj pełne wiado&mości" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Pobie&raj treści wiadomości na żądanie" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Przechowaj treści wiadomości lokalnie na czas:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Wieczność" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Szukaj" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Użyj katalogu domyślnie" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nowy podkatalog..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Utwórz nowy podkatalog w obecnie wybranym katalogu" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nowy katalog" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nazwa" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Nie można utworzyć katalogu: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Nie udało się utworzyć katalogu" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Ogólne" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Jeden obiekt" +msgstr[1] "%1 obiekty" +msgstr[2] "%1 obiektów" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nazwa:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Użyj własnej ikony:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "katalog" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statystyki" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Zawartość:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 obiektów" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Rozmiar:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 bajtów" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Pamiętaj, że indeksowanie może zająć dużo czasu." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Konserwacja" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Błąd podczas uzyskiwania liczby zaindeksowanych elementów" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Zaindeksowano %1 element w tym katalogu" +msgstr[1] "Zaindeksowano %1 elementy w tym katalogu" +msgstr[2] "Zaindeksowano %1 elementów w tym katalogu" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Obliczanie zaindeksowanych elementów..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Pliki" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Rodzaj katalogu:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "nieznany" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Elementy" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Razem elementów:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Nieprzeczytane elementy:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indeksowanie" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Włącz indeskowanie pełnego tekstu" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Pobieranie liczby zaindeksowanych elementów ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Ponownie zaindeksuj katalog" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Brak katalogu" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Otwórz okno zbioru" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Wybierz zbiór" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Przenieś tutaj" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "S&kopiuj tutaj" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Anuluj" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Czas zmiany" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flagi" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atrybut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Rozwiązywanie niejednoznaczności" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Wybierz moją wersję" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Wybierz ich wersję" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Zachowaj obie wersje" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Twoje zmiany są w sprzeczności z tym co kto inny zrobił w międzyczasie." +"
Jeśli nie można odrzucić jednej z wersji, to będziesz musiał je ręcznie " +"scalić.
Naciśnij na\"Otwórz edytor tekstu\" aby zachować kopię tekstów, następnie zaznacz, która wersja jest " +"najbardziej poprawna, następnie otwórz ją ponownie i zmień, tak aby dodać, " +"to czego brakuje." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dane" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Uruchamianie serwera Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Zatrzymywanie serwera Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Przenieś tutaj" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "S&kopiuj tutaj" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Dowiąż tutaj" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Anuluj" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Nie można podłączyć się z usługą zarządzania informacjami osobistymi.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Uruchamianie modułu zarządzania informacjami osobistymi..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Zatrzymywanie modułu zarządzania informacjami osobistymi..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Usługa modułu zarządzania informacjami osobistymi wykonuje uaktualnienie " +"bazy danych." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Usługa modułu zarządzania informacjami osobistymi wykonuje uaktualnienie " +"bazy danych.\n" +"Dzieje się tak po uaktualnieniu oprogramowania i jest niezbędne do " +"optymalizacji wydajności.\n" +"W zależności od ilości informacji osobistych, może to zająć kilka minut." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Usługa zarządzania informacjami osobistymi Akonadi nie została uruchomiona. " +"Ta aplikacja nie może bez niej pracować." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Uruchom" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Moduł zarządzania informacjami osobistymi Akonadi nie został uruchomiony.\n" +"Naciśnij na \"Szczegóły...\", aby uzyskać szczegółowe informacje o tym " +"problemie." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Moduł zarządzanie informacjami osobistymi Akonadi nie jest gotowy do pracy." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Szczegóły..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Czy chcesz usunąć konto '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Czy usunąć konto?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Konta poczty przychodzącej (dodaj co najmniej jedno):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Dodaj..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Z&mień..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Usuń" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Uruchom ponownie" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Ostatni katalog" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Przemianuj ulubione" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nazwa:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Próba serwera Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Zapisz sprawozdanie..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Skopiuj sprawozdanie do schowka" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Sterownik QtSQL \"%1\" jest wymagany przez twoje ustawienie serwera Akonadi " +"i został znaleziony w systemie." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Sterownik QtSQL \"%1\" jest wymagany przez twoje ustawienie serwera " +"Akonadi.\n" +"Następujące sterowniki są wgrane: \"%2\".\n" +"Upewnij się, że wymagany sterownik został wgrany." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Znaleziono sterownik bazy danych." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Nie znaleziono sterownika bazy danych." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Nie testowano pliku wykonywalnego serwera MySQL." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Bieżące ustawienie nie wymaga zewnętrznego serwera MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Używanie serwera MySQL \"%1\" w Akonadi właśnie zostało ustawione.\n" +"Upewnij się, że masz wgrany serwer MySQL, ustaw poprawną ścieżkę i sprawdź, " +"czy masz niezbędne prawa odczytu i wykonywania do pliku wykonywalnego " +"serwera. Serwer wykonywalny jest często nazywany \"mysqld\"; jego położenie " +"różni się zależnie od dystrybucji." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Nie znaleziono serwera MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Nie masz praw do odczytu serwera MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Nie masz praw wykonywania serwera MySQL." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Znaleziono serwer MySQL o niespodziewanej nazwie." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Znaleziono serwer MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Znaleziono serwer MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Serwer MySQL jest wykonywalny." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Nie udało się uruchomić serwera MySQL \"%1\" z powodu następującego błędu: " +"\"%2\"" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Nie udało się wykonać serwera MySQL." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Nie testowano dziennika błędów serwera MySQL." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Nie znaleziono aktualnego dziennika błędów MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Serwer MySQL nie zgłosił żadnych błędów podczas uruchomienia. " +"Dziennikzapisano w \"%1\"." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Nie masz praw odczytu dziennika błędów MySQL." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Znaleziono dziennik błędów serwera MySQL, lecz nie jest on do odczytu: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Dziennik serwera MySQL zawiera błędy." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Dziennik błędów serwera MySQL \"%1\" zawiera błędy." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Dziennik serwera MySQL zawiera ostrzeżenia." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Plik dziennika serwera MySQL \"%1\" zawiera ostrzeżenia." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Dziennik serwera MySQL nie zawiera błędów." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Plik dziennika serwera MySQL \"%1\" nie zawiera żadnych błędów ani ostrzeżeń." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Nie wypróbowano ustawień serwera MySQL." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Znaleziono domyślne ustawienia serwera MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Znaleziono domyślne ustawienia serwera MySQL i są ona do odczytu w %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Nie znaleziono domyślnych ustawień serwera MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Nie znaleziono domyślnych ustawień serwera MySQL lub nie są one do odczytu. " +"Sprawdź, czy twoja instalacja Akonadi jest całkowita i czy masz wymagane " +"prawa dostępu." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Własne ustawienia serwera MySQL nie są dostępne." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "Znaleziono własne ustawienie serwera MySQL, lecz są one opcjonalne." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Znaleziono własne ustawienia serwera MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Znaleziono własne ustawienia serwera MySQL i są ono do odczytu w %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Nie masz praw odczytu własnych ustawień serwera MySQL." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Znaleziono własne ustawienia serwera MySQL q %1, lecz nie są one do odczytu. " +"Sprawdź swoje prawa dostępu." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" +"Nie znaleziono ustawień serwera MySQL lub nie masz praw do ich odczytu." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Nie znaleziono ustawień serwera MySQL lub nie są one do odczytu." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Ustawienie serwera MySQL są do wykorzystania." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Znaleziono ustawienia serwera MySQL w %1 i są one do odczytu." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Nie można podłączyć się do serwera PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Znaleziono serwer PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Serwer PostgreSQL został znaleziony. Połączenie działa poprawnie." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "nie znaleziono akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Program \"akonadictl\" powinien być dostępny w $PATH. Upewnij się, że masz " +"zainstalowany serwer Akonadi." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "znaleziono akonadictl i można go użyć" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Znaleziono program %1, kontrolujący serwer Akonadi, i można było go " +"uruchomić.\n" +"Wynik:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "znaleziono akonadictl, lecz nie można go użyć" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Znaleziono program %1, kontrolujący serwer Akonadi, lecz nie można było go " +"prawidłowo wykonać.\n" +"Wynik:\n" +"%2\n" +"Upewnij się, że serwer Akonadi został prawidłowo zainstalowany." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Zarejestrowano proces sterujący Akonadi w D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Zarejestrowano proces sterujący Akonadi w D-Bus, co oznacza zazwyczaj, że " +"działa." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Nie zarejestrowano procesu sterującego Akonadi w D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Nie zarejestrowano procesu sterującego Akonadi w D-Bus, co oznacza, że nie " +"został uruchomiony lub podczas uruchamiania wystąpił błąd krytyczny." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Zarejestrowano proces serwera Akonadi w D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Zarejestrowano proces serwera Akonadi w D-Bus, co oznacza, że jest on " +"operacyjny." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Nie zarejestrowano procesu serwera Akonadi w D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Nie zarejestrowano procesu serwera Akonadi w D-Bus, co oznacza, że nie " +"został on uruchomiony lub podczas uruchamiania wystąpił błąd krytyczny." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Nie można sprawdzić wersji protokołu." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Bez połączenia z serwerem nie można sprawdzić, czy wersja protokołu jest " +"zgodna z wymaganiami." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Wersja protokołu serwera jest za stara." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Wersja protokołu serwera to %1, lecz przez klienta wymagana jest co najmniej " +"wersja %2. Jeśli ostatnio nastąpiło uaktualnienie programów do ZIO, to " +"uruchom ponownie zarówno Akonadi jak i programy do ZIO." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Wersja protokołu jest zbyt nowa." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Wersja protokołu serwera zgadza się." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Obecna wersja protokołu to %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Znaleziono usługi zasobów." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Znaleziono co najmniej jedną usługę zasobów." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Nie znaleziono usług zasobów." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nie znaleziono żadnych usług zasobów. Akonadi jest nieużyteczny bez co " +"najmniej jednej usługi. Oznacza to, że nie zainstalowano żadnych usługi lub " +"wystąpił błąd w ustawieniach. Przeszukano następujące ścieżki: \"%1\". " +"Zmienna środowiskowa XDG_DATA_DIRS została ustawiona na \"%2\"; upewnij się, " +"że zostały włączone wszystkie ścieżki, w których zainstalowano usługi " +"Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Nie znaleziono aktualnego dziennika błędów serwera Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Serwer Akonadi nie zgłosił żadnych błędów podczas bieżącego uruchomienia." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Znaleziono aktualny dziennik błędów serwera Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Serwer Akonadi zgłosił błędy podczas bieżącego uruchomienia. Dziennik " +"zapisano w \"%1\"." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Nie znaleziono poprzedniego dziennika błędów serwera Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Serwer Akonadi nie zgłosił żadnych błędów podczas poprzedniego uruchomienia." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Znaleziono poprzedni dziennik błędów serwera Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Serwer Akonadi zgłosił błędy podczas poprzedniego uruchomienia. Dziennik " +"zapisano w \"%1\"." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Nie znaleziono aktualnego dziennika błędów kontroli Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Proces kontrolny Akonadi nie zgłosił żadnych błędów podczas bieżącego " +"uruchomienia." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Znaleziono aktualny dziennik błędów kontroli Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Proces kontrolny Akonadi zgłosił błędy podczas bieżącego uruchomienia. " +"Dziennik zapisano w \"%1\"." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Nie znaleziono poprzedniego dziennika błędów kontroli Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Proces kontrolny Akonadi nie zgłosił żadnych błędów podczas ostatniego " +"uruchomienia." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Znaleziono poprzedni dziennik błędów kontroli Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Proces kontrolny Akonadi zgłosił błędy podczas poprzedniego uruchomienia. " +"Dziennik zapisano w \"%1\"." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi został uruchomiony jako root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Uruchamianie aplikacji korzystających z Internetu jako root/administrator " +"może zagrażać bezpieczeństwu. MySQL, używany przez Akonadi, nie zezwala na " +"pracę jako root, aby chronić przed ryzykiem." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi nie jest uruchomiony jako root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi nie jest uruchomiony jako root/administrator, co jest zalecane dla " +"zabezpieczenia systemu." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Zapisz sprawozdanie z prób" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Błąd" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Nie można otworzyć pliku '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Wystąpił błąd podczas uruchamiania serwera Akonadi. Następujące testy mogą " +"pomóc w wyśledzeniu i rozwiązaniu problemu. Proszę dołączyć to sprawozdanie " +"przy wysyłaniu prośby o pomoc lub zgłaszaniu błędów." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Szczegóły" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Więcej porad na temat rozwiązywania problemów można znaleźć na userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nowy katalog..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nowy" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Usuń katalog" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Usuń" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Synchronizuj katalog" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchronizuj" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Właściwości katalogu" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Właściwości" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Wklej" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Wklej" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Zarządzanie lokalnymi &subskrypcjami.." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Zarządzanie lokalnymi subskrypcjami" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Dodaj do katalogów ulubionych" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Dodaj do ulubionych" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Usuń z ulubionych katalogów" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Usuń z ulubionych" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Zmień nazwę ulubionych..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Zmień nazwę" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Skopiuj katalog do..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Skopiuj do" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Skopiuj element do..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Przenieś element do..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Przenieś do" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Przenieś katalog do..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Wytnij element" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Wytnij" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "&Wytnij katalog" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Utwórz zasób" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Usuń zasób" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Właściwości zasobu" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synchronizuj zasób" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Pracuj w trybie bez sieci" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synchronizuj katalog rekursywnie" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synchronizuj rekursywnie" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Przenieś katalog do kosza" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Przenieś katalog do kosza" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Przenieś element do kosza" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Przenieś element do kosza" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "P&rzywróć katalog z kosza" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "P&rzywróć katalog z kosza" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "P&rzywróć element z kosza" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Przywróć element z kosza" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "P&rzywróć zbiór z kosza" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Przywróć zbiór z kosza" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synchronizuj ulubione katalogi" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synchronizuj ulubione katalogi" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synchronizuj drzewo katalogu" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "S&kopiuj katalog" +msgstr[1] "S&kopiuj %1 katalogi" +msgstr[2] "S&kopiuj %1 katalogów" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "S&kopiuj element" +msgstr[1] "S&kopiuj %1 elementy" +msgstr[2] "S&kopiuj %1 elementów" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Wytnij element" +msgstr[1] "&Wytnij %1 elementy" +msgstr[2] "&Wytnij %1 elementów" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Wytnij katalog" +msgstr[1] "&Wytnij %1 katalogi" +msgstr[2] "&Wytnij %1 katalogów" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Usuń element" +msgstr[1] "&Usuń %1 elementy" +msgstr[2] "&Usuń %1 elementów" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Usuń katalog" +msgstr[1] "Usuń %1 &katalogi" +msgstr[2] "Usuń %1 &katalogów" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synchronizuj katalog" +msgstr[1] "&Synchronizuj %1 katalogi" +msgstr[2] "&Synchronizuj %1 katalogów" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Usuń zasób" +msgstr[1] "&Usuń %1 zasoby" +msgstr[2] "&Usuń %1 zasobów" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synchronizuj zasób" +msgstr[1] "&Synchronizuj %1 zasoby" +msgstr[2] "&Synchronizuj %1 zasobów" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Skopiuj katalog" +msgstr[1] "Skopiuj %1 katalogi" +msgstr[2] "Skopiuj %1 katalogów" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Skopiuj element" +msgstr[1] "Skopiuj %1 elementy" +msgstr[2] "Skopiuj %1 elementów" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Wytnij element" +msgstr[1] "Wytnij %1 elementy" +msgstr[2] "Wytnij %1 elementów" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Wytnij katalog" +msgstr[1] "Wytnij %1 katalogi" +msgstr[2] "Wytnij %1 katalogów" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Usuń element" +msgstr[1] "Usuń %1 elementy" +msgstr[2] "Usuń %1 elementów" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Usuń katalog" +msgstr[1] "Usuń %1 katalogi" +msgstr[2] "Usuń %1 katalogów" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synchronizuj katalog" +msgstr[1] "Synchronizuj %1 katalogi" +msgstr[2] "Synchronizuj %1 katalogów" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Usuń zasób" +msgstr[1] "Usuń %1 zasoby" +msgstr[2] "Usuń %1 zasobów" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synchronizuj zasób" +msgstr[1] "Synchronizuj %1 zasoby" +msgstr[2] "Synchronizuj %1 zasobów" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nazwa" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Czy na pewno usunąć ten katalog i wszystkie jego podkatalogi?" +msgstr[1] "Czy na pewno usunąć %1 katalogi i wszystkie ich podkatalogi?" +msgstr[2] "Czy na pewno usunąć %1 katalogów i wszystkie ich podkatalogi?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Usunąć katalog?" +msgstr[1] "Usunąć katalogi?" +msgstr[2] "Usunąć katalogi?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Nie można usunąć katalogu: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Nieudane usuwanie katalogu" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Właściwości katalogu %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Czy na pewno usunąć zaznaczone elementy?" +msgstr[1] "Czy na pewno usunąć %1 elementy?" +msgstr[2] "Czy na pewno usunąć %1 elementów?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Usunąć element?" +msgstr[1] "Usunąć elementy?" +msgstr[2] "Usunąć elementy?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Nie można usunąć elementu: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Usuwanie elementu nie powiodło się" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Zmień nazwę ulubionych" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nazwa:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nowy zasób" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Nie udało się utworzyć zasobu: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Tworzenie zasobu nie powiodło się" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Czy na pewno usunąć ten zasób?" +msgstr[1] "Czy na pewno usunąć %1 zasoby?" +msgstr[2] "Czy na pewno usunąć %1 zasobów?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Usunąć zasób?" +msgstr[1] "Usunąć zasoby?" +msgstr[2] "Usunąć zasoby?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Nie można wkleić danych: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Wklejanie nieudane" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Nie można dodać \"/\" do nazwy katalogu." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Błąd tworzenia nowego katalogu" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Nie można dodać \".\" ani do początku ani do końca nazwy katalogu." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Przed synchronizacją katalogu \"%1\" konieczne jest podłączenie go do sieci. " +"Czy chcesz podłączyć go do sieci?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Konto \"%1\" odłączono od sieci" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Przejdź do trybu z siecią" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Przenieś do tego katalogu" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Skopiuj do tego katalogu" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Nie udało się uaktualnić subskrypcji: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Błąd subskrypcji" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Lokalne subskrypcje" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Znajdź:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Tylko &subskrybowane" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Su&bskrybuj" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Wyłącz s&ubskrypcję" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Nie udało się utworzyć nowego znacznika" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Wystąpił błąd przy tworzeniu nowego znacznika" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Czy na pewno usunąć znacznik %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Usuń znacznik" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Usuwanie znacznika" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Wybierz znaczniki, które zastosować." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Utwórz nowy znacznik" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Zarządzanie znacznikami" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Wybierz znaczniki..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Wybór znaczników" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Wyczyść" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Naciśnij, aby dodać znaczniki" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Przekształcenie Akonadi do XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Przekształca poddrzewo zbioru Akonadi na plik XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nie wczytano danych." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nie podano nazwy pliku" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Nie można otworzyć pliku danych '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Plik %1 nie istnieje." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Nie można przetworzyć pliku danych '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Nie można wczytać i przetworzyć definicji schematu." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Nie można utworzyć kontekstu przetwarzania schematu." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Nie można utworzyć schematu." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Nie można utworzyć kontekstu potwierdzenia schematu." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Nieprawidłowy format pliku." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Nie można przetworzyć pliku danych: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Nie można znaleźć zbioru %1" + +#~ msgid "Id" +#~ msgstr "Identyfikator" + +#~ msgid "Remote Id" +#~ msgstr "Zdalne Id" + +#~ msgid "MimeType" +#~ msgstr "Typ Mime" + +#~ msgid "Default Name" +#~ msgstr "Domyślna nazwa" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Usuń" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Anuluj" + +#~ msgid "Take left one" +#~ msgstr "Wybierz lewą" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Dwa uaktualnienie są sprzeczne wobec siebie.Wybierz którą (które) " +#~ "zastosować." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Nieprzeczytane" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Suma" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Rozmiar" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Zasób Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Nazwa" + +#~ msgid "Invalid collection specified" +#~ msgstr "Podano nieprawidłowy zbiór" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Wykryto protokół w wersji %1, spodziewano się co najmniej %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Wersja protokołu serwera jest nowsza, niż wymagana." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Wersja protokołu serwera to %1, czyli jest nowsza niż wymagana wersja %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Wykryto niespójności w lokalnym drzewie zbioru." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "Dostarczono zdalną zbiór głównego węzła, zasób jest uszkodzony." + +#~ msgid "KDE Test Program" +#~ msgstr "Program próbny KDE" diff --git a/po/pt/akonadi_knut_resource.po b/po/pt/akonadi_knut_resource.po new file mode 100644 index 0000000..afc4083 --- /dev/null +++ b/po/pt/akonadi_knut_resource.po @@ -0,0 +1,85 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2016-01-19 11:06+0000\n" +"Last-Translator: José Nuno Coelho Pires \n" +"Language-Team: Portuguese \n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-POFile-SpellExtra: Knut\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Não foi seleccionado nenhum ficheiro de dados." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "O ficheiro '%1' foi carregado com sucesso." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Seleccione o Ficheiro de Dados" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Ficheiro de Dados do Knut para o Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Não foi encontrado nenhum item remoto com o ID %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "A colecção-mãe não foi encontrada na árvore de DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Não é possível gravar a colecção." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "A colecção modificada não foi encontrada na árvore de DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "A colecção removida não foi encontrada na árvore de DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "A colecção-mãe '%1' não foi encontrada na árvore de DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Não foi possível gravar o item." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "O item modificado não foi encontrado na árvore de DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "O item removido não foi encontrado na árvore de DOM." diff --git a/po/pt/libakonadi5.po b/po/pt/libakonadi5.po new file mode 100644 index 0000000..a91c611 --- /dev/null +++ b/po/pt/libakonadi5.po @@ -0,0 +1,2613 @@ +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-02 13:13+0000\n" +"Last-Translator: José Nuno Coelho Pires \n" +"Language-Team: pt \n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-POFile-SpellExtra: Akonadi Id QtSQL TextLabel akonadictl mysqld\n" +"X-POFile-SpellExtra: XDGDATADIRS PostgreSQL Sesame DBus Volker Krause\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "José Nuno Pires" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "zepires@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Não está nenhuma conta configurada de momento." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "A integração de contas não está suportada" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Não foi possível registar o objecto no DBus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 do tipo %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificador do agente" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agente do Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pronto" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Desligado" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "A sincronizar..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Erro." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Não configurado" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificador do recurso" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Recurso do Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Foi obtido um item inválido" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Erro ao criar o item: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Erro ao actualizar a colecção: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "A actualização da colecção local falhou: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "A actualização dos itens locais falhou: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Não é possível obter o item no modo desligado." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "A sincronizar a pasta '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Não foi possível obter a colecção para a sincronização." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Não foi possível obter a colecção para a sincronização de atributos." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "O item pedido já não existe mais" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "A tarefa foi cancelada." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Não existe essa colecção." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Foram detectadas colecções-órfãs não resolvidas" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Não foi encontrado o outro item para o tratamento do conflito" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Não foi possível obter a interface de D-Bus para o agente criado." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "A criação da instância do agente expirou o tempo-limite." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Não foi possível obter o tipo de agente '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Não foi possível criar a instância do agente." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "A instância da colecção é inválida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "A instância do recurso é inválida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Não foi possível obter a interface de D-Bus para o recurso '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "A sincronização dos atributos da colecção expirou o tempo-limite." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Colecção a copiar inválida" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Colecção de destino inválida" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "A instância-mãe é inválida" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Não foi possível processar a colecção a partir da resposta" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Colecção inválida" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Foi indicado um nome de colecção inválido." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Não foram indicados nenhuns itens a mover" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Não foi indicado nenhum destino válido" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "A colecção é inválida." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Colecção-mãe inválida" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Não é possível contactar o serviço do Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"A versão do protocolo do servidor de Akonadi é incompatível. Certifique-se " +"que tem instalada uma versão incompatível." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "A operação foi cancelada pelo utilizador." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "O erro é desconhecido." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Resposta inesperada" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Não foi possível criar a relação." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "A sincronização do recurso expirou o tempo-limite." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Não foi possível obter a colecção de topo do recurso %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Não foi indicado nenhum ID de recurso." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "O identificador do recurso '%1' é inválido." + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Não foi possível configurar o recurso predefinido através de D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Não foi possível obter a colecção de recursos." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Expirou o tempo-limite para obter o bloqueio." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Não foi possível criar a marca." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"O envio para a colecção do lixo foi mal-sucedido, a interromper a operação " +"do lixo" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Foram passados itens inválidos" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Foi passada uma colecção inválida" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "A colecção é inválida ou está em branco" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Não foi possível repor a colecção e o recurso de reposição não está " +"disponível" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "A carregar..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Erro" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"A colecção de destino '%1' já contém\n" +"uma colecção chamada '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Não foi possível copiar o item: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Não foi possível copiar a colecção: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Não foi possível mover o item: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Não foi possível mover a colecção: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Não foi possível associar a entidade: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Erro" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Pastas Favoritas" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Mensagens Totais" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Mensagens Não-Lidas" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Tamanho do Armazenamento" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Tamanho do Armazenamento da Sub-Pasta" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Não-Lidas" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Tamanho" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Marca" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Não é possível obter o item do índice" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "O índice já não está mais disponível" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "A componente de conteúdo '%1' não está disponível para este índice" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Não está disponível nenhuma sessão para este índice" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Não está disponível nenhum item para este índice" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "'Plugin' sem nome" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "A descrição não está disponível" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"A versão do protocolo do servidor do Akonadi é diferente da versão do " +"protocolo usado por esta aplicação.\n" +"Se actualizou recentemente o seu sistema, por favor encerre a sessão e ligue-" +"se de novo para garantir que todas as aplicações usam a versão correcta do " +"protocolo." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Não existem agentes do Akonadi disponíveis. Verifique por favor a sua " +"instalação do KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"As versões do protocolo não correspondem. A versão do servidor é mais antiga " +"(%1) que a nossa (%2). Se actualizou recentemente o seu sistema, por favor " +"reinicie o servidor do Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"As versões do protocolo não correspondem. A versão do servidor é mais " +"recente (%1) que a nossa (%2). Se actualizou recentemente o seu sistema, por " +"favor reinicie todas as aplicações do KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Auto-Teste do Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Verifica e comunica o estado do serviço do Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nova Instância do Agente..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Apa&gar a Instância do Agente" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configurar a Instância do Agente" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nova Instância do Agente" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Não foi possível criar a instância do agente: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "A criação da instância do agente foi mal-sucedida" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Apagar a Instância do Agente?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Deseja mesmo apagar a instância do agente seleccionada?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuração do %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manual do %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Acerca do %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "A janela de configuração foi aberta noutra janela" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "A configuração do %1 já está aberta noutro lado." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Não foi possível registar a janela de configuração do %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuto" +msgstr[1] "minutos" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Obtenção" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usar as opções da pasta-mãe ou conta-mãe" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronizar ao seleccionar esta pasta" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronizar automaticamente ao fim de:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nunca" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutos" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Componentes na 'Cache' Local" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opções de Recepção" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Obter sempre as &mensagens completas" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Obte&r o conteúdo das mensagens a pedido" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Manter o conteúdo das mensagens a nível local durante:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Para Sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Procurar" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usar a pasta por omissão" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nova Sub-Pasta..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Criar uma nova sub-pasta sob a pasta seleccionada de momento" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nova Pasta" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nome" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Não foi possível criar a pasta: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "A criação da pasta falhou" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Geral" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Um objecto" +msgstr[1] "%1 objectos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nome:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usar um ícone personalizado:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "pasta" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estatísticas" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Conteúdo:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objectos" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Tamanho:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Lembre-se que a indexação poderá demorar alguns minutos." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Manutenção" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Erro ao obter a quantidade de itens indexados" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Foi indexado %1 item nesta pasta" +msgstr[1] "Foram indexados %1 itens nesta pasta" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "A calcular os itens indexados..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Ficheiros" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipo de pasta:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "desconhecido" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Itens" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Itens no total:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Itens não-lidos:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexação" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Activar a indexação por texto completo" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "A obter a quantidade de itens indexados..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indexar de novo a pasta" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Sem Pasta" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Abrir a janela da colecção" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Seleccionar uma colecção" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mover para aqui" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copiar para aqui" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancelar" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Hora da Modificação" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Opções" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atributo: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolução de Conflitos" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Usar a minha versão" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Usar a versão deles" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Manter ambas as versões" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"As suas alterações entram em conflito com as que foram feitas por outras " +"pessoas entretanto.
A menos que uma das versões possa ser simplesmente " +"eliminada, terá de integrar essas alterações manualmente.
Carregue em \"Abrir um editor de texto\" para manter uma " +"cópia dos textos, depois seleccione a versão que é mais correcta, volte a " +"abri-lo e modifique-o de novo para adicionar o que faltar." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dados" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "A iniciar o servidor do Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "A parar o servidor do Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mover para Aqui" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copiar para Aqui" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Criar uma &Ligação Aqui" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancelar" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Não é possível contactar o serviço de gestão de informações pessoais.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "O serviço de gestão de informações pessoais está a iniciar..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "O serviço de gestão de informações pessoais está a encerrar..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"O serviço de gestão de informações pessoais está a actualizar a base de " +"dados." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"O serviço de gestão de informações pessoais está a efectuar uma actualização " +"da base de dados.\n" +"Isto acontece após uma actualização da aplicação e é necessária para " +"optimizar a performance.\n" +"Dependendo da quantidade de informações pessoais, isto poderá levar alguns " +"minutos." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"A plataforma de gestão de informações pessoais Akonadi não está a correr. " +"Esta aplicação não pode ser usada sem ela." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Iniciar" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"A plataforma de gestão de informações pessoais Akonadi não está " +"operacional.\n" +"Carregue em \"Detalhes...\" para obter informações detalhadas sobre este " +"problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"A plataforma de gestão de informações pessoais Akonadi não está operacional." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalhes..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Deseja remover a conta '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Remover a conta?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Contas de recepção (adicione pelo menos uma):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&dicionar..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modificar..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "R&emover" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reiniciar" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Pasta Recente" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Mudar o Nome do Favorito" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nome:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Auto-Teste do Servidor do Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Gravar o Relatório..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copiar o Relatório para a Área de Transferência" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"O controlador do QtSQL '%1' é necessário para a sua configuração actual do " +"servidor do Akonadi e foi encontrado no seu sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"O controlador do QtSQL '%1' é necessário para a sua configuração actual do " +"servidor do Akonadi.\n" +"Estão instalados os seguintes controladores: %2.\n" +"Certifique-se que o controlador necessário está instalado." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Foi encontrado um controlador de base de dados." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Não foi encontrado nenhum controlador de base de dados." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "O executável do servidor de MySQL não foi testado." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "A configuração actual não necessita de um servidor de MySQL interno." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Neste momento, tem o Akonadi configurado para usar o servidor de MySQL " +"'%1'.\n" +"Certifique-se que tem o servidor de MySQL instalado, a sua localização " +"devidamente indicada e garantir que tem as permissões de leitura e execução " +"necessárias para o programa do servidor. O executável do servidor chama-se " +"normalmente 'mysqld', embora a sua localização varie de acordo com as " +"distribuições." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "O servidor de MySQL não foi encontrado." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "O servidor de MySQL não está acessível para leitura." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "O servidor de MySQL não está acessível para execução." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "O MySQL foi encontrado, com um nome inesperado." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Foi encontrado um servidor de MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Servidor de MySQL encontrado: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "O servidor de MySQL é executável." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"A execução do servidor de MySQL '%1' falhou com a seguinte mensagem de erro: " +"'%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "A execução do servidor de MySQL foi mal-sucedida." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "O registo de erros do servidor de MySQL não foi testado." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Não foi encontrado nenhum registo de erros actual do MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"O servidor de MySQL não devolveu nenhuns erros no seu arranque. O registo " +"pode ser visto em '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "O registo de erros do MySQL não está acessível para leitura." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Foi encontrado um registo de erros do servidor de MySQL, mas não está " +"acessível: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "O registo do servidor de MySQL contém erros." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "O ficheiro de registo de erros do servidor de MySQL '%1' contém erros." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "O registo do servidor de MySQL contém avisos." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "O ficheiro de registo do servidor de MySQL '%1' contém avisos." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "O registo do servidor de MySQL não contém erros." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"O ficheiro de registo do servidor de MySQL '%1' não contém quaisquer erros " +"ou avisos." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "A configuração do servidor de MySQL não foi testada." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Foi encontrada a configuração predefinida do servidor de MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Foi encontrada a configuração predefinida do servidor de MySQL e está " +"acessível em %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Não foi encontrada a configuração predefinida do servidor de MySQL." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Não foi encontrada a configuração predefinida do servidor de MySQL ou então " +"não está acessível. Verifique se a sua instalação do Akonadi está completa e " +"se tem todas as permissões de acesso necessárias." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "A configuração personalizada do servidor de MySQL não está disponível." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"A configuração personalizada do servidor de MySQL não foi encontrada; porém, " +"esta é opcional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Foi encontrada a configuração personalizada do servidor de MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"A configuração personalizada do servidor de MySQL foi encontrada e está " +"acessível em %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" +"A configuração personalizada do servidor de MySQL não está acessível para " +"leitura." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"A configuração personalizada do servidor de MySQL foi encontrada em %1; " +"porém, não está acessível. Verifique as suas permissões de acesso." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" +"A configuração do servidor de MySQL não foi encontrada ou não está acessível " +"para leitura." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"A configuração do servidor MySQL não foi encontrada ou então não está " +"acessível." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "A configuração do servidor de MySQL pode ser usada." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"A configuração do servidor MySQL foi encontrada em %1 e está acessível." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Não é possível contactar o servidor de PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Foi encontrado um servidor de PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "O servidor de PostgreSQL foi encontrado e a ligação está funcional." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "O 'akonadictl' não foi encontrado" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"O programa 'akonadictl' precisa de estar acessível no $PATH. Certifique-se " +"que tem o servidor do Akonadi instalado." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "O 'akonadictl' foi encontrado e pode ser usado" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"O programa '%1' para controlar o servidor do Akonadi foi encontrado e pôde " +"ser executado com sucesso.\n" +"Resultado:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "O 'akonadictl' foi encontrado mas não pode ser usado" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"O programa '%1' para controlar o servidor do Akonadi foi encontrado, mas não " +"pôde ser executado com sucesso.\n" +"Resultado:\n" +"%2\n" +"Certifique-se que o servidor do Akonadi está instalado correctamente." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "O processo de controlo do Akonadi está registado no D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"O processo de controlo do Akonadi está registado no D-Bus, o que significa " +"normalmente que está operacional." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "O processo de controlo do Akonadi não está registado no D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"O processo de controlo do Akonadi não está registado no D-Bus, o que " +"significa normalmente que não foi iniciado ou que obteve um erro fatal no " +"seu arranque." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "O processo do servidor do Akonadi está registado no D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"O processo do servidor do Akonadi está registado no D-Bus, o que significa " +"normalmente que está operacional." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "O processo do servidor do Akonadi não está registado no D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"O processo do servidor do Akonadi não está registado no D-Bus, o que " +"significa normalmente que não foi iniciado ou que obteve um erro fatal no " +"seu arranque." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Não é possível verificar a versão do protocolo." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Sem uma ligação ao servidor, não é possível verificar se a versão do " +"protocolo corresponde aos requisitos." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "A versão do protocolo do servidor é demasiado antiga." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"A versão do protocolo do servidor é a %1, mas é necessária pelo menos a %2 " +"por parte do cliente. Se tiver actualizado recentemente o KDE PIM, " +"certifique-se por favor que reinicia tanto o Akonadi como as aplicações do " +"KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "A versão do protocolo do servidor é demasiado recente." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "A versão do protocolo do servidor corresponde." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "A versão actual do protocolo do servidor é a %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Foram encontrados os agentes de recursos." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Foi encontrado pelo menos um agente de recursos." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Não foram encontrados quaisquer agentes de recursos." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Não foram encontrados agentes de recursos; o Akonadi não pode ser usado sem " +"ter pelo menos um. Isto significa normalmente que não estão instalados " +"agentes de recursos ou que ocorreu um problema de configuração. Foram " +"pesquisadas as seguintes localizações: '%1'. A variável de ambiente " +"XDG_DATA_DIRS está configurada como '%2'; certifique-se que esta inclui " +"todos os locais onde estão instalados os agentes do Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Não foi encontrado nenhum registo de erro do servidor do Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"O servidor do Akonadi não devolveu nenhuns erros no seu arranque actual." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Foi encontrado o registo de erros do servidor do Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"O servidor do Akonadi devolveu nenhuns erros no seu arranque actual. O " +"registo poderá ser visto em %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" +"Não foi encontrado nenhum registo com os erros anteriores do servidor do " +"Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"O servidor do Akonadi não devolveu nenhuns erros no seu arranque anterior." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Foi encontrado um registo de erros anteriores do servidor do Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"O servidor do Akonadi devolveu alguns erros no seu arranque anterior. O " +"registo pode ser visto em '%1'." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Não foi encontrado nenhum registo de erros de controlo do Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"O processo de controlo do Akonadi não devolveu nenhuns erros no seu arranque " +"actual." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Foi encontrado um registo de erros de controlo do Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"O processo de controlo do Akonadi não devolveu nenhuns erros no seu arranque " +"actual. O registo pode ser visto em '%1'." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Foi encontrado um registo de erros de controlo do Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"O processo de controlo do Akonadi não devolveu nenhuns erros no seu arranque " +"anterior." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Foi encontrado um registo com erros de controlo do Akonadi anteriores." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"O processo de controlo do Akonadi devolveu alguns erros no seu arranque " +"anterior. O registo pode ser visto em '%1'." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "O Akonadi foi iniciado como administrador" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"A execução de aplicações vocacionadas para a Internet como 'root' ou " +"administrador podê-lo-á expor a vários riscos de segurança. O MySQL, que é " +"usado por esta instalação do Akonadi, não poderá ser ele próprio executado " +"como administrador, de modo a protegê-lo destes riscos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "O Akonadi não foi iniciado como administrador" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"O Akonadi não está a correr com um super-utilizador; esta é a configuração " +"recomendada para um sistema seguro." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Gravar o Relatório do Teste" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Erro" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Não foi possível aceder ao ficheiro '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Ocorreu um erro durante o arranque do servidor do Akonadi. Os seguintes " +"testes automáticos pretendem ajudar a localizar e a resolver este problema. " +"Ao pedir suporte ou ao relatar erros, inclua sempre este relatório, por " +"favor." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalhes" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Para mais sugestões, consulte por favor a referência userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nova Pasta..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nova" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Apa&gar a Pasta" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Apagar" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sincronizar a Pasta" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronizar" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propriedades da Pasta" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propriedades" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "Co&lar" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Colar" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gerir a&s Inscrições Locais..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gerir as Inscrições Locais" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Adicionar às Pastas de Favoritos" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Adicionar aos Favoritos" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Remover das Pastas de Favoritos" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Remover dos Favoritos" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Mudar o Nome do Favorito..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Mudar o Nome" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copiar a Pasta Para..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copiar Para" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copiar o Item Para..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mover o Item Para..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mover Para" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mover a Pasta Para..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "&Cortar o Item" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Cortar" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Cor&tar a Pasta" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Criar um Recurso" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Apagar o Recurso" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Propriedades do &Recurso" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sincronizar o Recurso" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Funcionar Desligado" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronizar a Pasta Recursivamente" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronizar Recursivamente" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "E&nviar a Pasta para o Lixo" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Enviar a Pasta para o Lixo" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "E&nviar o Item para o Lixo" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Enviar o Item para o Lixo" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Repor a Pasta do Lixo" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Repor a Pasta do Lixo" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Repor o Item do Lixo" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Repor o Item do Lixo" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Repor a Colecção do Lixo" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Repor a Colecção do Lixo" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronizar as Pastas de Favoritos" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronizar as Pastas de Favoritos" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronizar a Árvore de Pastas" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copiar a Pasta" +msgstr[1] "&Copiar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copiar o Item" +msgstr[1] "&Copiar os %1 Itens" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Cortar o Item" +msgstr[1] "&Cortar os %1 Itens" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Cor&tar a Pasta" +msgstr[1] "Cor&tar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Apa&gar o Item" +msgstr[1] "Apa&gar os %1 Itens" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Apa&gar a Pasta" +msgstr[1] "Apa&gar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronizar a Pasta" +msgstr[1] "&Sincronizar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Apa&gar o Recurso" +msgstr[1] "Apa&gar os %1 Recursos" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronizar o Recurso" +msgstr[1] "&Sincronizar os %1 Recursos" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copiar a Pasta" +msgstr[1] "Copiar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copiar o Item" +msgstr[1] "Copiar os %1 Itens" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Cortar o Item" +msgstr[1] "Cortar os %1 Itens" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Cortar a Pasta" +msgstr[1] "Cortar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Apagar o Item" +msgstr[1] "Apagar os %1 Itens" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Apagar a Pasta" +msgstr[1] "Apagar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronizar a Pasta" +msgstr[1] "Sincronizar as %1 Pastas" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Apagar o Recurso" +msgstr[1] "Apagar os %1 Recursos" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronizar o Recurso" +msgstr[1] "Sincronizar os %1 Recursos" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nome" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Deseja mesmo apagar esta pasta e todas as suas sub-pastas?" +msgstr[1] "Deseja mesmo apagar as %1 pastas e todas as suas sub-pastas?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Apagar a pasta?" +msgstr[1] "Apagar as pastas?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Não foi possível apagar a pasta: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "A remoção da pasta falhou" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propriedades da Pasta %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Deseja mesmo apagar o item seleccionado?" +msgstr[1] "Deseja mesmo apagar os %1 itens seleccionados?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Apagar o item?" +msgstr[1] "Apagar os itens?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Não foi possível apagar o item: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "A remoção do item falhou" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Mudar o Nome do Favorito" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nome:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Novo Recurso" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Não foi possível criar o recurso: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "A criação do recurso falhou" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Deseja mesmo apagar este recurso?" +msgstr[1] "Deseja mesmo apagar os %1 recursos?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Apagar o Recurso?" +msgstr[1] "Apagar os Recursos?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Não foi possível colar os dados: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "A colagem falhou" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Não é possível adicionar um \"/\" ao nome da pasta." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Erro de criação da nova pasta" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Não é possível adicionar um \".\" ao início ou ao fim do nome da pasta." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Antes de sincronizar a pasta \"%1\", é necessário ligar-se ao recurso. " +"Deseja ligar-se ao mesmo?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "A conta \"%1\" está desligada" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Funcionar Ligado" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mover Para Esta Pasta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copiar Para Esta Pasta" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Não foi possível actualizar a inscrição: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Erro de Inscrição" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Inscrições Locais" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Procurar:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Apenas os &subscritos" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Su&bscrever" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Cancelar a S&ubscrição" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Não foi possível criar a marca nova" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Ocorreu um erro ao criar uma nova marca" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Deseja mesmo remover a marca %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Apagar a marca" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Apagar a marca" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Seleccionar as marcas que deverão ser aplicadas." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Criar uma nova marca" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gerir as Marcas" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Seleccionar as marcas..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Seleccionar as Marcas" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Limpar" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Carregar para adicionar marcas" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Conversor do Akonadi para XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" +"Converte uma sub-árvore de colecções do Akonadi para um ficheiro em XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Não foram carregados dados." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nenhum ficheiro indicado" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Não foi possível abrir o ficheiro de dados '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "O ficheiro %1 não existe." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Não foi possível processar o ficheiro de dados '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Não foi possível carregar e processar a definição do esquema de dados." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" +"Não foi possível criar o contexto de processamento do esquema de dados." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Não foi possível criar o esquema de dados." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Não foi possível criar o contexto de validação do esquema de dados." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "O formato do ficheiro é inválido." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Não foi possível processar o ficheiro de dados: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Não foi possível encontrar a colecção %1" diff --git a/po/pt_BR/akonadi_knut_resource.po b/po/pt_BR/akonadi_knut_resource.po new file mode 100644 index 0000000..037a418 --- /dev/null +++ b/po/pt_BR/akonadi_knut_resource.po @@ -0,0 +1,85 @@ +# Translation of akonadi_knut_resource.po to Brazilian Portuguese +# Copyright (C) 2009-2016 This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# André Marcelo Alvarenga , 2009, 2010, 2015, 2016. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2016-01-19 14:00-0200\n" +"Last-Translator: André Marcelo Alvarenga \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nenhum arquivo de dados selecionado." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "O arquivo '%1' foi carregado com sucesso." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Selecionar o arquivo de dados" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Arquivo de dados do Knut para o Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Não foi encontrado nenhum item remoto com o ID %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "A coleção-mãe não foi encontrada na árvore DOM." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Não foi possível gravar a coleção." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "A coleção modificada não foi encontrada na árvore DOM." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "A coleção excluída não foi encontrada na árvore DOM." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "A coleção-mãe '%1' não foi encontrada na árvore DOM." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Não foi possível gravar o item." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "O item modificado não foi encontrado na árvore DOM." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "O item excluído não foi encontrado na árvore DOM." diff --git a/po/pt_BR/libakonadi5.po b/po/pt_BR/libakonadi5.po new file mode 100644 index 0000000..9b35b78 --- /dev/null +++ b/po/pt_BR/libakonadi5.po @@ -0,0 +1,2623 @@ +# Translation of libakonadi5.po to Brazilian Portuguese +# Copyright (C) 2008-2019 This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# André Marcelo Alvarenga , 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2018, 2019. +# Andre Paulo Machado , 2008. +# Luiz Fernando Ranghetti , 2010, 2011, 2012, 2019, 2020, 2021. +# Marcus Vinícius de Andrade Gama , 2010, 2011, 2012. +# Aracele Torres , 2010. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi5\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-19 10:13-0300\n" +"Last-Translator: Luiz Fernando Ranghetti \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 20.04.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "André Marcelo Alvarenga" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "alvarenga@kde.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Não existe atualmente nenhuma conta configurada." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "A integração de contas não é suportada" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Não foi possível registrar o objeto no dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 do tipo %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificado do agente" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agente do Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pronto" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Offline" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Sincronizando..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Erro." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Não configurado" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificador do recurso" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Recurso do Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Foi obtido um item inválido" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Ocorreu um erro ao criar o item: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Ocorreu um erro ao atualizar a coleção: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Falha na atualização da coleção local: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Falha na atualização dos itens locais: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Não é possível obter o item no modo offline." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Sincronizando pasta '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Falha ao obter a coleção para sincronizar." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Falha ao obter a coleção para sincronizar os atributos." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "O item solicitado não existe mais" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Tarefa cancelada." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "A coleção não existe." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Foram detectadas coleções órfãs não resolvidas" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Não foi encontrado o outro item para o tratamento do conflito" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Não foi possível acessar a interface D-Bus do agente criado." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "A criação da instância do agente expirou o tempo." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Não foi possível obter o tipo do agente '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Impossível criar a instância do agente." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "A instância da coleção é inválida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Instância do recurso inválida." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Não foi possível obter a interface de D-Bus para o recurso '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "A sincronização dos atributos da coleção expirou o tempo de espera." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Coleção a copiar inválida" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Coleção de destino inválida" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Instância superior inválida" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Não foi possível processar a coleção a partir da resposta" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Coleção inválida" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Foi indicada uma coleção inválida." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nenhum objeto especificado para mover" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Não foi indicado nenhum destino válido" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Coleção inválida." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Coleção-mãe inválida" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Não foi possível conectar-se ao serviço do Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"A versão do protocolo do servidor do Akonadi é incompatível. Certifique-se " +"que a versão instalada é compatível." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "A operação foi cancelada pelo usuário." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Erro desconhecido." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Resposta inesperada" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Ocorreu uma falha ao criar a relação." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "A sincronização do recurso expirou o tempo de espera." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Não é possível obter a coleção raiz do recurso %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Nenhum ID fornecido." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identificador do recurso '%1' é inválido" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Falha ao configurar o recurso padrão via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Falha ao obter a coleção de recursos." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Expirou o tempo-limite para obter o bloqueio." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Ocorreu uma falha ao criar a marca." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"O envio para a coleção da lixeira falhou. Interrompendo a operação de " +"exclusão" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Foram passados itens inválidos" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Foi passada uma coleção inválida" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "A coleção é inválida ou está em branco" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Não foi possível restaurar a coleção e o recurso de restauração não está " +"disponível" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Carregando..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Erro" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"A coleção de destino '%1' já contém\n" +"uma coleção chamada '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Nome" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Não foi possível copiar o item: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Não foi possível copiar a coleção: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Não foi possível mover o item: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Não foi possível mover a coleção: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Não foi possível associar a entidade: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Erro" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Pastas favoritas" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Total de mensagens" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Mensagens não lidas" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Quota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Tamanho do armazenamento" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Tamanho do armazenamento da subpasta" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Não lida" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Tamanho" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Marca" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Não é possível obter o item do índice" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "O índice já não está mais disponível" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "O componente de conteúdo '%1' não está disponível para este índice" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Nenhuma sessão disponível para este índice" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Nenhum item disponível para este índice" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Plugin sem nome" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Nenhuma descrição disponível" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"A versão do protocolo do servidor Akonadi é diferente da versão do protocolo " +"usado por este aplicativo.\n" +"Se você atualizou recentemente o seu sistema, encerre a sessão e entre " +"novamente para garantir que todos os aplicativos usem a versão correta do " +"protocolo." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Não existem agentes do Akonadi disponíveis. Verifique a sua instalação do " +"KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"As versões do protocolo não correspondem. A versão do servidor é mais antiga " +"(%1) que a nossa (%2). Se atualizou seu sistema recentemente, reinicie o " +"servidor do Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"As versões do protocolo não correspondem. A versão do servidor é mais nova " +"(%1) que a nossa (%2). Se atualizou seu sistema recentemente, reinicie todos " +"os aplicativos do KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Autoteste do Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Verifica e informa o estado do servidor Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nova instância do agente..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Excluir a instância do agente" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configurar a instância do agente" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nova instância do agente" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Não foi possível criar a instância do agente: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Falha na criação da instância do agente" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Excluir a instância do agente?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Deseja realmente excluir a instância do agente selecionada?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Configuração de %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Manual do %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Sobre o %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "A caixa de diálogo de configuração foi aberta em outra janela" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "A configuração do %1 já está aberta em outro local." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Falha ao registrar a caixa de diálogo de configuração do %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuto" +msgstr[1] "minutos" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Recepção" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Usar as opções da pasta ou da conta principal" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronizar ao selecionar esta pasta" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronizar automaticamente após:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nunca" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minutos" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Componentes do cache local" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opções de recepção" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Sempre obter as &mensagens completas" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Obte&r o conteúdo das mensagens por solicitação" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Manter o conteúdo das mensagens localmente por:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Sempre" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Pesquisar" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Usar pasta por padrão" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nova subpasta..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Criar uma nova subpasta sob a pasta atualmente selecionada" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nova pasta" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Nome" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Não foi possível criar a pasta: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Falha na criação da pasta" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Geral" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Um objeto" +msgstr[1] "%1 objetos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Nome:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Usar ícone personalizado:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "pasta" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Estatísticas" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Conteúdo:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objetos" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Tamanho:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Lembre-se que a indexação poderá demorar alguns minutos." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Manutenção" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Erro ao obter a quantidade de itens indexados" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexado %1 item nesta pasta" +msgstr[1] "Indexados %1 itens nesta pasta" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Calculando os itens indexados..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Arquivos" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Tipo de pasta:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "desconhecido" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Itens" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Total de itens:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Itens não lidos:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexação" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Ativar a indexação por texto completo" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Obtendo a quantidade de itens indexados..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indexar a pasta novamente" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Nenhuma pasta" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Abrir o diálogo da coleção" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Selecione uma coleção" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mover aqui" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copiar aqui" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Cancelar" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Modificação" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Opções" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atributo: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Resolução de conflito" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Usar a minha versão" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Usar a versão deles" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Manter ambas as versões" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"As suas alterações conflitam com as que foram feitas por outras pessoas." +"
A menos que uma das versões possa ser simplesmente eliminada, você " +"precisa integrar essas alterações manualmente.
Clique em \"Abrir editor de texto\" para manter uma cópia dos " +"textos, depois selecione a versão que está mais correta, volte a abri-lo e " +"então modifique-o novamente para adicionar o que estiver faltando." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dados" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Iniciando o servidor do Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Parando o servidor do Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mover aqui" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copiar aqui" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Criar &link aqui" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "C&ancelar" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Não foi possível conectar-se ao serviço de gerenciamento de informações " +"pessoais.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Iniciando o serviço de gerenciamento de informações pessoais..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Encerrando o serviço de gerenciamento de informações pessoais..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Atualizando o banco de dados do serviço de gerenciamento de informações " +"pessoais." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"O serviço de gerenciamento de informações pessoais está atualizando o banco " +"de dados.\n" +"Isso acontece após a atualização do software e é necessário para otimizar o " +"desempenho.\n" +"Dependendo da quantidade de informações pessoais, isso pode levar alguns " +"minutos." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"O serviço de gerenciamento de informações pessoais do Akonadi não está em " +"execução. Este aplicativo não pode ser usado sem ele." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Iniciar" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"O framework de gerenciamento de informações pessoais do Akonadi não está " +"disponível.\n" +"Clique em \"Detalhes...\" para obter informações detalhadas sobre este " +"problema." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"O serviço de gerenciamento de informações pessoais do Akonadi não está " +"disponível." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalhes..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Deseja remover a conta '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Remover a conta?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Contas de recebimento (adicione ao menos uma):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "A&dicionar..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Modificar..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "R&emover" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reiniciar" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Pasta recente" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Renomear favorito" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Nome:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Teste automático do servidor do Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Salvar relatório..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copiar o relatório para a área de transferência" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"O driver QtSQL '%1' é necessário pela sua configuração atual do servidor do " +"Akonadi e foi encontrado em seu sistema." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"O driver QtSQL '%1' é necessário pela configuração atual do seu servidor " +"Akonadi.\n" +"Os seguintes drivers estão instalados: %2.\n" +"Certifique-se de que o driver necessário esteja instalado." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "O driver do banco de dados foi encontrado." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "O driver do banco de dados não foi encontrado." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "O executável do servidor MySQL não foi testado." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "A configuração atual não requer um servidor MySQL interno." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Você tem o Akonadi atualmente configurado para usar o servidor MySQL '%1'.\n" +"Certifique-se de que tem o servidor MySQL instalado, defina o caminho " +"correto e assegure-se de que possui permissões de leitura e gravação no " +"executável do servidor. O executável do servidor é normalmente chamado de " +"'mysqld', e a localização varia de acordo com a distribuição." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Servidor MySQL não encontrado." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Servidor MySQL não legível." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Servidor MySQL não executável." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "O MySQL foi encontrado com um nome inesperado." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "O servidor MySQL foi encontrado." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Servidor MySQL encontrado: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "O servidor MySQL está executável." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"A execução do servidor MySQL '%1' falhou com a seguinte mensagem de erro: " +"'%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "A execução do servidor MySQL falhou." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "O registro de erros do servidor MySQL não foi testado." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Não foi encontrado nenhum registro de erros atual do MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"O servidor MySQL não relatou qualquer erro durante a inicialização. O log " +"pode ser encontrado em '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "O registro de erros do MySQL não está legível." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Um log de erro do servidor MySQL foi encontrado mas não está legível: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "O log do servidor MySQL contém erros." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "O arquivo de log '%1' do servidor MySQL contém erros." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "O log do servidor MySQL contém avisos." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "O arquivo de log '%1' do servidor MySQL contém avisos." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "O log do servidor MySQL não contém erros." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"O arquivo de log '%1' do servidor MySQL não contém nenhum erro ou aviso." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "A configuração do servidor MySQL não foi testada." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "A configuração padrão do servidor MySQL foi encontrada." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"A configuração padrão para o servidor MySQL foi encontrada e está acessível " +"em %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "A configuração padrão do servidor MySQL não foi encontrada." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"A configuração padrão para o servidor MySQL não foi encontrada ou não está " +"acessível. Verifique se a sua instalação do Akonadi está completa e se você " +"possui todas as permissões de acesso necessárias." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "A configuração personalizada do servidor MySQL não está disponível." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"A configuração personalizada para o servidor MySQL não foi encontrada mas é " +"opcional." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "A configuração personalizada do servidor MySQL foi encontrada." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"A configuração personalizada para o servidor MySQL foi encontrada e está " +"legível em %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "A configuração personalizada do servidor MySQL não legível." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"A configuração personalizada do servidor MySQL foi encontrada em %1, mas não " +"está acessível. Verifique as suas permissões de acesso." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" +"A configuração do servidor MySQL não foi encontrada ou não está legível." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"A configuração do servidor MySQL não foi encontrada ou não está legível." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "A configuração do servidor MySQL está disponível para uso." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "A configuração do servidor MySQL foi encontrada em %1 e está legível." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Não foi possível conectar-se ao servidor PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Servidor PostgreSQL encontrado." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "O servidor PostgreSQL foi encontrado e a conexão está funcionando." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "O akonadictl não foi encontrado" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"O programa 'akonadictl' precisa estar acessível no $PATH. Certifique-se que " +"tem o servidor do Akonadi instalado." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "O akonadictl foi encontrado e está disponível para uso" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"O programa '%1' que controla o servidor do Akonadi foi encontrado e pôde ser " +"executado com sucesso.\n" +"Resultado:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "O akonadictl foi encontrado mas não está disponível para uso" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"O programa '%1' que controla o servidor do Akonadi foi encontrado, mas não " +"pôde ser executado com sucesso.\n" +"Resultado:\n" +"%2\n" +"Certifique-se de que o servidor do Akonadi está instalado corretamente." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "O processo de controle do Akonadi está registrado no D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"O processo de controle do Akonadi está registrado no D-Bus, o que " +"normalmente indica que ele está operacional." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "O processo de controle do Akonadi não está registrado no D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"O processo de controle do Akonadi não está registrado no D-Bus, o que " +"significa que ele não foi iniciado ou encontrou um erro fatal durante a " +"inicialização." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "O processo do servidor do Akonadi está registrado no D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"O processo do servidor do Akonadi está registrado no D-Bus, o que " +"normalmente significa que ele está operacional." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "O processo do servidor do Akonadi não está registrado no D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"O processo do servidor do Akonadi não está registrado no D-Bus, o que " +"normalmente significa que ele não foi iniciado ou encontrou um erro fatal " +"durante a inicialização." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Não é possível verificar a versão do protocolo." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Sem uma conexão com o servidor, não é possível verificar se a versão do " +"protocolo corresponde aos requisitos." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "A versão do protocolo do servidor é muito antiga." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"A versão do protocolo do servidor é a %1, mas é necessária pelo menos a %2 " +"por parte do cliente. Se atualizou o KDE PIM recentemente, certifique-se de " +"que o Akonadi e os aplicativos do KDE PIM foram reiniciados." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "A versão do protocolo do servidor é muito nova." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "A versão do protocolo do servidor corresponde." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "A versão atual do protocolo do servidor é a %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Agentes de recursos encontrados." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Foi encontrado pelo menos um agente de recursos." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Nenhum agente de recursos foi encontrado." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Não foram encontrados agentes de recursos e o Akonadi não pode ser usado sem " +"ter ao menos um. Isto normalmente significa que não estão instalados agentes " +"de recursos ou que ocorreu um problema de configuração. Os seguintes " +"caminhos foram pesquisados: '%1'. A variável de ambiente XDG_DATA_DIRS está " +"definida para '%2'. Certifique-se de que os caminhos onde os agentes do " +"Akonadi estão instalados, encontram-se nesta variável de ambiente." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Não foi encontrado nenhum registro de erros do servidor do Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"O servidor do Akonadi não relatou quaisquer erros durante a inicialização " +"atual." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "O registro de erros atual do servidor do Akonadi foi encontrado." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"O servidor do Akonadi apresentou erros durante a inicialização atual. O log " +"pode ser encontrado em %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Nenhum log anterior do servidor do Akonadi foi encontrado." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"O servidor do Akonadi não relatou quaisquer erros durante a inicialização " +"anterior." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "O log anterior do servidor do Akonadi foi encontrado." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"O servidor do Akonadi apresentou erros durante a inicialização anterior. O " +"log pode ser encontrado em %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Não foi encontrado nenhum registro de erros de controle do Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"O processo de controle do Akonadi não relatou quaisquer erros durante a " +"inicialização atual." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Foi encontrado um registro de erros de controle do Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"O processo de controle do Akonadi apresentou erros durante a inicialização " +"atual. O log pode ser encontrado em %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Nenhum log de erros de controle anterior do Akonadi foi encontrado." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"O processo de controle do Akonadi não relatou quaisquer erros durante a " +"inicialização anterior." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Foi encontrado um log de erros de controle anterior do Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"O processo de controle do Akonadi apresentou erros durante a inicialização " +"anterior. O log pode ser encontrado em %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "O Akonadi foi iniciado como root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"A execução de aplicativos da Internet como root/administrador pode expô-lo a " +"vários riscos de segurança. O MySQL, que é usado por esta instalação do " +"Akonadi, não poderá ser ele próprio executado como administrador, para " +"protegê-lo destes riscos." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "O Akonadi não foi iniciado como root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"O Akonadi não está em execução como usuário administrador (root); esta é a " +"configuração recomendada para um sistema seguro." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Salvar o relatório de teste" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Erro" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Não foi possível abrir o arquivo '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Ocorreu um erro durante o carregamento do servidor do Akonadi. Os seguintes " +"testes automáticos pretendem ajudá-lo a localizar e resolver este problema. " +"Quando solicitar suporte ou relatar erros, inclua sempre este relatório." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalhes" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Para mais dicas, consulte a referência userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nova pasta..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nova" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Excluir pasta" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Excluir" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Sincronizar pasta" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronizar" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Propriedades da pasta" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Propriedades" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "C&olar" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Colar" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Gerenciar inscrições &locais..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gerenciar as inscrições locais" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Adicionar nas pastas favoritas" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Adicionar como favorito" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Remover das pastas favoritas" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Remover dos favoritos" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Renomear favorita..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Renomear" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copiar pasta para..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copiar para" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copiar o item para..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mover o item para..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mover para" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mover pasta para..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Re&cortar item" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Recortar" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Re&cortar pasta" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Criar recurso" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Excluir recurso" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Propriedades do &recurso" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Sincronizar recurso" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Trabalhar desconectado" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronizar pasta recursivamente" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Sincronizar recursivamente" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mover pasta para a lixeira" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mover pasta para a lixeira" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Mover item para a lixeira" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Mover item para a lixeira" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restaurar pasta da lixeira" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restaurar pasta da lixeira" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restaurar item da lixeira" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restaurar item da lixeira" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restaurar coleção da lixeira" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restaurar coleção da lixeira" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronizar as pastas favoritas" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronizar as pastas favoritas" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Sincronizar a árvore de pastas" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copiar pasta" +msgstr[1] "&Copiar %1 pastas" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copiar item" +msgstr[1] "&Copiar %1 itens" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Re&cortar item" +msgstr[1] "Re&cortar %1 itens" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Re&cortar pasta" +msgstr[1] "Re&cortar %1 pastas" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Excluir item" +msgstr[1] "&Excluir %1 itens" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Excluir pasta" +msgstr[1] "&Excluir %1 pastas" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronizar pasta" +msgstr[1] "&Sincronizar %1 pastas" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Excluir o recurso" +msgstr[1] "&Excluir os %1 recursos" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronizar o recurso" +msgstr[1] "&Sincronizar os %1 recursos" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copiar pasta" +msgstr[1] "Copiar %1 pastas" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copiar item" +msgstr[1] "Copiar %1 itens" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Recortar item" +msgstr[1] "Recortar %1 itens" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Recortar pasta" +msgstr[1] "Recortar %1 pastas" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Excluir item" +msgstr[1] "Excluir %1 itens" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Excluir pasta" +msgstr[1] "Excluir %1 pastas" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronizar pasta" +msgstr[1] "Sincronizar %1 pastas" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Excluir recurso" +msgstr[1] "Excluir %1 recursos" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronizar recurso" +msgstr[1] "Sincronizar %1 recursos" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Nome" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Deseja realmente excluir esta pasta e todas as suas subpastas?" +msgstr[1] "Deseja realmente excluir as %1 pastas e todas as suas subpastas?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Excluir pasta?" +msgstr[1] "Excluir pastas?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Não foi possível excluir a pasta: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Falha na exclusão da pasta" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Propriedades da pasta %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Deseja realmente excluir o item selecionado?" +msgstr[1] "Deseja realmente excluir os %1 itens?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Excluir o item?" +msgstr[1] "Excluir os itens?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Não foi possível excluir o item: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Falha na exclusão do item" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Renomear favorito" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Nome:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Novo recurso" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Não foi possível criar o recurso: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Falha na criação do recurso" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Deseja realmente excluir este recurso?" +msgstr[1] "Deseja realmente excluir os %1 recursos?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Excluir o recurso?" +msgstr[1] "Excluir os recursos?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Não foi possível colar os dados: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Falha ao colar" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Não é possível adicionar o caractere \"/\" no nome da pasta." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Erro na criação da nova pasta" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Não é possível adicionar o caractere \".\" no início ou no final do nome da " +"pasta." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Antes de sincronizar a pasta \"%1\" é necessário estar conectado ao recurso. " +"Deseja conectar-se?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "A conta \"%1\" está desconectada" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Conectar" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mover para esta pasta" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copiar para esta pasta" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Ocorreu uma falha ao atualizar a inscrição: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Erro na inscrição" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Inscrições locais" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Pesquisar:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "A&penas as inscritas" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Inscrever" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Cancelar inscrição" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Ocorreu uma falha ao criar uma nova etiqueta" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Ocorreu um erro ao criar uma nova etiqueta" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Deseja realmente remover a etiqueta %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Excluir etiqueta" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Excluir etiqueta" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Selecionar as etiquetas que devem ser aplicadas." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Criar nova etiqueta" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Gerenciar etiquetas" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Selecionar etiquetas..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Selecionar etiquetas" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Limpar" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Clique para adicionar etiquetas" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Conversor de Akonadi para XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Converte uma subárvore de coleção Akonadi para um arquivo XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nenhum dado carregado." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nenhum nome de arquivo indicado" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Não foi possível abrir o arquivo de dados '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "O arquivo %1 não existe." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Não foi possível analisar o arquivo de dados '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "A definição do esquema não pôde ser carregada e analisada." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Não foi possível criar um contexto de análise do esquema." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Não foi possível criar o esquema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Não foi possível criar um contexto de validação do esquema." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Formato de arquivo inválido." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Não foi possível analisar o arquivo de dados: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Não foi possível encontrar a coleção %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "ID remoto" + +#~ msgid "MimeType" +#~ msgstr "Tipo MIME" + +#~ msgid "Default Name" +#~ msgstr "Nome padrão" diff --git a/po/ro/akonadi_knut_resource.po b/po/ro/akonadi_knut_resource.po new file mode 100644 index 0000000..1ea3677 --- /dev/null +++ b/po/ro/akonadi_knut_resource.po @@ -0,0 +1,90 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Sergiu Bivol , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2009-08-04 10:00+0300\n" +"Last-Translator: Sergiu Bivol \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nu este ales niciun fișier cu date." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Fișierul „%1” a fost încărcat cu succes." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Alegeți fișierul cu date" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Fișier Akonadi cu date Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "" + +#~ msgid "Path to the Knut data file." +#~ msgstr "Calea către fișierul cu date Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Nu modifica datele reale ale platformei." diff --git a/po/ro/libakonadi5.po b/po/ro/libakonadi5.po new file mode 100644 index 0000000..d63cea9 --- /dev/null +++ b/po/ro/libakonadi5.po @@ -0,0 +1,2867 @@ +# translation of libakonadi to Romanian +# Copyright (C) 2008 This_file_is_part_of_KDE +# This file is distributed under the same license as the libakonadi package. +# Laurenţiu Buzdugan , 2008". +# Sergiu Bivol , 2008, 2009, 2011, 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2013-01-13 00:43+0200\n" +"Last-Translator: Sergiu Bivol \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Sergiu Bivol" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "sergiu@cip.md" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Ofiectul nu poate fi înregistrat la dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 de tip %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identificator agent" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Agent Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pregătit" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Deconectat" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Se sincronizează..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Eroare." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, fuzzy, kde-format +#| msgctxt "@label commandline option" +#| msgid "Resource identifier" +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identificator de resursă" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Resursă Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "S-a preluat un element nevalid" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Eroare la crearea elementului: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Eroare la actualizarea colecției: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Actualizarea colecției locale a eșuat: %1." + +#: agentbase/resourcebase.cpp:718 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Updating local collection failed: %1." +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Actualizarea colecției locale a eșuat: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Elementul nu poate fi obținut în regim deconectat." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Se sincronizează dosarul „%1”" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for sync." +msgstr "Preluarea colecției de resurse a eșuat." + +#: agentbase/resourcebase.cpp:983 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to retrieve collection for attribute sync." +msgstr "Preluarea colecției de resurse a eșuat." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Elementul cerut nu mai există" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Lucrare anulată." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Nicio colecție de acest fel." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Au fost găsite colecții orfane nerezolvate" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Nu s-a găsit celălalt element pentru manipularea conflictului" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Nu se poate accesa interfața D-Bus a agentului creat." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Crearea instanței de agent a expirat." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Nu se poate obține tipul de agent „%1”." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Nu se poate crea instanța de agent." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Instanță de colecție nevalidă." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Instanță de resursă nevalidă." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Nu se poate obține interfața D-Bus pentru resursa „%1”" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Sincronizarea atributelor colecției a expirat." + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "Colecție nevalidă" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "Colecție nevalidă" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Părinte nevalid" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to fetch the resource collection." +msgid "Failed to parse Collection from response" +msgstr "Preluarea colecției de resurse a eșuat." + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Colecție nevalidă" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Colecție nevalidă furnizată." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Nu au fost specificate obiecte pentru mutare" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nu au fost specificate destinații valide" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Colecție nevalidă." + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "Colecție nevalidă" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Nu se poate conecta la serviciul Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Versiunea de protocol a serverului Akonadi este incompatibilă. Asigurați-vă " +"că aveți o versiune compatibilă instalată." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Utilizatorul a anulat operația." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Eroare necunoscută." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create relation." +msgstr "Nu se poate crea instanța de agent." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Sincronizarea resursei a expirat." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Nu s-a putut prelua colecția-rădăcină a resursei %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Nu a fost furnizat un identificator de resursă." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Identificator de resursă nevalid „%1”" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Configurarea resursei implicite prin D-Bus a eșuat." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Preluarea colecției de resurse a eșuat." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Obținerea blocajului a expirat." + +#: core/jobs/tagcreatejob.cpp:49 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create tag." +msgstr "Nu se poate crea instanța de agent." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Mutarea în colecția gunoiului a eșuat, se abandonează operația de mutare la " +"gunoi" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Au fost transmise argumente nevalide" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "A fost transmisă o colecție nevalidă" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Nicio colecție validă sau listă de elemente goală" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Colecția de restabilire nu a putut fi găsită și resursa de restabilire nu " +"este disponibilă" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Denumire" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Se încarcă..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "Eroare." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Colecția-țintă „%1” conține deja\n" +"o colecție cu denumirea „%2”." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Denumire" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Elementul nu poate fi copiat:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Colecția nu poate fi copiată:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Elementul nu poate fi mutat:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Colecția nu poate fi mutată:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Entitatea nu poate fi legată:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "Eroare." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Dosare favorite" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Total mesaje" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Mesaje necitite" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Cotă" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Dimensiune stocare" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Dimensiune stocare subdosar" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Necitite" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Total" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Dimensiune" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Nu se poate obține un element pentru index" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Indexul nu mai este disponibil" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Partea „%1” a sarcinii nu e disponibilă pentru acest index" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Nu este nicio sesiune disponibilă pentru acest index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Nu este niciun element disponibil pentru acest index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Extensie fără denumire" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Nicio descriere disponibilă" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgid "Akonadi Self Test" +msgstr "Testare a serverului Akonadi" + +#: selftest/main.cpp:21 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgid "Checks and reports state of Akonadi server" +msgstr "Nu se poate conecta la serviciul Akonadi." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Instanță de agent &nouă..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Ș&terge instanța agentului" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Configurează instanța agentului" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Instanță de agent nouă" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Instanța agentului nu poate fi creată: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Crearea instanței de agent a eșuat" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Ștergeți instanța agentului?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Sigur doriți să ștergeți instanța agentului aleasă?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to register %1 configuration dialog." +msgstr "Nu se poate crea instanța de agent." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minut" +msgstr[1] "minute" +msgstr[2] "de minute" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Preluare" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Folosește opțiunile de la dosarul sau contul părinte" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Sincronizează la alegerea acestui dosar" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Sincronizează automat după:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Niciodată" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minute" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Componente depozitate local" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Opțiuni de preluare" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "Preia mesaje întregi întotdeauna" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "Preia corpurile mesajelor la cerere" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Păstrează local corpurile mesajelor pentru:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Totdeauna" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "" +#| "@info/plain Displayed grayed-out inside the textbox, verb to search" +#| msgid "Search" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Caută" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Subdosar &nou..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Creează un subdosar nou sub dosarul ales acum" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Dosar nou" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Denumire" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Dosarul nu poate fi creat: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Crearea dosarului a eșuat" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "General" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Un obiect" +msgstr[1] "%1 obiecte" +msgstr[2] "%1 de obiecte" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "De&numire:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Folosește &pictogramă personalizată:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "dosar" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistici" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Conținut:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 obiecte" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Dimensiune:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Octeți" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "Eroare la crearea elementului: %1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "&Proprietăți dosar" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "Taie elementul" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "Total mesaje" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "Mesaje necitite" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "Dosar recent" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Niciun dosar" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Deschide dialogul colecției" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Alege o colecție" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Mută aici" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Copiază aici" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Renunță" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Ora modificării" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Fanioane" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Rezolvare conflict" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "Alege cea din dreapta" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Păstrează ambele" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Date" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Se pornește serverul Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Se oprește serverul Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Mută aici" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Copiază aici" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Leagă aici" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "R&enunță" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "Nu se poate conecta la serviciul Akonadi." + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Serviciul de gestiune a informațiilor personale se pornește..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Serviciul de gestiune a informațiilor personale se oprește..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Serviciul de gestiune a informațiilor personale efectuează o înnoire a bazei " +"de date." + +#: widgets/erroroverlay.cpp:243 +#, fuzzy, kde-format +#| msgid "" +#| "Personal information management service is performing a database upgrade. " +#| "This happens after a software update and is necessary to optimize " +#| "performance. Depending on the amount of personal information, this might " +#| "take a few minutes." +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Serviciul de gestiune a informațiilor personale efectuează o înnoire a bazei " +"de date. Acest lucru se întâmplă după o actualizare a aplicațiilor și e " +"necesar pentru a optimiza performanța. În dependență de volumul de " +"informație personală, aceasta poate dura câteva minute." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Serviciul de gestiune a informațiilor personale Akonadi nu rulează. Această " +"aplicație nu poate fi folosită fără el." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Pornește" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Platforma de gestiune a informațiilor personale Akonadi nu este " +"operațională.\n" +"Apăsați pe „Detalii...” pentru a obține informații detaliate despre problemă." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Serviciul de gestiune a informațiilor personale Akonadi nu este operațional." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detalii..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "Pornește" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Dosar recent" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Redenumește favoritul" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Denumire:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Testare a serverului Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Salvare raport..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Copiază raportul în clipboard" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Driverul QtSQL „%1” este cerut de către configurația actuală a serverului " +"Akonadi și a fost găsit în sistemul dumneavoastră." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Driverul QtSQL „%1” este cerut de către configurația actuală a serverului " +"Akonadi.\n" +"Următoarele drivere sunt instalate: %2.\n" +"Asigurați-vă că driverul necesar este instalat." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Driverul pentru baza de date a fost găsit." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Driverul pentru baza de date nu a fost găsit." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Executabilul serverului MySQl nu este testat." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Configurația actuală nu necesită un server MySQL intern." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Acum aveți serverul Akonadi configurat să folosească serverul MySQL „%1”.\n" +"Asigurați-vă că aveți serverul MySQL instalat, stabiliți calea corectă și " +"asigurați-vă că aveți drepturile necesare de citire și execuție asupra " +"executabilului serverului. Executabilul serverului se numește de obicei " +"„mysqld”; amplasarea acestuia variază în dependență de distribuție." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Serverul MySQL nu a fost găsit." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Serverul MySQL nu este citibil." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Serverul MySQL nu este executabil." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "S-a găsit MySQL cu denumire neașteptată." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Serverul MySQL a fost găsit." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Server MySQL găsit: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Serverul MySQL este executabil." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Executarea serverului MySQL „%1” a eșuat cu următorul mesaj de eroare: „%2”" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Executarea serverului MySQl a eșuat." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Jurnalul de erori al serverului MySQL netestat." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Nu s-a găsit un jurnal de erori MySQL actual." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Serverul MySQL nu a raportat erori în timpul pornirii. Jurnalul poate fi " +"găsit în „%1”." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Jurnalul de erori MySQl nu este citibil." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"A fost găsit un fișier cu jurnalul de erori al serverului MySQl, dar nu este " +"citibil: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Jurnalul serverului MySQL conține erori." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Fișierul cu jurnalul de erori al serverului MySQl „%1” conține erori." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Jurnalul serverului MySQL conține avertizări." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Fișierul cu jurnalul serverului MySQl „%1” conține avertizări." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Jurnalul serverului MySQL nu conține erori." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Fișierul cu jurnalul serverului MySQl „%1” nu conține erori sau avertizări." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Configuraţia serverului MySQL nu este testată." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Configurația implicită a serverului MySQL a fost găsită." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Configurația implicită a serverului MySQL a fost găsită și este lizibilă la " +"%1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Configurația implicită a serverului MySQL nu a fost găsită." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Configurația implicită a serverului MySQL nu a fost găsită sau nu poate fi " +"citită. Verificați dacă instalarea Akonadi este completă și dacă aveți toate " +"drepturile de acces necesare." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Configurația personalizată a serverului MySQL nu este disponibilă." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Configurația personalizată a serverului MySQL nu a fost găsită dar e " +"opțională." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Configurația personalizată a serverului MySQL a fost găsită." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Configurația personalizată a serverului MySQL a fost găsită și este lizibilă " +"la %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Configurația personalizată a serverului MySQL nu poate fi citită." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Configurația personalizată a serverului MySQL a fost găsită la %1 dar nu " +"poate fi citită. Verificați-vă drepturile de acces." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Configurația serverului MySQL nu a fost găsită sau nu poate fi citită." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Configurația serverului MySQL nu a fost găsită sau nu poate fi citită." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Configurația serverului MySQL este utilizabilă." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"Configurația serverului MySQL a fost găsită la %1 și este poate fi citită." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Imposibil de conectat la serverul PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Serverul PostgreSQL a fost găsit." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Serverul PostgreSQL a fost găsit și conexiunea funcționează." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl nu a fost găsit" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Programul „akonadictl” trebuie să fie accesibil în $PATH. Asigurați-vă că " +"aveți serverul Akondai instalat." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl găsit și utilizabil" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Programul „%1” pentru controlul serverului Akonadi a fost găsit și a putut " +"fi executat cu succes.\n" +"Rezultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl găsit dar inutilizabil" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Programul „%1” pentru controlul serverului Akonadi a fost găsit dar nu a " +"putut fi executat cu succes.\n" +"Rezultat:\n" +"%2\n" +"Asigurați-vă că serverul Akonadi este instalat corect." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Procesul de control Akonadi este înregistrat la D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Procesul de control Akonadi este înregistrat la D-Bus, ceea ce indică de " +"obicei că acesta e operațional." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Procesul de control Akonadi nu este înregistrat la D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Procesul de control Akonadi nu este înregistrat la D-Bus, ceea ce de obicei " +"înseamnă că acesta nu a fost pornit sau a întâmpinat o eroare fatală la " +"pornire." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Procesul serverului Akonadi este înregistrat la D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Procesul serverului Akonadi este înregistrat la D-Bus, ceea ce indică de " +"obicei că acesta e operațional." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Procesul serverului Akonadi nu este înregistrat la D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Procesul serverului Akonadi nu este înregistrat la D-Bus, ceea ce de obicei " +"înseamnă că acesta nu a fost pornit sau a întâmpinat o eroare fatală la " +"pornire." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Verificarea versiunii protocolului nu este posibilă." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Fără o conexiune la server nu e posibil să se verifice dacă versiunea " +"protocolului întrunește cerințele." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Versiunea de protocol a serverului este prea veche." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Versiunea de protocol a serverului este %1, dar este necesară măcar " +"versiunea %2. Instalați o versiune mai nouă a serverului Akonadi." + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "Versiunea de protocol a serverului este prea veche." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "Versiunea de protocol a serverului este prea veche." + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "Versiunea de protocol a serverului este prea veche." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Au fost găsiți agenți de resurse." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "A fost găsit cel puțin un agent de resurse." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Nu au fost găsiți agenți de resurse." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nu au fost găsiți agenți de resurse, Akonadi nu e utilizabil fără cel puțin " +"unul. Aceasta înseamnă de obicei că nu-i instalat niciun agent de resurse " +"sau că este o problemă e configurație. Următoarele căi au fost căutate: „%1” " +"Variabila de mediu XDG_DATA_DIRS este stabilită la „%2”; asigurațivă că " +"aceasta include toate căile în care sunt instalați agenți Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Nu s-a găsit niciun jurnal de erori Akonadi actual." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Serverul Akonadi nu a raportat nicio eroare în timpul pornirii sale actuale." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "S-a găsit un jurnal de erori actual al serverului Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Serverul Akonadi a raportat erori în timpul pornirii sale actuale. Jurnalul " +"poate fi găsit în %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Nu s-a găsit niciun jurnal de erori precedent al serverului Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Serverul Akonadi nu a raportat nicio eroare în timpul pornirii sale " +"precedente." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "S-a găsit un jurnal de erori precedent al serverului Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Serverul Akonadi a raportat erori în timpul pornirii sale precedente. " +"Jurnalul poate fi găsit în %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Nu s-a găsit niciun jurnal de erori actual al controlului Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Procesul de control Akonadi nu a raportat nicio eroare în timpul pornirii " +"sale actuale." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "S-a găsit un jurnal de erori actual al controlului Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Procesul de control Akonadi a raportat erori în timpul pornirii sale " +"actuale. Jurnalul poate fi găsit în %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Nu s-a găsit niciun jurnal de erori precedent al controlului Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Procesul de control Akonadi nu a raportat nicio eroare în timpul pornirii " +"sale precedente." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "S-a găsit un jurnal de erori precedent al controlului Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Procesul de control Akonadi a raportat erori în timpul pornirii sale " +"precedente. Jurnalul poate fi găsit în %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi a fost pornit ca root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Rularea aplicațiilor accesibile din Internet ca root/administrator vă expune " +"la multe riscuri de securitate. MySQL, folosit de această instalare Akonadi, " +"nu va permite să fie rulat ca root, pentru a vă proteja de aceste riscuri." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi nu rulează ca root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi nu rulează ca root/administrator, configurație recomandată pentru un " +"sistem sigur." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Salvează raportul testului" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "Eroare." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Fișierul „%1” nu poate fi deschis" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"A intervenit o eroare în timpul pornirii serverului Akonadi. Următoarele " +"teste ar trebui să ajute la depistarea și rezolvarea acestei probleme. Când " +"cereți asistență sau raportați erori, includeți tot timpul acest raport." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detalii" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Pentru sfaturi de depanare suplimentare accesați userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Dosar &nou..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nou" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "Ș&terge dosarul" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Șterge" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Sincronizează dosarul" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Sincronizează" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Proprietăți dosar" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Proprietăți" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "Li&pește" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Lipește" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Ge&stiune abonări locale..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Gestionează abonările locale" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Adaugă la dosare favorite" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Adaugă la favorite" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Elimină din dosare favorile" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Elimină din favorite" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Redenumire favorit..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Redenumește" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Copiere dosar la..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Copiază la" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Copiere element la..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Mutare element la..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Mută la" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Mutare dosar la..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Taie elementul" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Taie" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "&Taie dosarul" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Creează resursă" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Șterge resursa" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Proprietăți &resursă" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Sincronizează resursa" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Lucrează deconectat" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Sincronizează dosarul recursiv" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "&Sincronizează recursiv" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Mută dosarul la gunoi" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Mută dosarul la gunoi" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Mută elementul la gunoi" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Mută elementul la gunoi" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Restabilește dosarul din gunoi" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Restabilește dosarul din gunoi" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Restabilește elementul din gunoi" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Restabilește elementul din gunoi" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Restabilește colecția din gunoi" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Restabilește colecția din gunoi" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Sincronizează dosarele favorite" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Sincronizează dosarele favorite" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize Folder" +#| msgid_plural "Synchronize %1 Folders" +msgid "Synchronize Folder Tree" +msgstr "Sincronizează dosarul" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Copiază dosarul" +msgstr[1] "&Copiază %1 dosare" +msgstr[2] "&Copiază %1 de dosare" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Copiază elementul" +msgstr[1] "&Copiază %1 elemente" +msgstr[2] "&Copiază %1 de elemente" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Taie elementul" +msgstr[1] "&Taie %1 elemente" +msgstr[2] "&Taie %1 de elemente" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Taie dosarul" +msgstr[1] "&Taie %1 dosare" +msgstr[2] "&Taie %1 de dosare" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Ș&terge elementul" +msgstr[1] "Ș&terge %1 elemente" +msgstr[2] "Ș&terge %1 de elemente" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Ș&terge dosarul" +msgstr[1] "Ș&terge %1 dosare" +msgstr[2] "Ș&terge %1 de dosare" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Sincronizează dosarul" +msgstr[1] "&Sincronizează %1 dosare" +msgstr[2] "&Sincronizează %1 de dosare" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Ș&terge resursa" +msgstr[1] "Ș&terge %1 resurse" +msgstr[2] "Ș&terge %1 de resurse" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Sincronizează resursa" +msgstr[1] "&Sincronizează %1 resurse" +msgstr[2] "&Sincronizează %1 de resurse" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Copiază dosarul" +msgstr[1] "Copiază %1 dosare" +msgstr[2] "Copiază %1 de dosare" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Copiază elementul" +msgstr[1] "Copiază %1 elemente" +msgstr[2] "Copiază %1 de elemente" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Taie elementul" +msgstr[1] "Taie %1 elemente" +msgstr[2] "Taie %1 de elemente" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Taie dosarul" +msgstr[1] "Taie %1 dosare" +msgstr[2] "Taie %1 de dosare" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Șterge elementul" +msgstr[1] "Șterge %1 elemente" +msgstr[2] "Șterge %1 de elemente" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Șterge dosarul" +msgstr[1] "Șterge %1 dosare" +msgstr[2] "Șterge %1 de dosare" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Sincronizează dosarul" +msgstr[1] "Sincronizează %1 dosare" +msgstr[2] "Sincronizează %1 de dosare" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Șterge resursa" +msgstr[1] "Șterge %1 resurse" +msgstr[2] "Șterge %1 de resurse" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Sincronizează resursa" +msgstr[1] "Sincronizează %1 resurse" +msgstr[2] "Sincronizează %1 de resurse" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Denumire" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Sigur doriți să ștergeți acest dosar și toate subdosarele acestuia?" +msgstr[1] "Sigur doriți să ștergeți %1 dosare și toate subdosarele acestora?" +msgstr[2] "" +"Sigur doriți să ștergeți %1 de dosare și toate subdosarele acestora?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Se șterge dosarul?" +msgstr[1] "Se șterg dosarele?" +msgstr[2] "Se șterg dosarele?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Dosarul nu poate fi șters: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Ștergerea dosarului a eșuat" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Proprietățile dosarului %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Sigur doriți să ștergeți elementul ales?" +msgstr[1] "Sigur doriți să ștergeți %1 elemente?" +msgstr[2] "Sigur doriți să ștergeți %1 de elemente?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Se șterge elementul?" +msgstr[1] "Se șterg elementele?" +msgstr[2] "Se șterg elementele?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Elementul nu poate fi șters: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Ștergerea elementului a eșuat" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Redenumește favoritul" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Denumire:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Resursă nouă" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Resursa nu a putut fi creată: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Crearea resursei a eșuat" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Sigur doriți să ștergeți această resursă?" +msgstr[1] "Sigur doriți să ștergeți aceste %1 resurse?" +msgstr[2] "Sigur doriți să ștergeți aceste %1 de resurse?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Se șterge resursa?" +msgstr[1] "Se șterg resursele?" +msgstr[2] "Se șterg resursele?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Datele nu pot fi lipite: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Lipirea a eșuat" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Nu se poate adăuga „/” în denumirea dosarului." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Eroare la crearea noului dosar" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Nu se poate adăuga „.” la începutul sau sfârșitul denumirii dosarului." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Înainte de a sincroniza dosarul „%1” trebuie să conectați resursa. Doriți s-" +"o conectați?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Contul „%1” este deconectat" + +#: widgets/standardactionmanager.cpp:997 +#, fuzzy, kde-format +#| msgid "Work Offline" +msgctxt "@action:button" +msgid "Go Online" +msgstr "Lucrează deconectat" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Mută în acest dosar" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Copiază în acest dosar" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to update subscription: %1" +msgstr "Nu se poate crea instanța de agent." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Abonări locale" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Abonări locale" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Caută:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Numai abonate" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Abonează" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Dezabonează" + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Failed to create a new tag" +msgstr "Nu se poate crea instanța de agent." + +#: widgets/tageditwidget.cpp:116 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "An error occurred while creating a new tag" +msgstr "Eroare la crearea elementului: %1" + +#: widgets/tageditwidget.cpp:164 +#, fuzzy, kde-kuit-format +#| msgid "Do you really want to delete this resource?" +#| msgid_plural "Do you really want to delete %1 resources?" +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Sigur doriți să ștergeți această resursă?" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "Șterge elementul" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "Șterge elementul" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Create new tag" +msgstr "Nu se poate crea instanța de agent." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Șterge elementul" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, fuzzy, kde-format +#| msgid "No valid destination specified" +msgid "No filename specified" +msgstr "Nu au fost specificate destinații valide" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to open data file '%1'." +msgstr "Nu se poate obține tipul de agent „%1”." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, fuzzy, kde-format +#| msgid "Unable to obtain agent type '%1'." +msgid "Unable to parse data file '%1'." +msgstr "Nu se poate obține tipul de agent „%1”." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema parser context." +msgstr "Nu se poate crea instanța de agent." + +#: xml/xmldocument.cpp:161 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema." +msgstr "Nu se poate crea instanța de agent." + +#: xml/xmldocument.cpp:166 +#, fuzzy, kde-format +#| msgid "Unable to create agent instance." +msgid "Unable to create schema validation context." +msgstr "Nu se poate crea instanța de agent." + +#: xml/xmldocument.cpp:171 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Invalid item retrieved" +msgid "Invalid file format." +msgstr "S-a preluat un element nevalid" + +#: xml/xmldocument.cpp:179 +#, fuzzy, kde-format +#| msgid "Could not paste data: %1" +msgid "Unable to parse data file: %1" +msgstr "Datele nu pot fi lipite: %1" + +#: xml/xmldocument.cpp:304 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Unable to find collection %1" +msgstr "Colecție nevalidă" + +#~ msgid "Id" +#~ msgstr "Identificator" + +#~ msgid "Remote Id" +#~ msgstr "Identificator distant" + +#~ msgid "MimeType" +#~ msgstr "Tip MIME" + +#~ msgid "Default Name" +#~ msgstr "Denumire implicită" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Șterge" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Renunță" + +#~ msgid "Take left one" +#~ msgstr "Alege cea din stânga" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Două actualizări se află în conflict.Alegeți care actualizare să " +#~ "fie aplicată." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Necitite" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Total" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Dimensiune" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Resursă Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Denumire" + +#~ msgid "Invalid collection specified" +#~ msgstr "Colecție specificată nevalidă" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Versiunea de protocol %1 a fost găsită, se aștepta cel puțin %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Versiunea de protocol a serverului este destul de recentă." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Versiunea protocolului este %1, care este la fel sau mai nouă decât " +#~ "versiunea necesară %2." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "A fost detectat un arbore inconsistent de colecție locală." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "A fost furnizată o colecție distantă fără lanț de strămoși terminat în " +#~ "rădăcină, resursa este deteriorată." + +#~ msgid "KDE Test Program" +#~ msgstr "Program de testare KDE" + +#~ msgid "Cannot list root collection." +#~ msgstr "Nu se poate enumera colecția rădăcină." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Serviciul de căutare Nepomuk este înregistrat la D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Serviciul de căutare Nepomuk este înregistrat la D-Bus, ceea ce indică de " +#~ "obicei că acesta e operațional." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Serviciul de căutare Nepomuk nu este înregistrat la D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Serviciul de căutare Nepomuk nu este înregistrat la D-Bus, ceea ce de " +#~ "obicei înseamnă că acesta nu a fost pornit sau a întâmpinat o eroare " +#~ "fatală la pornire." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "" +#~ "Serviciul de căutare Nepomuk folosește o platformă necorespunzătoare." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Serviciul de căutare Nepomuk folosește platforma „%1” care nu este " +#~ "recomandată pentru utilizarea cu Akonadi." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "" +#~ "Serviciul de căutare Nepomuk folosește o platformă corespunzătoare. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Serviciul de căutare Nepomuk folosește una dintre platformele recomandate." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Extensia „%1” nu este încorporată static. Să specificați această " +#~ "informație în raportul de eroare." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Extensie neîncorporată static" + +#, fuzzy +#~ msgid "Fetch Job Error" +#~ msgstr "Eroare Producere Lucrare " diff --git a/po/ru/akonadi_knut_resource.po b/po/ru/akonadi_knut_resource.po new file mode 100644 index 0000000..b562e7c --- /dev/null +++ b/po/ru/akonadi_knut_resource.po @@ -0,0 +1,96 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Николай Ерёмин , 2009. +# Andrey Cherepanov , 2009. +# Julia Dronova , 2012. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2012-10-11 14:11+0400\n" +"Last-Translator: Julia Dronova \n" +"Language-Team: Русский \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Не выбраны файлы данных." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Файл «%1» успешно загружен." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Выбор файла данных" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Файл данных Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Не найден объект для удалённого идентификатора (remote id) %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Родительская коллекция в дереве DOM не найдена." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Невозможно записать коллекцию." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Изменённая коллекция в дереве DOM не найдена." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Удалённая коллекция в дереве DOM не найдена." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Родительская коллекция «%1» в дереве DOM не найдена." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Невозможно записать объект." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Изменённый объект в дереве DOM не найден." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Удалённый объект в дереве DOM не найден." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Путь к файлу данных Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Не изменять существующие данные." diff --git a/po/ru/libakonadi5.po b/po/ru/libakonadi5.po new file mode 100644 index 0000000..6dc5e17 --- /dev/null +++ b/po/ru/libakonadi5.po @@ -0,0 +1,2888 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Nick Shaforostoff , 2008. +# Andrey Cherepanov , 2009. +# Alexander Potashev , 2010, 2011, 2014, 2015, 2016, 2017. +# Alexander Lakhin , 2013. +# Alexander Yavorsky , 2017, 2018, 2019, 2020. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2020-05-10 11:39+0300\n" +"Last-Translator: Alexander Yavorsky \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Lokalize 20.04.0\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" +"Николай Шафоростов,Андрей Черепанов,Александр Поташев,Александр Яворский," +"Олеся Герасименко" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" +"shaforostoff@kde.ru,skull@kde.ru,aspotashev@gmail.com,kekcuha@gmail.com," +"gammaray@basealt.ru" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Отсутствуют настроенные учётные записи." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Встраивание учётных записей не поддерживается" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Зарегистрировать объект в dbus не удалось: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 типа %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Идентификатор агента" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Агент Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Готов к работе" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Автономный режим" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Выполняется синхронизация..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Ошибка." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Не настроено" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Идентификатор источника данных." + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Источник данных Akonadi." + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Получен неправильный элемент" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Ошибка при создании элемента: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Ошибка при обновлении коллекции: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Не удалось обновить локальную коллекцию: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Не удалось обновить локальные элементы: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Невозможно получить данные в автономном режиме." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Синхронизация папки «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Не удалось получить коллекцию для синхронизации." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Не удалось получить коллекцию для синхронизации атрибутов." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Запрашиваемый объект больше не существует" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Операция отменена." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Коллекция не найдена." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Найдены нераспознанные брошенные коллекции" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Не удалось найти второй объект для разрешения конфликта" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Не удалось получить доступ к созданному агенту через интерфейс D-Bus." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Ошибка создания экземпляра агента." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Не удалось получить тип агента «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Не удалось создать экземпляр агента." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Недопустимый экземпляр коллекции." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Недопустимый экземпляр источника данных." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" +"Не удалось получить доступ к источнику данных «%1» через интерфейс D-Bus." + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Истекло время синхронизации атрибутов коллекции." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Недопустимая коллекция для копирования" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Недопустимая конечная коллекция при копировании" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Недопустимая родительская коллекция" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Не удалось прочитать поле коллекции из ответа" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Недопустимая коллекция" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Указана недопустимая коллекция." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Не указаны объекты для перемещения." + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Не указан допустимый путь назначения" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Недопустимая коллекция" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Недопустимая родительская коллекция" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Невозможно подключиться к серверу Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Версия протокола сервера Akonadi не поддерживается. Проверьте версии " +"установленного программного обеспечения." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Пользователь прервал операцию." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Неизвестная ошибка." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Неожиданный ответ" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Не удалось создать связь." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Истекло время синхронизации источника данных." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Не удалось получить корневую коллекцию источника данных %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Не указан идентификатор источника данных." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Недопустимый идентификатор источника «%1»" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Не удалось настроить источник данных по умолчанию при помощи D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Не удалось получить коллекцию из источника данных." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Истекло время попытки блокировки службы." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Не удалось создать метку." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Перенести коллекцию в корзину не удалось, операция прерывается" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Переданы недопустимые элементы" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Передана недопустимая коллекция" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Неверная коллекция или пустой список" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Не удалось найти коллекцию и ресурс для восстановления" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Название" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Загрузка..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Ошибка" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Коллекция назначения «%1» уже содержит\n" +"коллекцию с названием «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Название" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Не удалось скопировать объект: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Не удалось скопировать коллекцию: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Не удалось переместить объект: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Не удалось переместить коллекцию: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" +"Не удалось создать ссылку на объект или коллекцию: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Ошибка" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Избранные папки" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Всего сообщений" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Непрочитанных сообщений" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Ограничение" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Размер на диске" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Размер подпапки на диске" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Непрочитанных" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Всего" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Размер" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Метка" + +# BUGME: what is "index" here? --aspotashev +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Не удалось выбрать элемент для индекса" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Индекс более не доступен" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Часть данных «%1» недоступна для выбранного индекса" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "С данным индексом не связана сессия" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Нет элемента для данного индекса" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Неизвестный модуль" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Описание недоступно" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Версия протокола сервера Akonadi отличается от версии протокола, которая " +"используется этим приложением.\n" +"Если программное обеспечение было обновлено, завершите сеанс и снова войдите " +"в систему, чтобы удостовериться, что все приложения используют " +"соответствующую версию протокола." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "Отсутствуют доступные агенты Akonadi. Проверьте установку KDE PIM. " + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Версии протокола не совпадают. Версия на стороне сервера (%1) старее, чем на " +"стороне клиента (%2). Если вы обновляли программное обеспечение, " +"перезапустите сервер Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Версии протокола не совпадают. Версия на стороне сервера (%1) новее, чем на " +"стороне клиента (%2). Если вы обновляли программное обеспечение, " +"перезапустите все приложения KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Внутренние тесты Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Проверка состояния сервера Akonadi." + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© Volker Krause , 2008" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Создать экземпляр агента..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Удалить экземпляр агента" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Настроить экземпляр агента..." + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Создание экземпляра агента" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Не удалось создать экземпляр агента: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Ошибка создания экземпляра агента" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Удаление экземпляра агента" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Удалить выбранный экземпляр агента?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Настройка %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Руководство пользователя %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "О программе %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Диалог настройки открыт в другом окне" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Окно настройки %1 уже открыто." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Не удалось зарегистрировать диалог настройки %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "минута" +msgstr[1] "минуты" +msgstr[2] "минут" +msgstr[3] "минута" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Синхронизация" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Использовать параметры родительской папки или учётной записи" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Синхронизировать при выборе этой папки" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Периодичность автоматической синхронизации:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Никогда" + +# [cachepolicypage-45.ui:132], [cachepolicypage.ui:144]: хранить ... в течение ... минут. --aspotashev +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "минут" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Части в локальном кэше" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Параметры синхронизации" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Всегда загружать письма &полностью" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Загружать содержимое писем по &необходимости" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Хранить тела писем на компьютере:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Бессрочно" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Поиск" + +# BUGME: please clarify the original string --aspotashev +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Использовать папку по умолчанию" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Создать подпапку..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Создать вложенную папку" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Новая папка" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Имя" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Невозможно создать папку: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Ошибка создания папки" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Главное" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 объект" +msgstr[1] "%1 объекта" +msgstr[2] "%1 объектов" +msgstr[3] "%1 объект" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Название:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Значок:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "папка" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Статистика" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Содержимое:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 объектов" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Размер:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 байт" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Индексирование может занять несколько минут." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Обслуживание" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Ошибка получения числа проиндексированных объектов" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "В этой папке проиндексирован %1 объект." +msgstr[1] "В этой папке проиндексированы %1 объекта." +msgstr[2] "В этой папке проиндексированы %1 объектов." +msgstr[3] "В этой папке проиндексирован %1 объект." + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Подсчёт проиндексированных объектов..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Файлы" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Тип папки:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "неизвестно" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Объекты" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Всего объектов:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Непрочитанных объектов:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Индексирование" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Использовать полнотекстовый поиск" + +# BUGME: please remove space before ellipsis --aspotashev +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Получение числа проиндексированных объектов..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Проиндексировать заново" + +# BUGME: unmatched terms used: "Select a _collection_" and "No _Folder_" +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Папка не выбрана" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Открыть диалоговое окно выбора коллекции" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Выбор коллекции" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Переместить сюда" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Копировать сюда" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Отмена" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Время изменения" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Флаги" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Атрибут: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Разрешение конфликта" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Использовать мою версию" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Использовать их версию" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Оставить обе версии" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Внесённые изменения конфликтуют с изменениями, сделанными другим " +"пользователем.
Если нельзя оставить только одну версию, придётся " +"применить эти изменения вручную.
Нажмите «Открыть текстовый редактор», чтобы сохранить копию текстов, затем " +"выберите наиболее правильную версию, снова откройте её и внесите необходимые " +"изменения, добавив недостающие фрагменты текста." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Данные" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Запуск сервера Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Остановка сервера Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Переместить сюда" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Копировать сюда" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Создать ссылку" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "О&тмена" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Не удалось подключиться к службе персонального информационного менеджера.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Запуск службы управления личной информацией..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Завершение работы службы управления личной информацией..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Служба управления личной информацией производит обновление базы данных." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Служба управления личной информацией производит обновление базы данных.\n" +"Это происходит после обновления версии программного обеспечения и требуется " +"для оптимизации производительности.\n" +"Вам придётся подождать несколько минут, в зависимости от объёма вашей " +"информации." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Служба управления личной информацией Akonadi не запущена, это приложение не " +"может работать без неё." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Запустить Akonadi" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Инфраструктура управления личной информацией Akonadi не работает.\n" +"Перейдите по ссылке «Подробности» для получения дополнительных сведений." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Инфраструктура управления личной информацией Akonadi не работает." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Подробности..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Удалить учётную запись «%1»?" + +# BUGME: please remove question mark; please add ctxt @title:window --aspotashev +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Удаление учётной записи" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Входящие (должна быть хотя бы одна учётная запись):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Добавить..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Изменить..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Удалить" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Перезапустить" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Недавние папки" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Переименование избранной папки" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Введите новое имя папки:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Внутренние тесты сервера Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Сохранить отчёт..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Копировать отчёт в буфер обмена" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Для работы сервера Akonadi с текущими параметрами требуется драйвер QtSQL " +"«%1», и он был найден в системе." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Для работы сервера Akonadi с текущими параметрами требуется драйвер QtSQL " +"«%1».\n" +"Установлены следующие драйверы: %2.\n" +"Установите необходимый драйвер." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Найден драйвер базы данных." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Драйвер базы данных не найден." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Сервер MySQL не проверен." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Текущая конфигурация не требует сервера MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Сервер Akonadi настроен на использование сервера MySQL «%1».\n" +"Проверьте, что сервер MySQL установлен и доступен для запуска. Исполняемый " +"файл MySQL обычно называется «mysqld», и его расположение различается в " +"разных дистрибутивах." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Сервер MySQL не найден." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Невозможно открыть исполняемый файл сервера MySQL." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Невозможно запустить исполняемый файл сервера MySQL." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Сервер MySQL обнаружен, но имеет недопустимое имя файла." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Сервер MySQL доступен." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Сервер MySQL доступен: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Сервер MySQL можно запустить." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "Ошибка запуска сервера MySQL «%1»: %2" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Ошибка запуска сервера MySQL" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Журнал сервера MySQL не проверен." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Журнал сервера MySQL не найден." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Сервер MySQL не сообщил об ошибках при запуске. Журнал находится в файле " +"«%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Невозможно прочитать журнал сервера MySQL." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Найден журнал сервера MySQL, но он недоступен для чтения: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Журнал сервера MySQL содержит ошибки." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Журнал сервера MySQL «%1» содержит ошибки." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Журнал сервера MySQL содержит предупреждения." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Журнал сервера MySQL «%1» содержит предупреждения." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Журнал сервера MySQL не содержит ошибок." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "Журнал сервера MySQL «%1» не содержит ошибок или предупреждений." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Конфигурация сервера MySQL не проверена." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Обнаружена конфигурация по умолчанию сервера MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Конфигурация по умолчанию сервера MySQL найдена в «%1»." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Конфигурация по умолчанию сервера MySQL не найдена." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Конфигурация по умолчанию сервера MySQL не найдена или недоступна для " +"чтения. Проверьте установку Akonadi и права доступа." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Настроенная конфигурация сервера MySQL не найдена." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "Необязательная настроенная конфигурация сервера MySQL не найдена." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Обнаружена настроенная конфигурация сервера MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Настроенная конфигурация сервера MySQL найдена и доступна для чтения в «%1»" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Настроенная конфигурация сервера MySQL недоступна для чтения." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Настроенная конфигурация сервера MySQL найдена в «%1», но недоступна для " +"чтения. Проверьте права доступа." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Конфигурация сервера MySQL не найдена или недоступна для чтения." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Конфигурация сервера MySQL не найдена или недоступна для чтения." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Конфигурация сервера MySQL настроена." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Конфигурация сервера MySQL найдена в «%1» и доступна для чтения." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Не удалось подключиться к серверу PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Сервер PostgreSQL доступен." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Сервер PostgreSQL доступен, подключение работает." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "Программа «akonadictl» не найдена" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Программа «akonadictl» должна быть доступна через $PATH. Проверьте установку " +"сервера Akonadi." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "Программа «akonadictl» найдена и готова к использованию" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Программа управления сервером Akonadi «%1» найдена и успешно запускается.\n" +"Результат запуска:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "Программа «akonadictl» найдена, но не готова к использованию" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Программа управления сервером Akonadi «%1» найдена, но запускается c " +"ошибкой.\n" +"Результат запуска:\n" +"%2\n" +"Проверьте установку сервера Akonadi." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Процесс управления Akonadi зарегистрирован в D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Процесс управления Akonadi зарегистрирован в D-Bus, что обычно показывает " +"готовность к использованию." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Процесс управления Akonadi не зарегистрирован в D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Процесс управления Akonadi не зарегистрирован в D-Bus, что обычно означает " +"ошибку при запуске или то, что служба не запущена." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Процесс управления сервером Akonadi зарегистрирован в D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Процесс управления сервером Akonadi зарегистрирован в D-Bus, что обычно " +"показывает готовность к использованию." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Процесс управления сервером Akonadi не зарегистрирован в D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Процесс управления сервером Akonadi не зарегистрирован в D-Bus, что обычно " +"означает ошибку при запуске или то, что сервер не запущен." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Невозможно проверить версию протокола." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "Невозможно проверить версию протокола без подключения к серверу." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Версия протокола сервера слишком старая." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Версия протокола сервера — %1, но для работы программы необходима версия не " +"ниже %2. Если было выполнено обновление KDE PIM, перезапустите сервер " +"Akonadi и все приложения KDE PIM." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Версия протокола сервера слишком новая." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Версия протокола сервера удовлетворяет требованиям программы." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Текущая версия протокола — %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Обнаружены агенты источников." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Обнаружен как минимум один агент источника." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Агенты источников не найдены." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Агенты источников не найдены, поэтому Akonadi не будет работать. Обычно это " +"означает, что агенты не установлены или были ошибки при установке. Поиск " +"агентов осуществляется в «%1». Переменная среды XDG_DATA_DIRS установлена в " +"«%2»; проверьте, входят ли в этот список все пути к агентам Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Журнал ошибок сервера Akonadi не найден." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "При запуске сервера Akonadi не было никаких ошибок." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Обнаружен журнал ошибок сервера Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"При текущем запуске сервера Akonadi произошли ошибки. Журнал находится в " +"файле %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Журнал ошибок предыдущего запуска сервера Akonadi не найден." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "При предыдущем запуске сервера Akonadi не было никаких ошибок." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Обнаружен журнал ошибок предыдущего запуска сервера Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"При предыдущем запуске сервера Akonadi произошли ошибки. Журнал находится в " +"файле %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Журнал ошибок запуска программы управления Akonadi не найден." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "При запуске программы управления Akonadi не было никаких ошибок." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Обнаружен журнал ошибок программы управления Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"При текущем запуске программы управления Akonadi произошли ошибки. Журнал " +"находится в файле %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" +"Журнал ошибок предыдущего запуска программы управления Akonadi не найден." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"При предыдущем запуске программы управления Akonadi не было никаких ошибок." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" +"Обнаружен журнал ошибок предыдущего запуска программы управления Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"При предыдущем запуске программы управления Akonadi произошли ошибки. Журнал " +"находится в файле %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi запущен с правами администратора" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Запуск приложений, доступных из Интернета, с правами администратора/root " +"может угрожать вашей безопасности. Поэтому база данных MySQL, используемая " +"этой инсталляцией Akonadi, не позволяет запускать себя под именем root." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi запущен без прав администратора" + +# BUGME: ambiguity (does "which" belong to "running as root" or to "not running as root"). --aspotashev +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi запущен не с правами администратора/root, как и рекомендуется для " +"безопасности системы." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Сохранить отчёт" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Ошибка" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Невозможно открыть файл «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Ошибка запуска сервера Akonadi. Будут запущены внутренние тесты для " +"определения причин ошибки и решения проблемы. Если вы будете отправлять " +"отчёт об ошибке, обязательно включите отчёт выполнения тестов." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Подробности" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Различные советы по решению проблем можно найти на веб-странице userbase.kde.org/Akonadi/ru.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Создать папку..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Создать" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Удалить %1 папку" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Удалить" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "С&инхронизировать %1 папку" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Синхронизировать" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Свойства &папки" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Свойства" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Вставить" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Вставить" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Управление п&одпиской..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Управление подпиской" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Добавить в избранные папки" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Добавить в избранные" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Удалить из избранных папок" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Удалить из избранных" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Переименовать в избранных..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Переименовать" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Копировать папку в..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Копировать в" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Копировать объект в..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Переместить объект в..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Переместить в" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Переместить папку в..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "&Копировать %1 объект" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Вырезать" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "В&ырезать %1 папку" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Создать источник данных" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Удалить %1 источник данных" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Свойства источника данных" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Синхронизировать %1 источник данных" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Работать автономно" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Синхронизировать папку &рекурсивно" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Синхронизировать рекурсивно" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Пере&местить папку в корзину" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Переместить папку в корзину" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Пере&местить объект в корзину" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Переместить объект в корзину" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Восстановить папку из корзины" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Восстановить папку из корзины" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Восстановить объект из корзины" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Восстановить объект из корзины" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Восстановить коллекцию из корзины" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Восстановить коллекцию из корзины" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Синхронизировать избранные папки" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Синхронизировать избранные папки" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Синхронизировать дерево папок" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Копировать %1 папку" +msgstr[1] "&Копировать %1 папки" +msgstr[2] "&Копировать %1 папок" +msgstr[3] "&Копировать папку" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Копировать %1 объект" +msgstr[1] "&Копировать %1 объекта" +msgstr[2] "&Копировать %1 объектов" +msgstr[3] "&Копировать объект" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Копировать %1 объект" +msgstr[1] "&Копировать %1 объекта" +msgstr[2] "&Копировать %1 объектов" +msgstr[3] "&Копировать объект" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "В&ырезать %1 папку" +msgstr[1] "В&ырезать %1 папки" +msgstr[2] "В&ырезать %1 папок" +msgstr[3] "В&ырезать папку" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Удалить %1 объект" +msgstr[1] "&Удалить %1 объекта" +msgstr[2] "&Удалить %1 объектов" +msgstr[3] "&Удалить объект" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Удалить %1 папку" +msgstr[1] "&Удалить %1 папки" +msgstr[2] "&Удалить %1 папок" +msgstr[3] "&Удалить папку" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "С&инхронизировать %1 папку" +msgstr[1] "С&инхронизировать %1 папки" +msgstr[2] "С&инхронизировать %1 папок" +msgstr[3] "С&инхронизировать папку" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Удалить %1 источник данных" +msgstr[1] "&Удалить %1 источника данных" +msgstr[2] "&Удалить %1 источников данных" +msgstr[3] "&Удалить источник данных" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "С&инхронизировать %1 источник данных" +msgstr[1] "С&инхронизировать %1 источника данных" +msgstr[2] "С&инхронизировать %1 источников данных" +msgstr[3] "С&инхронизировать источник данных" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Копировать %1 папку" +msgstr[1] "Копировать %1 папки" +msgstr[2] "Копировать %1 папок" +msgstr[3] "Копировать папку" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Копировать %1 объект" +msgstr[1] "Копировать %1 объекта" +msgstr[2] "Копировать %1 объектов" +msgstr[3] "Копировать объект" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Вырезать %1 объект" +msgstr[1] "Вырезать %1 объекта" +msgstr[2] "Вырезать %1 объектов" +msgstr[3] "Вырезать объект" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Вырезать %1 папку" +msgstr[1] "Вырезать %1 папки" +msgstr[2] "Вырезать %1 папок" +msgstr[3] "Вырезать папку" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Удалить %1 объект" +msgstr[1] "Удалить %1 объекта" +msgstr[2] "Удалить %1 объектов" +msgstr[3] "Удалить объект" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Удалить %1 папку" +msgstr[1] "Удалить %1 папки" +msgstr[2] "Удалить %1 папок" +msgstr[3] "Удалить папку" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Синхронизировать %1 папку" +msgstr[1] "Синхронизировать %1 папки" +msgstr[2] "Синхронизировать %1 папок" +msgstr[3] "Синхронизировать папку" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Удалить %1 источник данных" +msgstr[1] "Удалить %1 источника данных" +msgstr[2] "Удалить %1 источников данных" +msgstr[3] "Удалить источник данных" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Синхронизировать %1 источник данных" +msgstr[1] "Синхронизировать %1 источника данных" +msgstr[2] "Синхронизировать %1 источников данных" +msgstr[3] "Синхронизировать источник данных" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Имя" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Удалить %1 папку и все вложенные папки?" +msgstr[1] "Удалить %1 папки и все вложенные папки?" +msgstr[2] "Удалить %1 папок и все вложенные папки?" +msgstr[3] "Удалить эту папку и все вложенные папки?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Удаление папок" +msgstr[1] "Удаление папок" +msgstr[2] "Удаление папок" +msgstr[3] "Удаление папки" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Невозможно удалить папку «%1»." + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Ошибка удаления папки" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Свойства папки %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Удалить %1 объект?" +msgstr[1] "Удалить %1 объекта?" +msgstr[2] "Удалить %1 объектов?" +msgstr[3] "Удалить выбранный объект?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Удаление объектов" +msgstr[1] "Удаление объектов" +msgstr[2] "Удаление объектов" +msgstr[3] "Удаление объекта" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Не удалось удалить объект: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Ошибка удаления объекта" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Переименование избранной папки" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Введите новое имя папки:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Создание источника данных" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Не удалось создать источник данных Akonadi: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Ошибка создания источника" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Удалить %1 источник данных Akonadi?" +msgstr[1] "Удалить %1 источника данных Akonadi?" +msgstr[2] "Удалить %1 источников данных Akonadi?" +msgstr[3] "Удалить этот источник данных Akonadi?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Удаление источников" +msgstr[1] "Удаление источников" +msgstr[2] "Удаление источников" +msgstr[3] "Удаление источника" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Невозможно вставить данные: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Ошибка вставки" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Мы не можем включить «/» в имя папки." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Ошибка создания новой папки" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Мы не можем вставить «.» в начало или конец имени папки." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Для синхронизации папки «%1», нужно, чтобы этот ресурс был подключен. Вы " +"хотите подключить его?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Учётная запись «%1» не в сети" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Подключиться" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Переместить в эту папку" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Копировать в эту папку" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Не удалось обновить подписку: %1" + +# BUGME: what does this mean exactly? --aspotashev +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Ошибка подписки" + +# BUGME: what does this mean exactly? --aspotashev +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Локальные подписки" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Поиск:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Только подписанные" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Подписаться" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Отписаться" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Не удалось создать новую метку" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "При создании метки произошла ошибка" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Удалить метку %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Удаление метки" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Удалить метку" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Выберите метки для объектов." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Создать метку" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Редактор меток" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Выберите метки..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Выбор меток" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Очистить" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Нажмите для добавления меток" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Преобразователь из Akonadi в XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Преобразует поддерево коллекции Akonadi в файл XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© Volker Krause , 2009" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Данные не загружены." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Имя файла не указано." + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Не удалось открыть файл данных «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Файл %1 не существует." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Не удалось интерпретировать содержимое файла данных «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" +"Не удалось прочитать или интерпретировать определение схемы данных XML." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Не удалось создать контекст обработчика схемы данных XML." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Не удалось создать схему данных XML." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Не удалось создать контекст проверки правильности схемы данных XML." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Недопустимый формат файла." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Не удалось интерпретировать содержимое файла данных: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Не удалось найти коллекцию %1" + +#~ msgid "Id" +#~ msgstr "Идентификатор" + +#~ msgid "Remote Id" +#~ msgstr "Сетевой идентификатор" + +#~ msgid "MimeType" +#~ msgstr "Тип MIME" + +#~ msgid "Default Name" +#~ msgstr "Имя по умолчанию" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Удалить" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Отмена" + +#~ msgid "Take left one" +#~ msgstr "Применить левое" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Два обновления конфликтуют.Выберите, какое из обновление следует " +#~ "применить." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Непрочитанных" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Всего" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Размер" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Источник данных Akonadi" + +# BUGME: name of a folder? (need better msgctxt; what is "folder" here -- a mail folder or folder in filesystem?) --aspotashev +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Название" + +#~ msgid "Invalid collection specified" +#~ msgstr "Указана недопустимая коллекция" + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Обнаружена несогласованность в локальном дереве коллекций." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Удалённая коллекция не имеет цепочки предшественников от корня, ресурс " +#~ "испорчен." + +#~ msgid "KDE Test Program" +#~ msgstr "Тестовая программа KDE" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Версия протокола сервера достаточно новая." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Версия протокола сервера — %1, но для работы необходима версия не ниже %2." + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Обнаружена версия протокола %1, требуется %2 или более новая" + +#~ msgid "Cannot list root collection." +#~ msgstr "Не удалось прочитать список папок в корневой коллекции." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Служба поиска Nepomuk зарегистрирована в D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Служба поиска Nepomuk зарегистрирована в D-Bus, что обычно показывает " +#~ "готовность к использованию." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Служба поиска Nepomuk не зарегистрирована в D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Служба поиска Nepomuk не зарегистрирована в D-Bus, что обычно означает " +#~ "ошибку при запуске или то, что служба не запущена." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Служба поиска Nepomuk использует неподходящий модуль." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Служба поиска Nepomuk использует модуль «%1», который не рекомендуется " +#~ "для использования с Akonadi." + +# BUGME: remove trailing space --aspotashev +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Служба поиска Nepomuk использует подходящий модуль." + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Служба поиска Nepomuk использует один из модулей, рекомендованных для " +#~ "использования с Akonadi." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Модуль «%1» скомпилирован не как встроенный статический, пожалуйста, " +#~ "сообщите это в отчёте об ошибке." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Модуль скомпилирован не как статический" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "&Создать папку..." + +#, fuzzy +#~| msgid "Folder &Properties..." +#~ msgid "Resource Properties" +#~ msgstr "Свойства &папки..." + +#~ msgid "Cache" +#~ msgstr "Кэш" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Наследовать политику кэширования" + +#~ msgid "Cache Policy" +#~ msgstr "Политика кэширования" + +#~ msgid "Interval check time:" +#~ msgstr "Интер&вал между проверками:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Время ожидания для локального кэша:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Синхронизировать по запросу" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Выбор папок, которые будут показаны в списке папок" + +#, fuzzy +#~| msgctxt "search folder" +#~| msgid "Search:" +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Поиск:" + +#, fuzzy +#~| msgid "Available folders" +#~ msgid "Available Folders" +#~ msgstr "Доступные папки" + +#, fuzzy +#~| msgid "Current changes" +#~ msgid "Current Changes" +#~ msgstr "Текущие изменения" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Отписаться от выбранной папки" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "" +#~ "При запуске сервера Akonadi были ошибки, информация о которых сохранена в " +#~ "«%1»." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "" +#~ "При запуске программы управления Akonadi были ошибки, информация о " +#~ "которых сохранена в «%1»." + +#~ msgid "TODO" +#~ msgstr "Не реализовано" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Сервер Akonadi не работает.
Подробности...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Источник Akonadi" + +#, fuzzy +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "Коллекция не найдена." +#~ msgstr[1] "Коллекция не найдена." +#~ msgstr[2] "Коллекция не найдена." +#~ msgstr[3] "Коллекция не найдена." + +#, fuzzy +#~| msgid "&Copy Folder" +#~| msgid_plural "&Copy %1 Folders" +#~ msgid "Copy failed" +#~ msgstr "&Копировать %1 папку" diff --git a/po/se/libakonadi5.po b/po/se/libakonadi5.po new file mode 100644 index 0000000..f67d5a9 --- /dev/null +++ b/po/se/libakonadi5.po @@ -0,0 +1,2466 @@ +# Translation of libakonadi5 to Northern Sami +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2007-09-11 22:44+0200\n" +"Last-Translator: Northern Sami translation team \n" +"Language-Team: Northern Sami \n" +"Language: se\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "KPilot-heivehus" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "" +msgstr[1] "" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Oza" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "" +msgstr[1] "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" +msgstr[1] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Fiillat" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "amas" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "&Bienat" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Sisaboahtti konttut (lasit unnimus ovtta):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Váldde eret" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Álggat ođđasit" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Meattáhus" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "" +msgstr[1] "" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Sirdde dán máhppii" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Sihko gilkora" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Sihko gilkora" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Sálke" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr " …" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "" diff --git a/po/sk/akonadi_knut_resource.po b/po/sk/akonadi_knut_resource.po new file mode 100644 index 0000000..632f597 --- /dev/null +++ b/po/sk/akonadi_knut_resource.po @@ -0,0 +1,82 @@ +# translation of akonadi_knut_resource.po to Slovak +# Richard Fric , 2009. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2012-07-03 12:19+0100\n" +"Last-Translator: Roman Paholík \n" +"Language-Team: Slovak \n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 0.3\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nevybraný dátový súbor." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Súbor '%1' úspešne načítaný." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Vybrat dátový súbor" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Dátový súbor Akonadi Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Nenašli sa položky pre vzdialené id %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Nadradená kolekcia nenájdená v DOM strome." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Nemôžem zapísať kolekciu." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Zmenená kolekcia nenájdená v DOM strome." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Vymazaná kolekcia nenájdená v DOM strome." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Nadradená kolekcia '%1' nenájdená v DOM strome." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Nemôžem zapísať položku." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Zmenená položka nenájdená v DOM strome." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Vymazaná položka nenájdená v DOM strome." diff --git a/po/sk/libakonadi5.po b/po/sk/libakonadi5.po new file mode 100644 index 0000000..05b1606 --- /dev/null +++ b/po/sk/libakonadi5.po @@ -0,0 +1,2701 @@ +# translation of libakonadi5.po to Slovak +# Roman Paholík , 2014, 2015, 2016, 2017. +# Matej Mrenica , 2019, 2020. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi5\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2020-09-08 19:04+0200\n" +"Last-Translator: Matej Mrenica \n" +"Language-Team: Slovak \n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.08.1\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Roman Paholík" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "wizzardsk@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Momentálne nie je nakonfigurovaný žiadny účet." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integrácia účtov nie je podporovaná" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Nemôžem registrovať objekt na dbus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 typu %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Identifikátor agenta" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pripravený" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Offline" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synchronizovanie..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Chyba." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Nenastavené" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Identifikátor zdroja" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Zdroj Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Vrátená neplatná položka" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Chyba počas vytvárania položky: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Chyba počas aktualizácie kolekcie: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Aktualizácia miestnej kolekcie zlyhala: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Aktualizácia miestnych položiek zlyhala: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Nemôžete vybrať položku v offline móde." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synchronizujem priečinok '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Zlyhalo získanie kolekcie pre sync." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Zlyhalo získanie kolekcie pre atribút sync." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Vyžadovaná položka už neexistuje" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Úloha zrušená." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Žiadna kolekcia." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Nájdené nevyriešené opustené kolekcie" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Nenájdená ďalšia položka pre správu konfilktov" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Nemôžem pristupovať k rozhraniu D-Bus vytvoreného agenta." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Vypršal čas na vytvorenie inštancie agenta." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Nemôžem získať typ agenta '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Nie je možné vytvoriť inštanciu agenta" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Neplatná inštancia kolekcie." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Neplatná inštancia zdroja." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Nemôžem získať rozhranie D-Bus pre zdroj '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Vypršal čas synchronizácie atribútov kolekcie." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Neplatná kolekcia na kopírovanie" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Neplatná cieľová kolekcia" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Neplatný predok" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Zlyhalo spracovanie kolekcie z odpovede" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Neplatná kolekcia" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Zadaná neplatná kolekcia." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Neurčené objekty na presun" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Nezadaný platný cieľ" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Neplatná kolekcia." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Neplatná rodičovská kolekcia" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Nie je možné sa pripojiť ku službe Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Verzia protokolu Akonadi serveru je nekompatibilná. Uistite sa že máte " +"nainštalovanú kompatibilnú verziu." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Operácia zrušená uživateľom." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Neznáma chyba." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Neočakávaná odpoveď" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Zlyhalo vytvorenie vzťahu." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Vypršal čas synchronizácie zdroja." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Nemôžem získať koreňovú kolekciu zdroja %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Nezadané ID zdroja." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Neplatný identifikátor zdroja '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Zlyhalo nastavenie predvoleného zdroja cez D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Zlyhalo získanie kolekcie zdroja." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Vypršal čas pokusu o získanie zámku." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Zlyhalo vytvorenie značky." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Presun do kolekcie koša zlyhal, ruší sa operácia koša" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Zadané neplatné položky" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Zadaná neplatná kolekcia" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Žiadna platná kolekcia alebo prázdny zoznam položiek" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Nemôžem nájsť obnoviť kolekciu a obnovený zdroj je nedostupný" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Meno" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Načítava sa..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Chyba" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Cieľová kolekcia '%1' už obsahuje\n" +"kolekciu s názvom '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Názov" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Nemôžem kopírovať položku: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Nemôžem kopírovať kolekciu: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Nemôžem presunúť položku: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Nemôžem presunúť kolekciu: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Nemôžem odkázať entitu: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Chyba" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Obľúbené priečinky" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Spolu správ" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Neprečítaných správ" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvóta" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Veľkosť úložiska" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Veľkosť úložiska podpriečinka" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Neprečítané" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Celkom" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Veľkosť" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Značka" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Nemôžem natiahnuť položku pre index" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index už nie je dostupný" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Časť užitočného nákladu '%1' nie je dostupná pre tento index" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Žiadne sedenie dostupné pre tento index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Žiadna položka dostupná pre tento index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Nepomenovaný plugin" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Popis nie je k dispozícii" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Verzia protokolu servera Akonadi sa líši od verzie protokolu v tejto " +"aplikácii.\n" +"Ak ste nedávno aktualizovali systém, prosím odhláste sa a prihláste, aby " +"všetky aplikácie používali rovnakú verziu protokolu." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Nie sú dostupní žiadni agenti Akondi. Prosím, skontolujte vašu inštaáciu KDE " +"PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Verzia protokolu sa nezhoduje. Verzia servera je staršia (%1) ako naša (%2). " +"Ak ste nedávno aktualizovali systém, prosím reštartujte server Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Verzia protokolu sa nezhoduje. Verzia servera je novšia (%1) ako naša (%2). " +"Ak ste nedávno aktualizovali systém, prosím reštartujte všetky KDE PIM " +"aplikácie." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Samočinný test Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Skontroluje a oznámi stav servera Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "Nová inštancia agenta..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Vymazať inštanciu agenta" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Nastaviť inštanciu agenta" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nová inštancia agenta" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Nemôžem vytvoriť inštanciu agenta: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Vytvorenie inštancie agenta zlyhalo" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Vymazať inštanciu agenta?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Naozaj chcete vymazať vybranú inštanciu agenta?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 Nastavenia" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 Príručka" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "O %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Dialógové okno nastavení bolo otvorené v inom okne" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Nastavenia pre %1 je sú už otvorené niekde inde." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Zlyhalo registrovanie dialógového okna s nastaveniami %1" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minúta" +msgstr[1] "minúty" +msgstr[2] "minút" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Prijímanie" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Použiť voľby z nadradeného priečinka alebo účtu" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synchronizovať pri vybraní tohto priečinku" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Automaticky synchronizovať po:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nikdy" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minúty" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokálne cacheované časti" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Možnosti prijímania" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Vždy získať plné správy" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Získať telá správ na vyžiadanie" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Ponechať telá správ lokálne pre:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Navždy" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Hľadať" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Použiť priečinok predvolene" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Nový po&dpriečinok..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Vytvoriť nový podpriečinok pod aktuálne vybraným priečinkom" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nový priečinok" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Názov" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Nie je možné vytvoriť priečinok: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Nepodarilo sa vytvoriť priečinok" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Všeobecné" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Jeden objekt" +msgstr[1] "%1 objekty" +msgstr[2] "%1 objektov" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Názov:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "Použiť vlastnú ikonu:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "zložka" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Štatistika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Obsah:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objektov" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Veľkosť:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Byt" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Pamätajte, že indexovanie môže chvíľu trvať." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Údržba" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Chyba počas získavania počtu indexovaných položiek" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexovaná %1 položka v tomto priečinku" +msgstr[1] "Indexované %1 položky v tomto priečinku" +msgstr[2] "Indexovaných %1 položiek v tomto priečinku" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Počítam indexované položky..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Súbory" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Typ priečinka:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "neznáme" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Položky" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Spolu položiek:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Neprečítané položky:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexácia" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Povoliť fulltextové indexovanie" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Získavam počet indexovaných položiek..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Preindexovať priečinok" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Žiaden priečinok" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Otvoriť dialóg kolekcií" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Vybrať kolekciu" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Presunúť sem" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Skopírovať sem" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Zrušiť" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Čas zmeny" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Príznaky" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribút: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Riešenie konfliktu" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Zobrať moju verziu" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Zobrať ich verziu" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Ponechať obe verzie" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +" Vaše zmeny sú momentálne v konflikte s tými, ktoré medzitým urobil " +"niekto iný.
Pokiaľ sa jedna verzia nedá vyhodiť, budete musieť tieto " +"zmeny integrovať manuálne.
Kliknutím na " +"„Otvoriť textový editor“ si ponechajte kópiu textov, potom vyberte, " +"ktorá verzia je najvhodnejšia, potom ju znova otvorte a znova upravte, aby " +"ste pridali, čo chýba." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Dáta" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Spúšťa sa Akonadi server..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Zastavuje sa Akonadi server..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Presunúť sem" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopírovať sem" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Odkaz sem" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Zrušiť" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Nie je možné sa pripojiť ku službe správy osobných informácií.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Služba správy osobných informácií sa spúšťa..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Služba správy osobných informácií sa vypína..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Služba správy osobných informácií vykonáva inováciu databázy." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Služba správy osobných informácií vykonáva inováciu databázy.\n" +"Toto sa deje po aktualizácii softvéru a je to potrebné na optimalizáciu " +"výkonu.\n" +"V závislosti od množstva osobných informácií, môže to chvíľu trvať." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Služba správy osobných informácií Akonadi nebeží. Táto aplikácia sa nedá " +"použiť bez nej." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Spustiť" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Správa osobných informácií Akonadi nie je funkčná.\n" +"Kliknite na \"Detaily...\" ak sa chcete dozvedieť viacej informácií o " +"probléme." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Služba správy osobných informácií Akonadi nie je funkčná." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Podrobnosti..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Chcete odstrániť účet '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Odstrániť účet?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Účty pre príjem (zadajte aspoň jeden účet):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Pridať..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Upraviť..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Odstrániť" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Reštart" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Nedávny priečinok" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Premenovať obľúbené" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Názov:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr " Self-Test Akonadi servera" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Uložiť report..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Skopírovať report do schránky" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Ovládač QtSQL '%1' vyžaduje vaša aktuálna konfigurácia servera Akonadi a bol " +"nájdený vo vašom systéme." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Aktuálna konfigurácia vašeho Akonadi servera si vyžaduje QtSQL ovládač " +"'%1'.\n" +"Tieto ovládače sú nainštalované: %2.\n" +"Uistite sa že je nainštalovaný požadovaný ovládač.." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Nájdený ovládač databázy." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Nenájdený ovládač databázy." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Vykonávací súbor MySQL servera nenájdený." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Aktuálna konfigurácia nevyžaduje interný MySQL server." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Máte aktuálne nakonfigurovaný Akonadi aby mohol používať MySQL server '%1'.\n" +"Uistite sa že máte nainštalovaný MySQL server, nastavte cestu a presvedčte " +"sa že máte práva na čítanie a spúšťanie vykonávacieho súboru servera. " +"Vykonávací súbor servera sa nazýva typicky 'mysqld' a jeho umiestnenie je " +"rôzne v závislosti od distribúcie. " + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL server nenájdený." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL server nemá nastavené práva na čítanie." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL server nie je spustiteľný." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL nájdený pod neočakávaným menom." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL server nájdený." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL server nájdený: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL server je spustiteľný." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Spustenie MySQL servera '%1' sa nepodarilo s nasledujúcou chybovou správou: " +"'%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Spustenie MySQL servera sa nepodarilo." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Neboli testované chybové logy MySQL servera." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Neboli nájdené aktuálne chybové logy MySQL servera." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL neoznámil žiadnu chybu počas jeho spustenia. Záznam sa dá nájsť v '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Log súbor MySQL servera nie je na čítanie." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Log súbor MySQL servera bol nájdený ale nie je na čítanie: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Log súbor MySQL servera obsahuje chyby." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Log súbor MySQL servera '%1'obsahuje chyby." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Log súbor MySQL servera obsahuje upozornenia." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Log súbor MySQL servera '%1' obsahuje upozornenia." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Log súbor MySQL servera neobsahuje chyby." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "Log súbor MySQL servera '%1' neobsahuje chyby ani upozornenia." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Konfigurácia MySQL servera nie je testovaná." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Nájdená východzia konfigurácia MySQL servera." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Nájdená východzia konfigurácia MySQL servera a je čitateľná na %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Nenájdená východzia konfigurácia MySQL servera." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Nenájdená alebo nečitateľná východzia konfigurácia MySQL servera. " +"Skontrolujte si či je inštalácia Akonadi kompletná a či máte všetky potrebné " +"prístupové práva." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Uživateľská konfigurácia MySQL servera nedostupná." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "Uživateľská konfigurácia MySQL servera nenájdená ale je voliteľná." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Nájdená uživateľská konfigurácia MySQL servera." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Nájdená uživateľská konfigurácia MySQL servera a je čitateľná na %1." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Uživateľská konfigurácia MySQL servera nie je na čítanie." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Uživateľská konfigurácia MySQL servera bola nájdená na %1 ale nie je na " +"čítanie. Skontrolujte si prístupové práva." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Konfigurácia MySQL servera nenájdená alebo nie je na čítanie." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Konfigurácia MySQL servera nenájdená alebo nie je na čítanie." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Konfigurácia MySQL servera použiteľná." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Konfigurácia MySQL servera nájdená na %1 a je čitateľná." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Nepodarilo sa pripojiť k serveru PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL server nájdený." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL server bol nájdený a pripojenie funguje." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl nenájdený" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Program 'akonadictl' musí byť v $PATH. Uistite sa že máte Akonadi server " +"nainštalovaný." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl nájdený a použiteľný" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Program '%1' ktorý kontroluje Akonadi server bol nájdený a môže byť úspešne " +"spustený.\n" +"Výsledok:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl nájdený ale nepoužiteľný" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Program '%1' ktorý kontroluje Akonadi server bol nájdený ale nemôže byť " +"úspešne spustený.\n" +"Výsledok:\n" +"%2\n" +"Uistite sa že je Akonadi server nainštalovaný správne." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Kontrolný proces Akonadi registrovaný na D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Kontrolný proces Akonadi registrovaný na D-Bus čo normálne znamená že môže " +"fungovať." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Kontrolný proces Akonadi neregistrovaný na D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Kontrolný proces Akonadi neregistrovaný na D-Bus čo normálne znamená že " +"nebol spustený alebo sa stala fatálna chyba pri spúšťaní." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Proces Akonadi servera registrovaný na D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Proces Akonadi servera registrovaný na D-Bus čo normálne znamená že môže " +"fungovať." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Proces Akonadi servera neregistrovaný na D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Proces Akonadi servera neregistrovaný na D-Bus čo normálne znamená že nebol " +"spustený alebo sa stala fatálna chyba pri spúšťaní." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Kontrola verzie protokolu nie je možná." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Bez pripojenia k serveru nie je možné skontrolovať či verzia protokolu spľňa " +"požiadavky." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Verzia protokolu servera je stará." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Verzia protokolu servera je %1, ale požadovaná je prinajmenšom %2. Ak ste " +"nedávno aktualizovali KDE PIM, prosím uistite sa, že ste reštartovali " +"Akonadi a KDE PIM aplikácie." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Verzia protokolu servera je príliš nová." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Verzia protokolu servera sa zhoduje." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Aktuálna verzia protokolu je %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Nájdený zdrojový agenti." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Minimálne jeden zdrojový agent nájdený." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Nenájdený zdrojový agenti." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Nenájdený žiaden zdrojový agent. Akonadi potrebuje minimálne jedného. " +"Znamená to že neboli nainštalovaní alebo sa vyskytol problém počas " +"inštalácie. Nasledujúce cesty boli prehľadané: '%1'. premenná prostredia " +"XDG_DATA_DIRS je '%2'. Uistite sa, že obsahujú všetky cesty odkiaľ sa " +"Akonadi zdroje inštalujú." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Nenájdený žiaden aktuálny chybový log Akonadi servera." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi server nezapísal žiadne chyby počas aktuálneho štartu." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Nájdený aktuálny chybový log Akonadi servera." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Server Akonadi oznámil chyby počas jeho spustenia. Záznam je možné nájsť v " +"%1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Nenájdený žiaden predchádzajúci chybový log Akonadi servera." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Akonadi server nezapísal žiadne chyby počas jeho predchádzajúceho štartu." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Nájdený predchádzajúci chybový log Akonadi servera." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Server Akonadi oznámil chyby počas jeho predošlého spustenia. Záznam je " +"možné nájsť v %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Nenájdený žiaden Akonadi control chybový log." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Proces Akonadi control nezapísal žiadnu chybu počas aktuálneho štartu." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Nájdený aktuálny Akonadi control chybový log." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Ovládací proces Akonadi oznámil chyby počas aktuálneho spustenia. Záznam je " +"možné nájsť v %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Nenájdený žiaden predchádzajúci Akonadi control chybový log." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Proces Akonadi control nezapísal žiadne chyby počas jeho predchádzajúceho " +"štartu." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Nájdený predchádzajúci Akonadi control chybový log." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Ovládací proces Akonadi oznámil chyby počas predošlého spustenia. Záznam je " +"možné nájsť v %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi bol spustený ako root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Spúšťanie aplikácií s prístupom na Internet ako root/administrator vás " +"vystavuje mnohým bezpečnostným rizikám. MySQL, použitá v tejto inštalácii " +"Akonadi, vám nedovolí spustiť sa ako root, na ochranu pred týmito rizikami." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi nebeží ako root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi nebeží ako používateľ root/administrator, čo je odporúčané " +"nastavenie pre bezpečný systém." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Ulož testovací report." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Chyba" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Nepodarilo sa otvoriť súbor '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Chyba počas štartu Akonadi servera. Nasledujúce self-testy vám pomôžu s " +"riešením problému. Akk požadujete pomoc alebo nahlasujete chybu, pripojte " +"vždy prosím tento záznam." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaily" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Pre viac tipov na riešenie problémov sa prosím obráťte na userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nový priečinok..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nové" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "&Odstrániť priečinok" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Odstrániť" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "Synchronizovať priečinok" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synchronizovať" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Vlastnosti zložky" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Vlastnosti" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "V&ložiť" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Vložiť" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Spravovať miestne prihlásenia(&S)..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Spravovať miestne prihlásenia" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Pridať do obľúbených priečinkov" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Pridať do obľúbených" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Odstrániť z obľúbených priečinkov" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Odstrániť z obľúbených" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Premenovať obľúbené..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Premenovať" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopírovať priečinok do..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopírovať do" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopírovať položku do..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Presunúť položku do..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Presunúť do" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Presunúť priečinok do..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "Vystrihnúť položku" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Vystrihnúť" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "Vystrihnúť priečinok" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Vytvoriť zdroj" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Odstrániť zdroj" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Vlastnosti zdroja" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Synchronizovať zdroj" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Pracovať offline" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Synchronizovať priečinok rekurzívne" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synchronizovať rekurzívne" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Presunúť priečinok do koša" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Presunúť priečinok do koša" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Presunúť položku do koša" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Presunúť položku do koša" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Obnoviť priečinok z koša" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Obnoviť priečinok z koša" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Obnoviť položku z koša" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Obnoviť položku z koša" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Obnoviť kolekciu z koša" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Obnoviť kolekciu z koša" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "Synchronizovať obľúbené priečinky" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synchronizovať obľúbené priečinky" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synchronizovať strom priečinkov" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Kopírovať zložku(&C)" +msgstr[1] "Kopírovať %1 zložky(&C)" +msgstr[2] "Kopírovať %1 zložiek(&C)" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Kopírovať položku(&C)" +msgstr[1] "Kopírovať %1 položky(&C)" +msgstr[2] "Kopírovať %1 položiek(&C)" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Vystrihnúť položku" +msgstr[1] "Vystrihnúť %1 položky" +msgstr[2] "Vystrihnúť %1 položiek" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Vystrihnúť priečinok" +msgstr[1] "Vystrihnúť %1 priečinky" +msgstr[2] "Vystrihnúť %1 priečinkov" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Zmazať položku(&D)" +msgstr[1] "Vymazať %1 položky(&D)" +msgstr[2] "Vymazať %1 položiek(&D)" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Odstrániť priečinok" +msgstr[1] "&Odstrániť %1 priečinky" +msgstr[2] "&Odstrániť %1 priečinkov" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Synchronizovať priečinok" +msgstr[1] "Synchronizovať %1 priečinky" +msgstr[2] "Synchronizovať %1 priečinkov" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Odstrániť zdroj" +msgstr[1] "Odstrániť %1 zdroje" +msgstr[2] "Odstrániť %1 zdrojov" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Synchronizovať zdroj" +msgstr[1] "Synchronizovať %1 zdroje" +msgstr[2] "Synchronizovať %1 zdrojov" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopírovať priečinok" +msgstr[1] "Kopírovať %1 priečinky" +msgstr[2] "Kopírovať %1 priečinkov" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopírovať položku" +msgstr[1] "Kopírovať %1 položky" +msgstr[2] "Kopírovať %1 položiek" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Vystrihnúť položku" +msgstr[1] "Vystrihnúť %1 položky" +msgstr[2] "Vystrihnúť %1 položiek" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Vystrihnúť priečinok" +msgstr[1] "Vystrihnúť %1 priečinky" +msgstr[2] "Vystrihnúť %1 priečinkov" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Odstrániť položku" +msgstr[1] "Vymazať %1 položky" +msgstr[2] "Vymazať %1 položiek" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Odstrániť priečinok" +msgstr[1] "Odstrániť %1 priečinky" +msgstr[2] "Odstrániť %1 priečinkov" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synchronizovať priečinok" +msgstr[1] "Synchronizovať %1 priečinky" +msgstr[2] "Synchronizovať %1 priečinkov" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Odstrániť zdroj" +msgstr[1] "Odstrániť %1 zdroje" +msgstr[2] "Odstrániť %1 zdrojov" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synchronizovať zdroj" +msgstr[1] "Synchronizovať %1 zdroje" +msgstr[2] "Synchronizovať %1 zdrojov" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Názov" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Naozaj chcete vymazať tento priečinok a všetky jeho podpriečinky?" +msgstr[1] "Naozaj chcete vymazať %1 priečinky a všetky ich podpriečinky?" +msgstr[2] "Naozaj chcete vymazať %1 priečinkov a všetky ich podpriečinky?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Odstrániť priečinok?" +msgstr[1] "Odstrániť priečinky?" +msgstr[2] "Odstrániť priečinky?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Nie je možné vymazať priečinok: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Vymazanie priečinku zlyhalo" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Vlastnosti priečinka %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Skutočne chcete zmazať túto položku?" +msgstr[1] "Skutočne chcete zmazať %1 položky?" +msgstr[2] "Skutočne chcete zmazať %1 položiek?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Vymazať položku?" +msgstr[1] "Vymazať položky?" +msgstr[2] "Vymazať položky?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Nemôžem vymazať položku: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Vymazanie položky zlyhalo" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Premenovať obľúbené" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Názov:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nový zdroj" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Nemôžem vytvoriť zdroj: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Vytvorenie zdroja zlyhalo" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Naozaj chcete odstrániť tento zdroj?" +msgstr[1] "Naozaj chcete odstrániť %1 zdroje?" +msgstr[2] "Naozaj chcete odstrániť %1 zdrojov?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Odstrániť zdroj?" +msgstr[1] "Odstrániť zdroje?" +msgstr[2] "Odstrániť zdroje?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Nie je možné vložiť dáta: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Vloženie sa nepodarilo" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Nemôžeme pridať \"/\" do názvu priečinku." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Chyba vytvorenia nového priečinka" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Nemôžeme pridať \".\" na začiatok alebo koniec názvu priečinka." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Pred synchronizáciou priečinka \"%1\" je potrebné mať prostriedok online. " +"Chcete ho mať online?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Účet \"%1\" je offline" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Prejsť online" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Presunúť do tohto priečinka" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopírovať do tohto priečinka" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Zlyhala aktualizácia odoberania: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Chyba prihlásenia" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Miestne prihlásenia" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Hľadať:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Iba prihlásené" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Prihlásiť sa" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Odhlásiť sa" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Zlyhalo vytvorenie novej značky" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Nastala chyba počas vytvárania novej značky" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Naozaj chcete odstrániť značku %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Odstrániť značku" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Odstrániť značku" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Vyberte, ktoré značky majú byť použité." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Vytvoriť novú značku" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Spravovať značky" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Vybrať značky..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Vybrať značky" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Vyčistiť" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Kliknite na pridanie značiek" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Konvertor Akonadi do XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Konvertuje podstrom Akonadi kolekcie do XML súboru." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Nenačítané žiadne údaje." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Nezadaný žiadny názov súboru" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Nemôžem otvoriť dátový súbor '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Súbor %1 neexistuje." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Nemôžem spracovať dátový súbor '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Definícia schémy sa nedá načítať a spracovať." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Nemôžem vytvoriť kontext spracovača schémy." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Nemôžem vytvoriť schému." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Nemôžem vytvoriť kontext kontroly schémy." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Neznámy formát súboru." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Nemôžem spracovať dátový súbor: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Nemôžem nájsť kolekciu %1" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Vzdialené ID" + +#~ msgid "MimeType" +#~ msgstr "MimeTyp" + +#~ msgid "Default Name" +#~ msgstr "Predvolené meno" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Odstrániť" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Zrušiť" + +#~ msgid "Take left one" +#~ msgstr "Ponechať ľavý" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Dve aktualizácie sú navzájom v konflikte.Prosím vyberte, ktorú " +#~ "použiť." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Neprečítané" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Celkovo" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Veľkosť" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Zdroj Akonadi" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Názov" + +#~ msgid "Invalid collection specified" +#~ msgstr "Zadaná neplatná kolekcia" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Nájdená verzia protokolu %1, očakávaná najmenej %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Verzia protokolu servera je nová." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Verzia protokolu servera je %1, čo znamená že je taká istá ako požadovaná " +#~ "%2, alebo novšia." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Detekovaný nekonzistentný miestny strom kolekcií." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Vzdialená kolekcia bez predchodcu reťaze zakončeného rootom, zdroj je " +#~ "poškodený." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE Testovací Program" diff --git a/po/sl/akonadi_knut_resource.po b/po/sl/akonadi_knut_resource.po new file mode 100644 index 0000000..f63e533 --- /dev/null +++ b/po/sl/akonadi_knut_resource.po @@ -0,0 +1,86 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Andrej Vernekar , 2012. +# Andrej Mernik , 2014. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2014-08-22 09:27+0200\n" +"Last-Translator: Andrej Mernik \n" +"Language-Team: Slovenian \n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n" +"%100==4 ? 3 : 0);\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Izbrana ni nobena podatkovna datoteka." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Datoteka »%1« je uspešno naložena." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Izberite podatkovno datoteko" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Podatkovna datoteka Akonadi Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Za oddaljeni določilnik %1 ni najdenega nobenega predmeta" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Nadrejena zbirka v drevesu DOM ni najdena." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Zbirke ni mogoče zapisati." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Spremenjena zbirka v drevesu DOM ni najdena." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Izbrisana zbirka v drevesu DOM ni najdena." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Nadrejena zbirka »%1« v drevesu DOM ni najdena." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Predmeta ni mogoče zapisati." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Spremenjen predmet v drevesu DOM ni najden." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Izbrisan predmet v drevesu DOM ni najden." diff --git a/po/sl/libakonadi5.po b/po/sl/libakonadi5.po new file mode 100644 index 0000000..c8b3f61 --- /dev/null +++ b/po/sl/libakonadi5.po @@ -0,0 +1,2647 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the akonadi package. +# +# Andrej Mernik , 2014, 2015, 2016, 2017, 2018. +# Matjaž Jeran , 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: akonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-03 08:39+0100\n" +"Last-Translator: Matjaž Jeran \n" +"Language-Team: Slovenian \n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Translator: Andrej Mernik \n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n" +"%100==4 ? 3 : 0);\n" +"X-Generator: Poedit 2.4.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Jure Repinc,Andrej Vernekar,Andrej Mernik,Matjaž Jeran" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" +"jlp@holodeck1.com,andrej.vernekar@gmail.com,andrejm@ubuntu.si,matjaz." +"jeran@amis.net" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Trenutno ni nastavljenega računa." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Integracija računov ni podprta" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Registracija predmeta na D-Busu ni uspela: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 vrste %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Določilnik posrednika" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadijev posrednik" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Pripravljen" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Nepovezan" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Usklajevanje..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Napaka." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Ni nastavljeno" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Določilnik vira" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Vir Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Pridobljen neveljaven predmet" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Napaka med ustvarjanjem predmeta: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Napaka med posodabljanjem zbirke: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Posodabljanje krajevne zbirke ni uspelo: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Posodabljanje krajevnih predmetov ni uspelo: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "V nepovezanem načinu ni mogoče pridobiti predmeta." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Usklajevanje mape '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Pridobivanje zbirke za uskladitev ni uspelo." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Pridobivanje zbirke za uskladitev atributov ni uspelo." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Zahtevan predmet ne obstaja več" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Posel preklican." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Takšne zbirke ni." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Najdene nerazrešene osirotele zbirke" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Drugi predmet za obravnavo spora ni bil najden" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Ni mogoče dostopati do vmesnika D-Bus ustvarjenega posrednika." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Čas za ustvarjanje primerka posrednika je pretekel." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Vrste posrednika '%1' ni mogoče pridobiti." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Ni mogoče ustvariti primerka posrednika." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Neveljaven primerek zbirke." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Neveljaven primerek vira." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Ni mogoče pridobiti vmesnika D-Bus za vir '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Čas za usklajevanje atributov zbirke je pretekel." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Neveljavna zbirka za kopiranje" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Neveljavna ciljna zbirka" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Neveljaven nadrejeni" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Ni bilo mogoče razčleniti zbirke v odgovoru" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Neveljavna zbirka" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Dana je bila neveljavna zbirka." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Za premikanje ni določen noben predmet" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Določen ni noben veljaven cilj" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Neveljavna zbirka." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Neveljavna nadrejena zbirka" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Ni se mogoče povezati s storitvijo Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Različica protokola strežnika Akonadi ni združljiva. Prepričajte se, da " +"imate nameščeno združljivo različico." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Uporabnik je preklical opravilo." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Neznana napaka." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Nepričakovan odgovor" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Ni bilo mogoče ustvariti povezave." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Čas za usklajevanje vira je pretekel." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Vrhovne zbirke vira %1 ni bilo mogoče pridobiti." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "ID vira ni bil podan." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Neveljaven določilnik vira '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Nastavljanje privzetega vira preko D-Busa ni uspelo." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Pridobivanje zbirke vira ni uspelo." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Čas za pridobivanje zaklepa je pretekel." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Ni bilo mogoče ustvariti oznake." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Premikanje v zbirko smeti ni uspelo, prekinjanje opravila" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Podani neveljavni predmeti" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Podana neveljavna zbirka" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Ni veljavne zbirke ali pa je seznam predmetov prazen" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Zbirke za obnovitev ni bilo mogoče najti, vir za obnovitev pa ni na voljo" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Ime" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Nalaganje..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Napaka" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Ciljna zbirka '%1' že vsebuje\n" +"zbirko z imenom '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Ime" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Predmeta ni bilo mogoče kopirati: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Zbirke ni bilo mogoče kopirati: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Predmeta ni bilo mogoče premakniti: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Zbirke ni bilo mogoče premakniti: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Entitete ni bilo mogoče povezati: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Napaka" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Priljubljene mape" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Skupno sporočil" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Neprebrana sporočila" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Količinska omejitev" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Velikost shrambe" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Velikost shrambe v podmapi" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Neprebranih" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Skupno" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Velikost" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Oznaka" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Predmeta ni mogoče pridobiti za ustvarjanje kazala" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Kazalo ni več na voljo" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Del vsebine '%1' za to kazalo ni na voljo" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Za to kazalo ni na voljo nobene seje" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Za to kazalo ni na voljo nobenega predmeta" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Neimenovan vstavek" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Opis ni na voljo" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Različica protokola strežnika Akonadi se ne ujema z različico protokola, ki " +"jo uporablja ta program.\n" +"Če ste nedavno posodobili vaš sistem, se odjavite in znova prijavite, da se " +"prepričate, da vsi programi uporabljajo pravilno različico protokola." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Na voljo ni nobenih posrednikov za Akonadi. Preverite vašo namestitev KDE " +"PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Neujemanje različice protokola. Različica na strežniku (%1) je starejša od " +"naše (%2). Če ste nedavno posodobili sistem, znova zaženite strežnik Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Neujemanje različice protokola. Različica na strežniku (%1) je novejša od " +"naše (%2). Če ste nedavno posodobili sistem, znova zaženite vse programe KDE " +"PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Samodejni preizkus strežnika Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Preizkusi in poroča o stanju strežnika Akonadi" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Nov primerek posrednika ..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Izbriši primerek posrednika" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Nastavi primerek posrednika" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Nov primerek posrednika" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Primerka posrednika ni bilo mogoče ustvariti: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Ustvarjanje primerka posrednika ni uspelo" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Izbrišem primerek posrednika?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Ali res želite izbrisati izbran primerek posrednika?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 nastavitev" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 Navodila" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "O %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Pogovor za nastavitve je bil odprt v drugem oknu" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Nastavitev %1 je že odprta drugje." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Ni bilo mogoče registrirati nastavitve %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minuta" +msgstr[1] "minuti" +msgstr[2] "minute" +msgstr[3] "minut" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Pridobivanje" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Uporabi možnosti iz nadrejene mape ali računa" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Uskladi ob izboru te mape" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Samodejno uskladi po:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Nikoli" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minut" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Krajevno predpomnjeni deli" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Možnosti pridobivanja" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Vedno pridobi celotna sporočila" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Telesa sporočil p&ridobi na zahtevo" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Telesa sporočil ohrani krajevno:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Trajno" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Poišči" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Privzeto uporabi mapo" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Nova podmapa..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Ustvari novo podmapo v trenutno izbrani mapi" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Nova mapa" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Ime" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Ni bilo mogoče ustvariti mape: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Ustvarjanje mape ni uspelo" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Splošno" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 predmet" +msgstr[1] "%1 predmeta" +msgstr[2] "%1 predmete" +msgstr[3] "%1 predmetov" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "Ime:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Uporabi ikono po meri:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "mapa" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistika" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Vsebina:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 predmetov" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Velikost:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 bajtov" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Zapomnite si, da lahko izgrajevanje kazala traja nekaj minut." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Vzdrževanje" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Napaka med pridobivanjem števila predmetov iz kazala" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "%1 predmet iz te mape je bil dodan v kazalo" +msgstr[1] "%1 predmeta iz te mape sta bila dodan v kazalo" +msgstr[2] "%1 predmeti iz te mape so bili dodana v kazalo" +msgstr[3] "%1 predmetov iz te mape je bilo dodanih v kazalo" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Preračunavanje predmetov v kazalu ..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Datoteke" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Vrsta mape:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "neznana" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Predmeti" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Skupno predmetov:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Neprebrani predmeti:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Izgradnja kazala" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Omogoči izgradnjo kazala s celotnim besedilom" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Pridobivanje števila predmetov iz kazala ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Znova izgradi kazalo mape" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ni mape" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Odpri pogovorno okno zbirk" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Izberite zbirko" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Pre&makni sem" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Kopiraj sem" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Prekliči" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Čas spremembe" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Zastavice" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Atribut: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Razreševanje sporov" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Uporabi mojo različico" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Uporabi njihovo različico" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Ohrani obe različici" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Vaše spremembe so v sporu s spremembami, ki jih je med vašim urejanjem " +"opravil nekdo drug.
V primeru, da ene izmed različic ne morete zavreči, " +"boste morali spremembe uveljaviti ročno.
Kliknite na \"Odpri urejevalnik besedila\", da ohranite kopijo " +"besedil, nato pa izberite najbolj pravilno različico. Slednjo znova odprite " +"in dodajte manjkajočo oz. spremenjeno vsebino." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Podatki" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Zaganjanje strežnika Akonadi ..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Zaustavljanje strežnika Akonadi ..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Pre&makni sem" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "Kopiraj sem" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Poveži sem" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "Prekliči" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Ni se mogoče povezati s storitvijo Upravljanja z osebnimi podatki.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Storitev upravljanja z osebnimi podatki se zaganja ..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Storitev upravljanja z osebnimi podatki se zaustavlja ..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Storitev upravljanja z osebnimi podatki izvaja nadgradnjo podatkovne zbirke." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Storitev upravljanja z osebnimi podatki izvaja nadgradnjo podatkovne zbirke. " +"Do nje pride po nadgradnji programske opreme in je potrebna za optimizacijo " +"hitrosti delovanja.\n" +"Nadgradnja lahko traja nekaj minut, odvisno od količine osebnih podatkov." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Storitev upravljanja z osebnimi podatki Akonadi ne teče. Tega programa brez " +"te storitve ni mogoče uporabljati." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Zaženi" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Ogrodje upravljanja z osebnimi podatki Akonadi ni delujoče.\n" +"Kliknite na \"Podrobnosti ...\", da pridobite podrobne podatke o tej težavi." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Ogrodje upravljanja z osebnimi podatki Akonadi ni delujoče." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Podrobnosti ..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Ali želite odstraniti račun '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Odstranim račun?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Dohodni računi (dodajte vsaj enega):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "Dodaj..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Spre&meni..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "Odstrani" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Znova zaženi" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Nedavna mapa" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Preimenuj priljubljeno" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Ime:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Samodejni preizkus strežnika Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Shrani poročilo ..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopiraj poročilo v odložišče" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Trenutne nastavitve strežnika Akonadi zahtevajo gonilnik QtSQL %1, ki je bil " +"najden na sistemu." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Trenutne nastavitve strežnika Akonadi zahtevajo gonilnik QtSQL '%1'.\n" +"Nameščeni so naslednji gonilniki: %2.\n" +"Prepričajte se, da je zahtevan gonilnik nameščen." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Gonilnik podatkovne zbirke je bil najden." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Gonilnik podatkovne zbirke ni bil najden." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Izvedljivi program strežnika MySQL ni bil preizkušen." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Trenutne nastavitve ne zahtevajo notranjega strežnika MySQL." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Akonadi je trenutno nastavljen za uporabo strežnika MySQL '%1'.\n" +"Prepričajte se, da je strežnik MySQL nameščen, nastavite pravilno pot in se " +"prepričajte, da imate na strežniku potrebna dovoljenja za branje in " +"izvajanje. Izvedljivi program strežnika se običajno imenuje 'mysqld', " +"njegovo mesto pa je odvisno od distribucije." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Strežnik MySQL ni bil najden." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Strežnik MySQL ni berljiv." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Strežnik MySQL ni izvedljiv." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Strežnik MySQL je bil najden, vendar ima nepričakovano ime." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Strežnik MySQL je bil najden." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Strežnik MySQL je bil najden: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Strežnik MySQL je izvedljiv." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Izvajanje strežnika MySQL '%1' je spodletelo z naslednjim sporočilom o " +"napaki: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Izvajanje strežnika MySQL ni uspelo." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Dnevnik napak strežnika MySQL ni bil preizkušen." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Trenutni dnevnik napak MySQL ni bil najden." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Strežnik MySQL med tem zagonom ni poročal o napakah. Dnevnik lahko najdete v " +"'%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Dnevnik napak MySQL ni berljiv." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Datoteka dnevnika napak strežnika MySQL je bila najdena, vendar ni berljiva: " +"%1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Dnevnik strežnika MySQL vsebuje napake." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Datoteka dnevnika napak strežnika MySQL '%1' vsebuje napake." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Dnevnik strežnika MySQL vsebuje opozorila." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Datoteka dnevnika strežnika MySQL '%1' vsebuje opozorila." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Dnevnik strežnika MySQL ne vsebuje napak." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"Datoteka dnevnika strežnika MySQL '%1' ne vsebuje nobene napake ali " +"opozorila." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Nastavitve strežnika MySQL niso bile preizkušene." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Najdene so bile privzete nastavitve strežnika MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "V %1 so bile najdene berljive privzete nastavitve strežnika MySQL." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Privzete nastavitve strežnika MySQL niso bile najdene." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Privzete nastavitve strežnika MySQL niso bile najdene ali pa niso berljive. " +"Preverite, ali je namestitev Akonadija pravilna in ali imate vsa potrebna " +"dovoljenja za dostop." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Prilagojene nastavitve strežnika MySQL niso na voljo." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Prilagojene nastavitve strežnika MySQL niso bile najdene, vendar tudi niso " +"obvezne." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Najdene so bile prilagojene nastavitve strežnika MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "V %1 so bile najdene berljive prilagojene nastavitve strežnika MySQL" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Prilagojene nastavitve strežnika MySQL niso berljive." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"V %1 so bile najdene berljive prilagojene nastavitve strežnika MySQL, vendar " +"niso berljive. Preverite svoja dovoljenja za dostop." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Nastavitve strežnika MySQL niso bile najdene ali pa niso berljive." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Nastavitve strežnika MySQL niso bile najdene ali pa niso berljive." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Nastavitve strežnika MySQL so uporabne." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Nastavitve strežnika MySQL so bile najdene v %1 in so berljive." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Ni se bilo mogoče povezati s strežnikom PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Strežnik PostgreSQL je bil najden." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Strežnik PostgreSQL je bil najden, povezava pa je delujoča." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl ni bil najden" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Program 'akonadictl' mora biti dostopen v eni izmed map iz spremenljivke " +"$PATH. Prepričajte se, da je strežnik Akonadi nameščen." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl je bil najden in je uporaben" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Program '%1' za nadzor strežnika Akonadi je bil najden in se je lahko " +"izvedel uspešno.\n" +"Rezultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl je bil najden, vendar ni uporaben" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Program '%1' za nadzor strežnika Akonadi je bil najden, vendar se ni mogel " +"uspešno izvesti.\n" +"Rezultat:\n" +"%2\n" +"Poskrbite, da je strežnik Akonadi nameščen pravilno." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Nadzorno opravilo Akondija je registrirano na D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Nadzorno opravilo Akondija je registrirano na D-Bus, kar običajno pomeni, da " +"je delujoče." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Nadzorno opravilo Akondija ni registrirano na D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Nadzorno opravilo Akondija ni registrirano na D-Bus, kar običajno pomeni, da " +"ni bilo zagnano ali pa je med zagonom naletelo na usodno napako." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Strežniško opravilo Akondija je registrirano na D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Strežniško opravilo Akondija je registrirano na D-Bus, kar običajno pomeni, " +"da je delujoče." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Strežniško opravilo Akondija ni registrirano na D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Strežniško opravilo Akondija ni registrirano na D-Bus, kar običajno pomeni, " +"da ni bilo zagnano ali pa je med zagonom naletelo na usodno napako." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Preverjanje različice protokola ni mogoče." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Brez povezave s strežnikom ni mogoče preveriti, ali različica protokola " +"ustreza zahtevam." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Različica protokola strežnika je prestara." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Različica protokola strežnika je %1, toda zahtevana je %2 ali novejša. Če " +"ste nedavno posodobili KDE PIM, znova zaženite tako programe KDE PIM kot " +"tudi Akonadi." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Različica protokola strežnika je preveč nova." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Različica protokola strežnika se ujema." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Trenutna različica protokola je %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Posredniki virov so bili najdeni." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Najden je bil vsaj en posrednik vira." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Brez najdenih posrednikov virov." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Najden ni bil noben posrednik virov. Za uporabo Akonadija je potreben vsaj " +"en. To običajno pomeni, da ni nameščen noben posrednik vira ali pa gre za " +"težavo z namestitvijo. Preiskane so bile naslednje mape: '%1'. Okoljska " +"spremenljivka XDG_DATA_DIRS je nastavljena na '%2'. Prepričajte se, da " +"vsebuje vse mape, kjer so nameščeni Akonadijevi posredniki." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Trenutni dnevnik napak strežnika Akonadi ni bil najden." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Strežnik Akonadi med trenutnim zagonom ni poročal o napakah." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Najden je bil trenutni dnevnik napak strežnika Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Strežnik Akonadi je med trenutnim zagonom poročal o napakah. Dnevnik lahko " +"najdete v %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Prejšnji dnevnik napak strežnika Akonadi ni bil najden." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Strežnik Akonadi med prejšnjim zagonom ni poročal o napakah." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Najden je bil prejšnji dnevnik napak strežnika Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Strežnik Akonadi je med prejšnjim zagonom poročal o napakah. Dnevnik lahko " +"najdete v %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Trenutni dnevnik napak nadzora Akonadija ni bil najden." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Nadzorno opravilo Akonadija med trenutnim zagonom ni poročalo o napakah." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Najden je bil trenutni dnevnik napak nadzora Akonadija." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Nadzorno opravilo Akonadija je med trenutnim zagonom poročalo o napakah. " +"Dnevnik lahko najdete v %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Prejšnji dnevnik napak nadzora Akonadija ni bil najden." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Nadzorno opravilo Akonadija med prejšnjim zagonom ni poročalo o napakah." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Najden je bil prejšnji dnevnik napak nadzora Akonadija." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Nadzorno opravilo Akonadija je med prejšnjim zagonom poročalo o napakah. " +"Dnevnik lahko najdete v %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi je bil zagnan pod uporabnikom root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Zaganjanje programov dostopnih iz interneta kot uporabnik root (skrbnik) " +"močno poveča varnostna tveganja. MySQL, ki ga uporablja ta namestitev " +"Akonadija, ne bo dovolil zaganjanja pod uporabnikom root in vas tako " +"obvaroval pred tveganji." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi ne teče pod uporabnikom root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi ne teče pod uporabnikom root (skrbnikom), kar je priporočena " +"nastavitev za varen sistem." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Shrani poročilo o preizkusu" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Napaka" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Ni bilo mogoče odpreti datoteke '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Med zagonom strežnika Akonadi je prišlo do napake. Naslednji preizkusi vam " +"lahko pomagajo pri odkrivanju in reševanju težave. Ob zahtevi po podpori ali " +"poročilu o napaki, vedno priložite to poročilo." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Podrobnosti" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Za več namigov pri odpravljanju napak se obrnite na userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Nova mapa..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Nov" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "Zbriši mapo" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Izbriši" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "Uskladi mapo" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Uskladi" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Lastnosti mape" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Lastnosti" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "Prilepi" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Prilepi" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Upravljaj krajevne naročnine..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Upravljaj krajevne naročnine" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Dodaj med priljubljene mape" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Dodaj med priljubljene" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Odstrani iz priljubljenih map" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Odstrani iz priljubljenih" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Preimenuj priljubljeno ..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Preimenuj" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopiraj mapo v ..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopiraj v" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopiraj predmet v ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Premakni predmet v ..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Premakni v" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Premakni mapo v ..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Izreži predmet" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Izreži" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Izreži mapo" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Ustvari vir" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Zbriši vir" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "Lastnosti vira" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Uskladi vir" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Delaj nepovezano" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "Rekurzivno u&skladi mapo" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Uskladi rekurzivno" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Pre&makni mapo v smeti" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Premakni mapo v smeti" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Pre&makni predmet v smeti" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Premakni predmet v smeti" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Obnovi mapo iz smeti" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Obnovi mapo iz smeti" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Obnovi predmet iz smeti" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Obnovi predmet iz smeti" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Obnovi zbirko iz smeti" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Obnovi zbirko iz smeti" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "U&skladi priljubljene mape" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Uskladi priljubljene mape" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Uskladi drevo map" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Kopiraj %1 mapo" +msgstr[1] "Kopiraj %1 mapi" +msgstr[2] "Kopiraj %1 mape" +msgstr[3] "Kopiraj %1 map" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Kopiraj %1 predmet" +msgstr[1] "Kopiraj %1 predmeta" +msgstr[2] "Kopiraj %1 predmete" +msgstr[3] "Kopiraj %1 predmetov" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Izreži %1 predmet" +msgstr[1] "Izreži %1 predmeta" +msgstr[2] "Izreži %1 predmete" +msgstr[3] "Izreži %1 predmetov" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Izreži %1 mapo" +msgstr[1] "Izreži %1 mapi" +msgstr[2] "Izreži %1 mape" +msgstr[3] "Izreži %1 map" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Zbriši %1 predmet" +msgstr[1] "Zbriši %1 predmeta" +msgstr[2] "Zbriši %1 predmete" +msgstr[3] "Zbriši %1 predmetov" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Zbriši %1 mapo" +msgstr[1] "Zbriši %1 mapi" +msgstr[2] "Zbriši %1 mape" +msgstr[3] "Zbriši %1 map" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "Uskladi %1 mapo" +msgstr[1] "Uskladi %1 mapi" +msgstr[2] "Uskladi %1 mape" +msgstr[3] "Uskladi %1 map" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Izbriši %1 vir" +msgstr[1] "Izbriši %1 vira" +msgstr[2] "Izbriši %1 vire" +msgstr[3] "Izbriši %1 virov" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "U&skladi %1 vir" +msgstr[1] "U&skladi %1 vira" +msgstr[2] "U&skladi %1 vire" +msgstr[3] "U&skladi %1 virov" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopiraj %1 mapo" +msgstr[1] "Kopiraj %1 mapi" +msgstr[2] "Kopiraj %1 mape" +msgstr[3] "Kopiraj %1 map" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopiraj %1 predmet" +msgstr[1] "Kopiraj %1 predmeta" +msgstr[2] "Kopiraj %1 predmete" +msgstr[3] "Kopiraj %1 predmetov" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Izreži %1 predmet" +msgstr[1] "Izreži %1 predmeta" +msgstr[2] "Izreži %1 predmete" +msgstr[3] "Izreži %1 predmetov" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Izreži %1 mapo" +msgstr[1] "Izreži %1 mapi" +msgstr[2] "Izreži %1 mape" +msgstr[3] "Izreži %1 map" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Izbriši %1 predmet" +msgstr[1] "Izbriši %1 predmeta" +msgstr[2] "Izbriši %1 predmete" +msgstr[3] "Izbriši %1 predmetov" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Izbriši %1 mapo" +msgstr[1] "Izbriši %1 mapi" +msgstr[2] "Izbriši %1 mape" +msgstr[3] "Izbriši %1 map" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Uskladi %1 mapo" +msgstr[1] "Uskladi %1 mapi" +msgstr[2] "Uskladi %1 mape" +msgstr[3] "Uskladi %1 map" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Zbriši %1 vir" +msgstr[1] "Zbriši %1 vira" +msgstr[2] "Zbriši %1 vire" +msgstr[3] "Zbriši %1 virov" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Uskladi %1 vir" +msgstr[1] "Uskladi %1 vira" +msgstr[2] "Uskladi %1 vire" +msgstr[3] "Uskladi %1 virov" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Ime" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Ali resnično želite izbrisati %1 mapo in vse podmape?" +msgstr[1] "Ali resnično želite izbrisati %1 mapi in vse podmape?" +msgstr[2] "Ali resnično želite izbrisati %1 mape in vse podmape?" +msgstr[3] "Ali resnično želite izbrisati %1 map in vse podmape?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Izbrišem mapo?" +msgstr[1] "Izbrišem mapi?" +msgstr[2] "Izbrišem mape?" +msgstr[3] "Izbrišem mape?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Ni bilo mogoče izbrisati mape: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Brisanje mape ni uspelo" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Lastnosti mape %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Ali resnično želite izbrisati %1 predmet?" +msgstr[1] "Ali resnično želite izbrisati %1 predmeta?" +msgstr[2] "Ali resnično želite izbrisati %1 predmete?" +msgstr[3] "Ali resnično želite izbrisati %1 predmetov?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Izbrišem predmet?" +msgstr[1] "Izbrišem predmeta?" +msgstr[2] "Izbrišem predmete?" +msgstr[3] "Izbrišem predmete?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Ni bilo mogoče izbrisati predmeta: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Brisanje predmeta ni uspelo" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Preimenuj priljubljeno" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Ime:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Nov vir" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Vira ni bilo mogoče ustvariti: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Ustvarjanje vira ni uspelo" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Ali resnično želite izbrisati %1 vir?" +msgstr[1] "Ali resnično želite izbrisati %1 vira?" +msgstr[2] "Ali resnično želite izbrisati %1 vire?" +msgstr[3] "Ali resnično želite izbrisati %1 virov?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Izbrišem vir?" +msgstr[1] "Izbrišem vira?" +msgstr[2] "Izbrišem vire?" +msgstr[3] "Izbrišem vire?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Podatkov ni bilo mogoče prilepiti: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Lepljenje ni uspelo" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Znaka \"/\" ni mogoče dodati v ime mape." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Napaka ustvarjanja nove mape" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Pike ni mogoče dodati na začetek ali konec imena mape." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Pred usklajevanjem mape \"%1\" mora biti vir povezan. Ali ga želite povezati?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Račun \"%1\" ni povezan" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Postani povezan" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Premakni v to mapo" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopiraj v to mapo" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Ni bilo mogoče osvežiti naročnine: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Napaka naročnine" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Krajevne naročnine" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Poišči:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Samo za naročene" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "Naroči se" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Odjavi naročnino" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Ni bilo mogoče ustvariti nove oznake" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Med ustvarjanjem nove oznake je prišlo do napake" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Ali resnično želite odstraniti oznako %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Izbriši oznako" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Izbriši oznako" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Izberite oznake, ki naj bodo uveljavljene." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Ustvari novo oznako" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Upravljanje oznak" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Izbriši oznake..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Izberi oznake" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Počisti" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Kliknite za dodajanje oznak" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Pretvornik iz Akonadi v XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Pretvori drevo zbirke Akonadi v datoteko XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Ni naloženih podatkov." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Ime datoteke ni navedeno" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Ni mogoče odpreti podatkovne datoteke '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Datoteka %1 ne obstaja." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Ni mogoče razčleniti podatkovne datoteke '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Ni bilo mogoče naložiti in razčleniti določila sheme." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Ni mogoče ustvariti konteksta razčlenjevalnika sheme." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Ni mogoče ustvariti sheme." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Ni mogoče ustvariti konteksta za preverjanje sheme." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Neveljavna vrsta datoteke." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Ni mogoče razčleniti podatkovne datoteke: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Ni mogoče najti zbirke %1" + +#~ msgid "Id" +#~ msgstr "ID" + +#~ msgid "Remote Id" +#~ msgstr "Oddaljeni ID" + +#~ msgid "MimeType" +#~ msgstr "Vrsta MIME" + +#~ msgid "Default Name" +#~ msgstr "Privzeto ime" diff --git a/po/sq/akonadi_knut_resource.po b/po/sq/akonadi_knut_resource.po new file mode 100644 index 0000000..bd8dd5e --- /dev/null +++ b/po/sq/akonadi_knut_resource.po @@ -0,0 +1,92 @@ +# Albanian translation for kdepim-runtime +# Copyright (c) 2009 Rosetta Contributors and Canonical Ltd 2009 +# This file is distributed under the same license as the kdepim-runtime package. +# FIRST AUTHOR , 2009. +# +msgid "" +msgstr "" +"Project-Id-Version: kdepim-runtime\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2009-08-13 12:39+0000\n" +"Last-Translator: Vilson Gjeci \n" +"Language-Team: Albanian \n" +"Language: sq\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2011-04-22 06:17+0000\n" +"X-Generator: Launchpad (build 12883)\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Nuk u përzgjodh asnjë skedar të dhënash." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Skedari '%1' u ngarkua me sukses." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Zgjidh Skedarin e të Dhënave" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Skedari i të Dhënave Akonadi Knut" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "" + +#~ msgid "Path to the Knut data file." +#~ msgstr "Shtegu për tek skedari i të dhënave Knut." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Mos i ndrysho të dhënat aktuale backend." diff --git a/po/sr/akonadi_knut_resource.po b/po/sr/akonadi_knut_resource.po new file mode 100644 index 0000000..0a928ef --- /dev/null +++ b/po/sr/akonadi_knut_resource.po @@ -0,0 +1,85 @@ +# Translation of akonadi_knut_resource.po into Serbian. +# Chusslove Illich , 2011. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-06-11 02:14+0200\n" +"PO-Revision-Date: 2011-08-20 23:55+0200\n" +"Last-Translator: Chusslove Illich \n" +"Language-Team: Serbian \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" +"X-Environment: kde\n" + +#: knutresource.cpp:78 +#, kde-format +msgid "No data file selected." +msgstr "Није изабран ниједан фајл са подацима." + +#: knutresource.cpp:95 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Фајл „%1“ је успешно учитан." + +#: knutresource.cpp:121 +#, kde-format +msgid "Select Data File" +msgstr "Избор фајла са подацима" + +#: knutresource.cpp:122 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Аконадијев КНУТ фајл са подацима" + +#: knutresource.cpp:164 knutresource.cpp:184 knutresource.cpp:337 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Није нађена ниједна ставка за удаљени ИД %1." + +#: knutresource.cpp:202 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Родитељска збирка није нађена у ДОМ стаблу." + +#: knutresource.cpp:210 +#, kde-format +msgid "Unable to write collection." +msgstr "Не могу да упишем збирку." + +#: knutresource.cpp:222 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Измењена збирка није нађена у ДОМ стаблу." + +#: knutresource.cpp:252 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Обрисана збирка није нађена у ДОМ стаблу." + +#: knutresource.cpp:266 knutresource.cpp:324 knutresource.cpp:331 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Родитељска збирка „%1“ није нађена у ДОМ стаблу." + +#: knutresource.cpp:274 knutresource.cpp:344 +#, kde-format +msgid "Unable to write item." +msgstr "Не могу да упишем ставку." + +#: knutresource.cpp:288 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Измењена ставка није нађена у ДОМ стаблу." + +#: knutresource.cpp:303 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Обрисана ставка није нађена у ДОМ стаблу." diff --git a/po/sr/libakonadi5.po b/po/sr/libakonadi5.po new file mode 100644 index 0000000..8b6f538 --- /dev/null +++ b/po/sr/libakonadi5.po @@ -0,0 +1,2645 @@ +# Translation of libakonadi5.po into Serbian. +# Chusslove Illich , 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017. +# Dalibor Djuric , 2009, 2010, 2011. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi5\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2020-06-11 02:14+0200\n" +"PO-Revision-Date: 2017-12-17 18:01+0100\n" +"Last-Translator: Chusslove Illich \n" +"Language-Team: Serbian \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" +"X-Environment: kde\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Часлав Илић" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "caslav.ilic@gmx.net" + +#: agentbase/accountsintegration.cpp:99 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:117 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:387 agentbase/preprocessorbase_p.cpp:42 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Не могу да региструјем објекат на д‑бусу: %1" + +#: agentbase/agentbase.cpp:470 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 типа %2" + +#: agentbase/agentbase.cpp:920 +#, kde-format +msgid "Agent identifier" +msgstr "Идентификатор агента" + +#: agentbase/agentbase.cpp:927 +#, kde-format +msgid "Akonadi Agent" +msgstr "Аконадијев агент" + +#: agentbase/agentbase_p.h:64 agentbase/resourcescheduler.cpp:294 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Спреман." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Ван везе." + +#: agentbase/agentbase_p.h:71 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Синхронизујем..." + +#: agentbase/agentbase_p.h:76 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Грешка." + +#: agentbase/agentbase_p.h:81 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Није подешено" + +#: agentbase/resourcebase.cpp:535 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Идентификатор ресурса" + +#: agentbase/resourcebase.cpp:542 +#, kde-format +msgid "Akonadi Resource" +msgstr "Аконадијев ресурс" + +#: agentbase/resourcebase.cpp:590 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Добављена лоша ставка" + +#: agentbase/resourcebase.cpp:613 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Грешка при стварању ставке: %1" + +#: agentbase/resourcebase.cpp:637 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Грешка при ажурирању збирке: %1" + +#: agentbase/resourcebase.cpp:723 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Пропало ажурирање локалне збирке: %1." + +#: agentbase/resourcebase.cpp:728 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Пропало ажурирање локалних ставки: %1." + +#: agentbase/resourcebase.cpp:747 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Не могу ван везе дохватити ставку." + +#: agentbase/resourcebase.cpp:930 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Синхронизујем фасциклу „%1“" + +#: agentbase/resourcebase.cpp:951 agentbase/resourcebase.cpp:958 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Не могу да добавим збирку за синхронизовање." + +#: agentbase/resourcebase.cpp:998 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Не могу да добавим збирку за синхронизовање атрибута." + +#: agentbase/resourcebase.cpp:1049 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Захтевана ставка више не постоји." + +#: agentbase/resourcescheduler.cpp:510 agentbase/resourcescheduler.cpp:516 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Посао отказан." + +#: core/collectionpathresolver.cpp:113 core/collectionpathresolver.cpp:132 +#, kde-format +msgid "No such collection." +msgstr "Нема такве збирке." + +#: core/collectionsync.cpp:521 core/collectionsync.cpp:659 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Нађене су неразрешене збирке сирочићи." + +#: core/conflicthandler.cpp:66 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Није нађена друга ставка за обраду сукоба." + +#: core/jobs/agentinstancecreatejob.cpp:85 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Не могу да приступим д‑бус сучељу створеног агента." + +#: core/jobs/agentinstancecreatejob.cpp:109 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Истекло време за стварање примерка агента." + +#: core/jobs/agentinstancecreatejob.cpp:170 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Не могу да добавим тип агента „%1“." + +#: core/jobs/agentinstancecreatejob.cpp:178 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Не могу да направим примерак агента." + +#: core/jobs/collectionattributessynchronizationjob.cpp:88 +#, kde-format +msgid "Invalid collection instance." +msgstr "Лош примерак збирке." + +#: core/jobs/collectionattributessynchronizationjob.cpp:95 +#: core/jobs/resourcesynchronizationjob.cpp:103 +#, kde-format +msgid "Invalid resource instance." +msgstr "Лош примерак ресурса." + +#: core/jobs/collectionattributessynchronizationjob.cpp:116 +#: core/jobs/resourcesynchronizationjob.cpp:129 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Не могу да приступим д‑бус сучељу за ресурс „%1“." + +#: core/jobs/collectionattributessynchronizationjob.cpp:139 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Истекло време за синхронизацију атрибута збирке." + +#: core/jobs/collectioncopyjob.cpp:68 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Лоша збирка за копирање" + +#: core/jobs/collectioncopyjob.cpp:74 +#, kde-format +msgid "Invalid destination collection" +msgstr "Лоша одредишна збирка" + +#: core/jobs/collectioncreatejob.cpp:64 +#, kde-format +msgid "Invalid parent" +msgstr "Лош родитељ" + +#: core/jobs/collectioncreatejob.cpp:112 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Не могу да рашчланим збирку из одговора" + +#: core/jobs/collectiondeletejob.cpp:66 +#, kde-format +msgid "Invalid collection" +msgstr "Лоша збирка" + +#: core/jobs/collectionfetchjob.cpp:223 +#, kde-format +msgid "Invalid collection given." +msgstr "Дата је лоша збирка." + +#: core/jobs/collectionmovejob.cpp:66 core/jobs/itemmovejob.cpp:104 +#, kde-format +msgid "No objects specified for moving" +msgstr "Нису задати објекти за премештање" + +#: core/jobs/collectionmovejob.cpp:73 core/jobs/itemmovejob.cpp:111 +#: core/jobs/linkjobimpl_p.h:53 +#, kde-format +msgid "No valid destination specified" +msgstr "Није наведено добро одредиште" + +#: core/jobs/invalidatecachejob.cpp:72 +#, kde-format +msgid "Invalid collection." +msgstr "Лоша збирка." + +#: core/jobs/itemcreatejob.cpp:121 +#, kde-format +msgid "Invalid parent collection" +msgstr "Лоша родитељска збирка" + +#: core/jobs/job.cpp:345 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Не могу да се повежем са сервисом Аконадија." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Несагласна верзија протокола на серверу Аконадија. Морате инсталирати " +"сагласну." + +#: core/jobs/job.cpp:351 +#, kde-format +msgid "User canceled operation." +msgstr "Корисник отказа поступак." + +#: core/jobs/job.cpp:356 +#, kde-format +msgid "Unknown error." +msgstr "Непозната грешка." + +#: core/jobs/job.cpp:389 +#, kde-format +msgid "Unexpected response" +msgstr "Неочекиван одзив." + +#: core/jobs/relationcreatejob.cpp:55 core/jobs/relationdeletejob.cpp:55 +#, kde-format +msgid "Failed to create relation." +msgstr "Не могу да направим релацију." + +#: core/jobs/resourcesynchronizationjob.cpp:155 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Истекло време за синхронизацију ресурса." + +#: core/jobs/specialcollectionshelperjobs.cpp:161 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Не могу да добавим корену збирку ресурса „%1“." + +#: core/jobs/specialcollectionshelperjobs.cpp:208 +#, kde-format +msgid "No resource ID given." +msgstr "ИД ресурса није нађен." + +#: core/jobs/specialcollectionshelperjobs.cpp:340 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Лош идентификатор ресурса „%1“." + +#: core/jobs/specialcollectionshelperjobs.cpp:357 +#: core/jobs/specialcollectionshelperjobs.cpp:365 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Не могу да подесим подразумевани ресурс преко д‑буса." + +#: core/jobs/specialcollectionshelperjobs.cpp:426 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Не могу да добавим збирку ресурса." + +#: core/jobs/specialcollectionshelperjobs.cpp:612 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Прековреме при покушају закључавања." + +#: core/jobs/tagcreatejob.cpp:62 +#, kde-format +msgid "Failed to create tag." +msgstr "Не могу да направим ознаку." + +#: core/jobs/trashjob.cpp:166 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Премештање у збирку смећа није успело, обустављам поступак бацања." + +#: core/jobs/trashjob.cpp:221 core/jobs/trashrestorejob.cpp:159 +#, kde-format +msgid "Invalid items passed" +msgstr "Прослеђене лоше ставке" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:206 +#, kde-format +msgid "Invalid collection passed" +msgstr "Прослеђена лоша збирка" + +#: core/jobs/trashjob.cpp:367 core/jobs/trashrestorejob.cpp:330 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Нема добре збирке или празан списак ставки." + +#: core/jobs/trashrestorejob.cpp:105 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Не могу да нађем збирку враћања и ресурс враћања није доступан." + +#: core/models/agentinstancemodel.cpp:193 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "име" + +#: core/models/entitytreemodel.cpp:214 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Учитавам..." + +#: core/models/entitytreemodel.cpp:509 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@window:title" +msgid "Error" +msgstr "Грешка" + +#: core/models/entitytreemodel.cpp:510 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Циљна збирка „%1“ већ садржи\n" +"збирку по имену „%2“." + +#: core/models/entitytreemodel.cpp:683 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "име" + +#: core/models/entitytreemodel_p.cpp:1360 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Не могу да копирам ставку:" + +#: core/models/entitytreemodel_p.cpp:1362 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Не могу да копирам збирку:" + +#: core/models/entitytreemodel_p.cpp:1364 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Не могу да преместим ставку:" + +#: core/models/entitytreemodel_p.cpp:1366 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Не могу да преместим збирку:" + +#: core/models/entitytreemodel_p.cpp:1368 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Не могу да повежем ентитет:" + +#: core/models/entitytreemodel_p.cpp:1370 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@title:window" +msgid "Error" +msgstr "Грешка" + +#: core/models/favoritecollectionsmodel.cpp:418 +#, kde-format +msgid "Favorite Folders" +msgstr "Омиљене фасцикле" + +# >> @label +# >! Put together: "...: %1%" +#: core/models/statisticsproxymodel.cpp:102 +#, kde-format +msgid "Total Messages" +msgstr "Укупно порука" + +# >> @label +# >! Put together: "...: %1%" +#: core/models/statisticsproxymodel.cpp:103 +#, kde-format +msgid "Unread Messages" +msgstr "Непрочитаних порука" + +# >> @label +# >! Put together: "...: %1%" +#: core/models/statisticsproxymodel.cpp:114 +#, kde-format +msgid "Quota" +msgstr "Квота" + +# >> @label +# >! Put together: "...: %1%" +#: core/models/statisticsproxymodel.cpp:123 +#, kde-format +msgid "Storage Size" +msgstr "Складишна величина" + +# >> @label +# >! Put together: "...: %1%" +#: core/models/statisticsproxymodel.cpp:131 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Складишна величина потфасцикле" + +# >> @title:column +#: core/models/statisticsproxymodel.cpp:247 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "непрочитано" + +# >> @title:column +#: core/models/statisticsproxymodel.cpp:248 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "укупно" + +# >> @title:column +#: core/models/statisticsproxymodel.cpp:249 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "величина" + +#: core/models/tagmodel.cpp:80 +#, kde-format +msgid "Tag" +msgstr "Ознака" + +#: core/partfetcher.cpp:63 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Не могу да добавим ставку за индекс" + +#: core/partfetcher.cpp:78 +#, kde-format +msgid "Index is no longer available" +msgstr "Индекс више није доступан" + +#: core/partfetcher.cpp:131 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Товарни део „%1“ није доступан за овај индекс" + +#: core/partfetcher.cpp:140 +#, kde-format +msgid "No session available for this index" +msgstr "Сесија није доступна за овај индекс" + +#: core/partfetcher.cpp:149 +#, kde-format +msgid "No item available for this index" +msgstr "Ставка није доступна за овај индекс" + +#: core/pluginloader.cpp:158 +#, kde-format +msgid "Unnamed plugin" +msgstr "неименовани прикључак" + +#: core/pluginloader.cpp:164 +#, kde-format +msgid "No description available" +msgstr "Нема описа" + +#: core/servermanager.cpp:254 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Верзија протокола сервера Аконадија разликује се од верзије коју користи " +"овај програм.\n" +"Ако сте недавно ажурирали систем, одјавите се и поново се пријавите да би " +"сви програми почели да користе праву верзију протокола." + +#: core/servermanager.cpp:271 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "Нема ниједног агента Аконадија. Проверите инсталацију КДЕ‑ПИМ‑а." + +#: core/session.cpp:181 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Несагласне верзије протокола. Верзија на серверу (%1) старија је од наше " +"(%2). Ако сте недавно ажурирали систем, поново покрените сервер Аконадија." + +#: core/session.cpp:187 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Несагласне верзије протокола. Верзија на серверу (%1) новија је од наше " +"(%2). Ако сте недавно ажурирали систем, поново покрените све програме из " +"КДЕ‑ПИМ‑а." + +#: selftest/main.cpp:32 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Самопроба Аконадија" + +#: selftest/main.cpp:34 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Проверава и извештава о стању сервера Аконадија" + +# |, no-check-markup +#: selftest/main.cpp:36 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008, Фолкер Краусе " + +#: widgets/agentactionmanager.cpp:51 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Нови примерак агента..." + +#: widgets/agentactionmanager.cpp:55 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Обриши примерак агента" + +#: widgets/agentactionmanager.cpp:59 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Подеси примерак агента" + +#: widgets/agentactionmanager.cpp:84 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Нови примерак агента" + +#: widgets/agentactionmanager.cpp:88 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Не могу да направим примерак агента: %1" + +#: widgets/agentactionmanager.cpp:92 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Неуспело стварање примерка агента" + +#: widgets/agentactionmanager.cpp:96 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Обрисати примерак агента?" + +#: widgets/agentactionmanager.cpp:100 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Желите ли заиста да обришете изабрани примерак агента?" + +#: widgets/agentconfigurationdialog.cpp:69 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:100 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:101 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:110 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:117 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:119 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to register %1 configuration dialog." +msgstr "Не могу да направим релацију." + +#: widgets/cachepolicypage.cpp:54 widgets/cachepolicypage.cpp:59 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "минут" +msgstr[1] "минута" +msgstr[2] "минута" +msgstr[3] "минут" + +#: widgets/cachepolicypage.cpp:76 +#, kde-format +msgid "Retrieval" +msgstr "Добављање" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Користи опције родитељске фасцикле или налога" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Синхронизуј фасциклу када се изабере" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Аутоматски синхронизуј сваких:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "никад" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "минута" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Локално кеширани делови" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Опције добављања" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Увек добављај &пуне поруке" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Добављај &тела порука на захтев" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Држи тела порука локално:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "заувек" + +#: widgets/collectiondialog.cpp:67 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Тражи" + +#: widgets/collectiondialog.cpp:75 +#, kde-format +msgid "Use folder by default" +msgstr "Подразумевано користи фасциклу" + +#: widgets/collectiondialog.cpp:231 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Нова потфасцикла..." + +#: widgets/collectiondialog.cpp:233 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Направи нову потфасциклу унутар тренутно изабране фасцикле" + +#: widgets/collectiondialog.cpp:267 widgets/standardactionmanager.cpp:233 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Нова фасцикла" + +#: widgets/collectiondialog.cpp:268 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Име" + +#: widgets/collectiondialog.cpp:287 widgets/standardactionmanager.cpp:239 +#, kde-format +msgid "Folder creation failed" +msgstr "Неуспело стварање фасцикле" + +#: widgets/collectiondialog.cpp:288 widgets/standardactionmanager.cpp:237 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Не могу да направим фасциклу: %1" + +#: widgets/collectiongeneralpropertiespage.cpp:40 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Опште" + +#: widgets/collectiongeneralpropertiespage.cpp:67 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 објекат" +msgstr[1] "%1 објекта" +msgstr[2] "%1 објеката" +msgstr[3] "1 објекат" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Име:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Посебна иконица:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "фасцикла" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Статистика" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Садржај:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "без објеката" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Величина:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 бајтова" + +#: widgets/collectionmaintenancepage.cpp:59 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Имајте на уму да индексирање може потрајати неколико минута." + +#: widgets/collectionmaintenancepage.cpp:84 +#, kde-format +msgid "Maintenance" +msgstr "Одржавање" + +#: widgets/collectionmaintenancepage.cpp:147 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Грешка при добављању броја индексираних ставки" + +#: widgets/collectionmaintenancepage.cpp:150 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Индексирана %1 ставка у овој фасцикли" +msgstr[1] "Индексиране %1 ставке у овој фасцикли" +msgstr[2] "Индексирано %1 ставки у овој фасцикли" +msgstr[3] "Индексирана %1 ставка у овој фасцикли" + +#: widgets/collectionmaintenancepage.cpp:156 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Рачунам индексиране ставке..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Фајлови" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Тип фасцикле:" + +# >! Contexts. +# >> @item +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "непознато" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Ставке" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Укупно ставки:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Непрочитаних ставки:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Индексирање" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Индексирање пуног текста" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Добављам број индексираних ставки..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Поново индексирај фасциклу" + +#: widgets/collectionrequester.cpp:123 +#, kde-format +msgid "No Folder" +msgstr "нема фасцикле" + +#: widgets/collectionrequester.cpp:132 +#, kde-format +msgid "Open collection dialog" +msgstr "Отвори дијалог збирке" + +# >> @title:window +#: widgets/collectionrequester.cpp:149 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Избор збирке" + +#: widgets/collectionview.cpp:229 +#, kde-format +msgid "&Move here" +msgstr "&Премести овде" + +#: widgets/collectionview.cpp:230 +#, kde-format +msgid "&Copy here" +msgstr "&Копирај овде" + +#: widgets/collectionview.cpp:232 +#, kde-format +msgid "Cancel" +msgstr "Одустани" + +# >> @item +#: widgets/conflictresolvedialog.cpp:142 +#, kde-format +msgid "Modification Time" +msgstr "време измене" + +# >> @item +#: widgets/conflictresolvedialog.cpp:151 +#, kde-format +msgid "Flags" +msgstr "заставице" + +# >> @item +#: widgets/conflictresolvedialog.cpp:163 widgets/conflictresolvedialog.cpp:170 +#: widgets/conflictresolvedialog.cpp:179 +#, kde-format +msgid "Attribute: %1" +msgstr "атрибут: %1" + +#: widgets/conflictresolvedialog.cpp:190 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Разрешење сукоба" + +#: widgets/conflictresolvedialog.cpp:196 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Узми моју верзију" + +#: widgets/conflictresolvedialog.cpp:202 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Узми туђу верзију" + +#: widgets/conflictresolvedialog.cpp:208 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Задржи обе верзије" + +#: widgets/conflictresolvedialog.cpp:220 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"

Ваше измене су у сукобу са онима које је неко други направио у " +"међувремену. У случају да не можете просто да одбаците једну од верзија, " +"мораћете ручно да их уклопите.

Кликните на " +"\"Отвори уређивач текста\" да задржите копију текстова, затим изаберите " +"ону која више одговара, поново је отворите и додајте јој оно што недостаје." + +# >> @item +#: widgets/conflictresolvedialog.cpp:269 +#, kde-format +msgid "Data" +msgstr "подаци" + +#: widgets/controlgui.cpp:243 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Покрећем сервер Аконадија..." + +#: widgets/controlgui.cpp:249 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Заустављам сервер Аконадија..." + +#: widgets/dragdropmanager.cpp:230 +#, kde-format +msgid "&Move Here" +msgstr "&Премести овде" + +#: widgets/dragdropmanager.cpp:236 +#, kde-format +msgid "&Copy Here" +msgstr "&Копирај овде" + +#: widgets/dragdropmanager.cpp:242 +#, kde-format +msgid "&Link Here" +msgstr "По&вежи овде" + +#: widgets/dragdropmanager.cpp:246 +#, kde-format +msgid "C&ancel" +msgstr "&Одустани" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Не могу да се повежем са сервисом управљања личним подацима.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:246 widgets/erroroverlay.cpp:247 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Покрећем сервис за управљање личним подацима..." + +#: widgets/erroroverlay.cpp:251 widgets/erroroverlay.cpp:252 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Гасим сервис за управљање личним подацима..." + +#: widgets/erroroverlay.cpp:256 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Сервис за управљање личним подацима надограђује базу..." + +#: widgets/erroroverlay.cpp:257 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Сервис за управљање личним подацима надограђује базу.\n" +"До овога долази после ажурирања софтвера и неопходно је ради оптимизације " +"перформанси.\n" +"У зависности од количине личних података, ово може потрајати неколико минута." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Аконади, сервис за управљање личним подацима, није у погону. Овај програм се " +"не може користити без њега." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Покрени" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Аконади, радни оквир за управљање личним подацима, није оперативан.\n" +"Кликните на „Детаљи“ за детаљне информације о овом проблему." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Аконади, сервис за управљање личним подацима, није оперативан." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Детаљи..." + +#: widgets/manageaccountwidget.cpp:208 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Желите ли да уклоните налог „%1“?" + +#: widgets/manageaccountwidget.cpp:209 +#, kde-format +msgid "Remove account?" +msgstr "Уклонити налог?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Долазни налози (додајте бар један):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Додај..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Измени..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Уклони" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Покрени поново" + +#: widgets/recentcollectionaction.cpp:42 +#, kde-format +msgid "Recent Folder" +msgstr "Недавна фасцикла" + +# >> @title:window Rename favorite folder +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Преименовање омиљене" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "Име:" + +#: widgets/selftestdialog.cpp:73 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Самопроба сервера Аконадија" + +#: widgets/selftestdialog.cpp:84 +#, kde-format +msgid "Save Report..." +msgstr "Сачувај извештај..." + +#: widgets/selftestdialog.cpp:86 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Копирај извештај у клипборд" + +#: widgets/selftestdialog.cpp:202 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Текућа постава сервера Аконадија захтева драјвер КуТ‑СКуЛ‑а „%1“; нађен је " +"на систему." + +#: widgets/selftestdialog.cpp:204 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Текућа постава сервера Аконадија захтева драјвер КуТ‑СКуЛ‑а „%1“.\n" +"Инсталирани су следећи драјвери: %2.\n" +"Побрините се да и захтевани буде инсталиран." + +#: widgets/selftestdialog.cpp:211 +#, kde-format +msgid "Database driver found." +msgstr "Драјвер базе података нађен." + +#: widgets/selftestdialog.cpp:213 +#, kde-format +msgid "Database driver not found." +msgstr "Драјвер базе података није нађен." + +#: widgets/selftestdialog.cpp:221 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Извршни фајл сервера МајСКуЛ‑а није испробан." + +#: widgets/selftestdialog.cpp:222 widgets/selftestdialog.cpp:263 +#: widgets/selftestdialog.cpp:312 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Тренутна постава не захтева унутрашњи сервер МајСКуЛ‑а." + +#: widgets/selftestdialog.cpp:229 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Тренутно сте подесили Аконади да користи сервер МајСКуЛ‑а „%1“.\n" +"Побрините се да је заиста инсталиран, поставите исправно путању и проверите " +"да ли имате неопходна права читања и извршавања за извршни фајл сервера. " +"Обично се зове mysqld, а тачна локација зависи од дистрибуције." + +#: widgets/selftestdialog.cpp:236 +#, kde-format +msgid "MySQL server not found." +msgstr "Сервер МајСКуЛ‑а није нађен." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server not readable." +msgstr "Сервер МајСКуЛ‑а није читљив." + +#: widgets/selftestdialog.cpp:240 +#, kde-format +msgid "MySQL server not executable." +msgstr "Сервер МајСКуЛ‑а није извршив." + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "МајСКуЛ нађен с неочекиваним именом." + +#: widgets/selftestdialog.cpp:244 +#, kde-format +msgid "MySQL server found." +msgstr "Сервер МајСКуЛ‑а нађен." + +#: widgets/selftestdialog.cpp:250 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Сервер МајСКуЛ‑а нађен: %1" + +#: widgets/selftestdialog.cpp:251 +#, kde-format +msgid "MySQL server is executable." +msgstr "Сервер МајСКуЛ‑а је извршан." + +#: widgets/selftestdialog.cpp:253 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "Извршавање сервера МајСКуЛ‑а „%1“ пропало, са следећом грешком: „%2“" + +#: widgets/selftestdialog.cpp:255 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Извршавање сервера МајСКуЛ‑а пропало." + +#: widgets/selftestdialog.cpp:262 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Дневник грешака сервера МајСКуЛ‑а није испробан." + +#: widgets/selftestdialog.cpp:271 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Нема текућег дневника грешака МајСКуЛ‑а." + +#: widgets/selftestdialog.cpp:272 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Сервер МајСКуЛ‑а није пријавио ниједну грешку током овог покретања. Дневник " +"се налази у ‘%1’." + +#: widgets/selftestdialog.cpp:277 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Дневник грешака МајСКуЛ‑а није читљив." + +#: widgets/selftestdialog.cpp:278 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Фајл дневник грешака сервера МајСКуЛ‑а је нађен, али није читљив: %1" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "Дневник сервера МајСКуЛ‑а садржи грешке." + +#: widgets/selftestdialog.cpp:287 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "Фајл дневника грешака сервера МајСКуЛ‑а ‘%1’ садржи грешке." + +#: widgets/selftestdialog.cpp:296 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "Дневник сервера МајСКуЛ‑а садржи упозорења." + +#: widgets/selftestdialog.cpp:297 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "Фајл дневника сервера МајСКуЛ‑а ‘%1’ садржи упозорења." + +#: widgets/selftestdialog.cpp:299 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "Дневник сервера МајСКуЛ‑а не садржи грешке." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "Фајл дневника сервера МајСКуЛ‑а ‘%1’ не садржи ни грешке ни упозорења." + +#: widgets/selftestdialog.cpp:311 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Постава сервера МајСКуЛ‑а није испробана." + +#: widgets/selftestdialog.cpp:320 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Подразумевана постава сервера МајСКуЛ‑а нађена." + +#: widgets/selftestdialog.cpp:321 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Подразумевана постава за сервер МајСКуЛ‑а нађена је и читљива код ‘%1’." + +#: widgets/selftestdialog.cpp:325 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Подразумевана постава сервера МајСКуЛ‑а није нађена." + +#: widgets/selftestdialog.cpp:326 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Подразумевана постава за сервер МајСКуЛ‑а није нађена или није читљива. " +"Проверите да ли је инсталација Аконадија потпуна, и да ли имате неопходна " +"права приступа." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Посебна постава сервера МајСКуЛ‑а није доступна." + +#: widgets/selftestdialog.cpp:334 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "Посебна постава за сервер МајСКуЛ‑а није нађена, али је опциона." + +#: widgets/selftestdialog.cpp:336 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Посебна постава сервера МајСКуЛ‑а нађена." + +#: widgets/selftestdialog.cpp:337 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Посебна постава за сервер МајСКуЛ‑а нађена је и читљива код ‘%1’." + +#: widgets/selftestdialog.cpp:341 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Посебна постава сервера МајСКуЛ‑а није читљива." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Посебна постава за сервер МајСКуЛ‑а нађена је код ‘%1’, али није читљива. " +"Проверите права приступа." + +#: widgets/selftestdialog.cpp:349 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "Постава сервера МајСКуЛ‑а није нађена или није читљива." + +#: widgets/selftestdialog.cpp:350 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "Постава за сервер МајСКуЛ‑а или није нађена или није читљива." + +#: widgets/selftestdialog.cpp:352 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Постава сервера МајСКуЛ‑а употребљива." + +#: widgets/selftestdialog.cpp:353 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "Постава сервера МајСКуЛ‑а нађена је код ‘%1’ и читљива је." + +#: widgets/selftestdialog.cpp:382 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Не могу да се повежем са сервером ПостгреСКуЛ‑а." + +#: widgets/selftestdialog.cpp:384 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Сервер ПостгреСКуЛ‑а нађен." + +#: widgets/selftestdialog.cpp:385 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Сервер ПостгреСКуЛ‑а је нађен и веза ради." + +#: widgets/selftestdialog.cpp:394 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl није нађена" + +#: widgets/selftestdialog.cpp:395 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Наредба akonadictl мора бити доступна у путањи. Проверите да ли је сервер " +"Аконадија инсталиран." + +#: widgets/selftestdialog.cpp:401 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl нађена и употребљива" + +#: widgets/selftestdialog.cpp:402 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Наредба %1, за управљање сервером Аконадија, нађена је и успешно извршена.\n" +"Резултат:\n" +"%2" + +#: widgets/selftestdialog.cpp:405 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl нађена али неупотребљива" + +#: widgets/selftestdialog.cpp:406 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Наредба %1, за управљање сервером Аконадија, нађена је али није могла бити " +"успешно извршена.\n" +"Резултат:\n" +"%2\n" +"Проверите да ли је сервер Аконадија исправно инсталиран." + +#: widgets/selftestdialog.cpp:415 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Управљачки процес Аконадија регистрован на д‑бусу." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Управљачки процес Аконадија регистрован је на д‑бусу, што обично значи да је " +"оперативан." + +#: widgets/selftestdialog.cpp:418 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Управљачки процес Аконадија није регистрован на д‑бусу." + +#: widgets/selftestdialog.cpp:419 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Управљачки процес Аконадија није регистрован на д‑бусу, што обично значи или " +"да није покренут, или да је на покретању дошло до кобне грешке." + +#: widgets/selftestdialog.cpp:424 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Серверски процес Аконадија регистрован на д‑бусу" + +#: widgets/selftestdialog.cpp:425 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Серверски процес Аконадија регистрован је на д‑бусу, што обично значи да је " +"оперативан." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Серверски процес Аконадија није регистрован на д‑бусу." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Серверски процес Аконадија није регистрован на д‑бусу, што обично значи или " +"да није покренут, или да је на покретању дошло до кобне грешке." + +#: widgets/selftestdialog.cpp:436 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Није могуће проверити верзију протокола." + +#: widgets/selftestdialog.cpp:437 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Без везе са сервером није могуће проверити да ли верзија протокола испуњава " +"захтеве." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Верзија протокола сервера превише стара." + +#: widgets/selftestdialog.cpp:442 widgets/selftestdialog.cpp:448 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Верзија протокола сервера је %1, али клијент захтева верзију %2. Ако сте " +"недавно ажурирали КДЕ ПИМ‑а, поново покрените и Аконади и ПИМ програме." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Верзија протокола сервера превише нова." + +#: widgets/selftestdialog.cpp:453 +#, kde-format +msgid "Server protocol version matches." +msgstr "Верзија протокола сервера одговара." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Тренутна верзија протокола је %1." + +#: widgets/selftestdialog.cpp:473 +#, kde-format +msgid "Resource agents found." +msgstr "Агенти ресурса нађени." + +#: widgets/selftestdialog.cpp:473 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Нађен је бар један агент ресурса." + +#: widgets/selftestdialog.cpp:475 +#, kde-format +msgid "No resource agents found." +msgstr "Агенти ресурса нису нађени." + +#: widgets/selftestdialog.cpp:476 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Ниједан агент ресурса није нађен, а Аконади се не може користити без бар " +"једног. Ово обично значи или да агенти ресурса нису инсталирани или да " +"постоји проблем у постави. Претражене су следеће путање: „%1“. Променљива " +"окружења $XDG_DATA_DIRS постављена је на „%2“, проверите укључује ли ово све " +"путање где су инсталирани агенти Аконадија." + +#: widgets/selftestdialog.cpp:493 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Текући дневник грешака сервера Аконадија није нађен." + +#: widgets/selftestdialog.cpp:494 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Сервер Аконадија није пријавио ниједну грешку током текућег покретања." + +#: widgets/selftestdialog.cpp:496 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Текући дневник грешака сервера Аконадија нађен." + +#: widgets/selftestdialog.cpp:497 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Сервер Аконадија пријавио је грешке током текућег покретања. Дневник се " +"налази у ‘%1’." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Претходни дневник грешака сервера Аконадија нађен." + +#: widgets/selftestdialog.cpp:505 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Сервер Аконадија није пријавио ниједну грешку током претходног покретања." + +#: widgets/selftestdialog.cpp:507 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Претходни дневник грешака сервера Аконадија нађен." + +#: widgets/selftestdialog.cpp:508 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Сервер Аконадија пријавио је грешке при претходном покретању. Дневник се " +"налази у ‘%1’." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Текући дневник грешака управљања Аконадија није нађен." + +#: widgets/selftestdialog.cpp:519 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Управљачки процес Аконадија није пријавио ниједну грешку при текућем " +"покретању." + +#: widgets/selftestdialog.cpp:521 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Текући дневник грешака управљања Аконадија нађен." + +#: widgets/selftestdialog.cpp:522 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Управљачки процес Аконадија пријавио је грешке током текућег покретања. " +"Дневник се налази у ‘%1’." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Претходни дневник грешака управљања Аконадија нађен." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Управљачки процес Аконадија није пријавио ниједну грешку током претходног " +"покретања." + +#: widgets/selftestdialog.cpp:532 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Претходни дневник грешака управљања Аконадија нађен." + +#: widgets/selftestdialog.cpp:533 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Управљачки процес Аконадија пријавио је грешке током претходног покретања. " +"Дневник се налази у ‘%1’." + +#: widgets/selftestdialog.cpp:542 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Аконади покренут под кореном" + +#: widgets/selftestdialog.cpp:542 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Извршавање програма окренутих Интернету под кореном (администраторским " +"налогом) излаже вас многим безбедносним ризицима. МајСКуЛ, који користи ова " +"инсталација Аконадија, неће допустити извршавање под кореном да би вас " +"заштитио од ових ризика." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Аконади не ради под кореном" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Аконади не ради под кореним (администраторским) корисником, што је и " +"препоручена постава за безбедност система." + +# >> @title:window +#: widgets/selftestdialog.cpp:623 +#, kde-format +msgid "Save Test Report" +msgstr "Уписивање извештаја о пробама" + +#: widgets/selftestdialog.cpp:630 +#, kde-format +msgid "Error" +msgstr "Грешка" + +#: widgets/selftestdialog.cpp:630 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Не могу да отворим фајл ‘%1’" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Дошло је до грешке при покретању сервера Аконадија. Следеће самопробе " +"требало би да помогну у откривању и решавању овог проблема. Када тражите " +"подршку или пријављујете грешке, молимо вас да увек укључите овај извештај." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Детаљи" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Још савета за претресање проблема потражите на userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:84 +#, kde-format +msgid "&New Folder..." +msgstr "&Нова фасцикла..." + +# >> New folder +#: widgets/standardactionmanager.cpp:84 +#, kde-format +msgid "New" +msgstr "Нова" + +#: widgets/standardactionmanager.cpp:86 widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Обриши %1 фасциклу" +msgstr[1] "&Обриши %1 фасцикле" +msgstr[2] "&Обриши %1 фасцикли" +msgstr[3] "&Обриши фасциклу" + +#: widgets/standardactionmanager.cpp:86 +#, kde-format +msgid "Delete" +msgstr "Обриши" + +#: widgets/standardactionmanager.cpp:87 widgets/standardactionmanager.cpp:207 +#, kde-format +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Синхронизуј %1 фасциклу" +msgstr[1] "&Синхронизуј %1 фасцикле" +msgstr[2] "&Синхронизуј %1 фасцикли" +msgstr[3] "&Синхронизуј фасциклу" + +#: widgets/standardactionmanager.cpp:87 widgets/standardactionmanager.cpp:105 +#: widgets/standardactionmanager.cpp:121 +#, kde-format +msgid "Synchronize" +msgstr "Синхронизуј" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "&Својства фасцикле" + +#: widgets/standardactionmanager.cpp:88 widgets/standardactionmanager.cpp:104 +#, kde-format +msgid "Properties" +msgstr "Својства" + +#: widgets/standardactionmanager.cpp:90 +#, kde-format +msgid "&Paste" +msgstr "&Налепи" + +#: widgets/standardactionmanager.cpp:90 +#, kde-format +msgid "Paste" +msgstr "Налепи" + +#: widgets/standardactionmanager.cpp:92 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Управљај локалним &претплатама..." + +#: widgets/standardactionmanager.cpp:92 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Управљај локалним претплатама" + +#: widgets/standardactionmanager.cpp:93 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Додај у омиљене фасцикле" + +#: widgets/standardactionmanager.cpp:93 +#, kde-format +msgid "Add to Favorite" +msgstr "Додај у омиљене" + +#: widgets/standardactionmanager.cpp:94 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Уклони из омиљених фасцикли" + +#: widgets/standardactionmanager.cpp:94 +#, kde-format +msgid "Remove from Favorite" +msgstr "Уклони из омиљених" + +#: widgets/standardactionmanager.cpp:95 +#, kde-format +msgid "Rename Favorite..." +msgstr "Преименуј омиљену..." + +#: widgets/standardactionmanager.cpp:95 +#, kde-format +msgid "Rename" +msgstr "Преименуј" + +#: widgets/standardactionmanager.cpp:96 widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Copy Folder To..." +msgstr "Копирај фасциклу у..." + +#: widgets/standardactionmanager.cpp:96 widgets/standardactionmanager.cpp:97 +#: widgets/standardactionmanager.cpp:107 widgets/standardactionmanager.cpp:109 +#, kde-format +msgid "Copy To" +msgstr "Копирај у" + +#: widgets/standardactionmanager.cpp:97 widgets/standardactionmanager.cpp:109 +#, kde-format +msgid "Copy Item To..." +msgstr "Копирај ставку у..." + +#: widgets/standardactionmanager.cpp:98 widgets/standardactionmanager.cpp:110 +#, kde-format +msgid "Move Item To..." +msgstr "Премести ставку у..." + +#: widgets/standardactionmanager.cpp:98 widgets/standardactionmanager.cpp:99 +#: widgets/standardactionmanager.cpp:108 widgets/standardactionmanager.cpp:110 +#, kde-format +msgid "Move To" +msgstr "Премести у" + +#: widgets/standardactionmanager.cpp:99 widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Move Folder To..." +msgstr "Премести фасциклу у..." + +#: widgets/standardactionmanager.cpp:100 widgets/standardactionmanager.cpp:199 +#, kde-format +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "&Исеци %1 ставку" +msgstr[1] "&Исеци %1 ставке" +msgstr[2] "&Исеци %1 ставки" +msgstr[3] "&Исеци ставку" + +#: widgets/standardactionmanager.cpp:100 widgets/standardactionmanager.cpp:101 +#, kde-format +msgid "Cut" +msgstr "Исеци" + +#: widgets/standardactionmanager.cpp:101 widgets/standardactionmanager.cpp:201 +#, kde-format +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "&Исеци %1 фасциклу" +msgstr[1] "&Исеци %1 фасцикле" +msgstr[2] "&Исеци %1 фасцикли" +msgstr[3] "&Исеци фасциклу" + +#: widgets/standardactionmanager.cpp:102 +#, kde-format +msgid "Create Resource" +msgstr "Направи ресурс" + +#: widgets/standardactionmanager.cpp:103 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Обриши %1 ресурс" +msgstr[1] "Обриши %1 ресурса" +msgstr[2] "Обриши %1 ресурса" +msgstr[3] "Обриши ресурс" + +#: widgets/standardactionmanager.cpp:104 +#, kde-format +msgid "&Resource Properties" +msgstr "Својства &ресурса" + +#: widgets/standardactionmanager.cpp:105 widgets/standardactionmanager.cpp:230 +#, kde-format +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Синхронизуј %1 ресурс" +msgstr[1] "Синхронизуј %1 ресурса" +msgstr[2] "Синхронизуј %1 ресурса" +msgstr[3] "Синхронизуј ресурс" + +#: widgets/standardactionmanager.cpp:106 +#, kde-format +msgid "Work Offline" +msgstr "Ради ван везе" + +#: widgets/standardactionmanager.cpp:111 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Синхронизуј фасциклу рекурзивно" + +#: widgets/standardactionmanager.cpp:111 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Синхронизуј рекурзивно" + +#: widgets/standardactionmanager.cpp:112 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Премести фасциклу у смеће" + +#: widgets/standardactionmanager.cpp:112 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Премести фасциклу у смеће" + +#: widgets/standardactionmanager.cpp:113 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Премести ставку у смеће" + +#: widgets/standardactionmanager.cpp:113 +#, kde-format +msgid "Move Item To Trash" +msgstr "Премести ставку у смеће" + +#: widgets/standardactionmanager.cpp:114 widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Врати фасциклу из смећа" + +#: widgets/standardactionmanager.cpp:114 widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Врати фасциклу из смећа" + +#: widgets/standardactionmanager.cpp:115 widgets/standardactionmanager.cpp:118 +#: widgets/standardactionmanager.cpp:119 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Врати ставку из смећа" + +#: widgets/standardactionmanager.cpp:115 widgets/standardactionmanager.cpp:118 +#: widgets/standardactionmanager.cpp:119 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Врати ставку из смећа" + +#: widgets/standardactionmanager.cpp:117 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Врати збирку из смећа" + +#: widgets/standardactionmanager.cpp:117 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Врати збирку из смећа" + +#: widgets/standardactionmanager.cpp:120 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Синхронизуј омиљене фасцикле" + +#: widgets/standardactionmanager.cpp:120 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Синхронизуј омиљене фасцикле" + +#: widgets/standardactionmanager.cpp:121 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Синхронизуј стабло фасцикли" + +#: widgets/standardactionmanager.cpp:195 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Копирај %1 фасциклу" +msgstr[1] "&Копирај %1 фасцикле" +msgstr[2] "&Копирај %1 фасцикли" +msgstr[3] "&Копирај фасциклу" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Копирај %1 ставку" +msgstr[1] "&Копирај %1 ставке" +msgstr[2] "&Копирај %1 ставки" +msgstr[3] "&Копирај ставку" + +#: widgets/standardactionmanager.cpp:203 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Обриши %1 ставку" +msgstr[1] "&Обриши %1 ставке" +msgstr[2] "&Обриши %1 ставки" +msgstr[3] "&Обриши ставку" + +#: widgets/standardactionmanager.cpp:209 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Обриши %1 ресурс" +msgstr[1] "&Обриши %1 ресурса" +msgstr[2] "&Обриши %1 ресурса" +msgstr[3] "&Обриши ресурс" + +#: widgets/standardactionmanager.cpp:211 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Синхронизуј %1 ресурс" +msgstr[1] "&Синхронизуј %1 ресурса" +msgstr[2] "&Синхронизуј %1 ресурса" +msgstr[3] "&Синхронизуј ресурс" + +#: widgets/standardactionmanager.cpp:214 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Копирај %1 фасциклу" +msgstr[1] "Копирај %1 фасцикле" +msgstr[2] "Копирај %1 фасцикли" +msgstr[3] "Копирај фасциклу" + +#: widgets/standardactionmanager.cpp:216 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Копирај %1 ставку" +msgstr[1] "Копирај %1 ставке" +msgstr[2] "Копирај %1 ставки" +msgstr[3] "Копирај ставку" + +#: widgets/standardactionmanager.cpp:218 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Исеци %1 ставку" +msgstr[1] "Исеци %1 ставке" +msgstr[2] "Исеци %1 ставки" +msgstr[3] "Исеци ставку" + +#: widgets/standardactionmanager.cpp:220 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Исеци %1 фасциклу" +msgstr[1] "Исеци %1 фасцикле" +msgstr[2] "Исеци %1 фасцикли" +msgstr[3] "Исеци фасциклу" + +#: widgets/standardactionmanager.cpp:222 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Обриши %1 ставку" +msgstr[1] "Обриши %1 ставке" +msgstr[2] "Обриши %1 ставки" +msgstr[3] "Обриши ставку" + +#: widgets/standardactionmanager.cpp:224 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Обриши %1 фасциклу" +msgstr[1] "Обриши %1 фасцикле" +msgstr[2] "Обриши %1 фасцикли" +msgstr[3] "Обриши фасциклу" + +#: widgets/standardactionmanager.cpp:226 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Синхронизуј %1 фасциклу" +msgstr[1] "Синхронизуј %1 фасцикле" +msgstr[2] "Синхронизуј %1 фасцикли" +msgstr[3] "Синхронизуј фасциклу" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Име" + +#: widgets/standardactionmanager.cpp:242 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Желите ли заиста да обришете %1 фасциклу и све њихове потфасцикле?" +msgstr[1] "Желите ли заиста да обришете %1 фасцикле и све њихове потфасцикле?" +msgstr[2] "Желите ли заиста да обришете %1 фасцикли и све њихове потфасцикле?" +msgstr[3] "Желите ли заиста да обришете ову фасциклу и све њене потфасцикле?" + +#: widgets/standardactionmanager.cpp:245 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Обрисати фасцикле?" +msgstr[1] "Обрисати фасцикле?" +msgstr[2] "Обрисати фасцикле?" +msgstr[3] "Обрисати фасциклу?" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Не могу да обришем фасциклу: %1" + +#: widgets/standardactionmanager.cpp:249 +#, kde-format +msgid "Folder deletion failed" +msgstr "Неуспело брисање фасцикле" + +#: widgets/standardactionmanager.cpp:252 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Својства фасцикле %1" + +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Желите ли заиста да обришете %1 изабрану ставку?" +msgstr[1] "Желите ли заиста да обришете %1 изабране ставке?" +msgstr[2] "Желите ли заиста да обришете %1 изабраних ставки?" +msgstr[3] "Желите ли заиста да обришете изабрану ставку?" + +#: widgets/standardactionmanager.cpp:258 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Обрисати ставке?" +msgstr[1] "Обрисати ставке?" +msgstr[2] "Обрисати ставке?" +msgstr[3] "Обрисати ставку?" + +#: widgets/standardactionmanager.cpp:260 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Не могу да обришем ставку: %1" + +#: widgets/standardactionmanager.cpp:262 +#, kde-format +msgid "Item deletion failed" +msgstr "Неуспело брисање ставке" + +# >> @title:window Rename favorite folder +#: widgets/standardactionmanager.cpp:265 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Преименовање омиљене" + +#: widgets/standardactionmanager.cpp:267 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Име:" + +#: widgets/standardactionmanager.cpp:270 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Нови ресурс" + +#: widgets/standardactionmanager.cpp:272 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Не могу да направим ресурс: %1" + +#: widgets/standardactionmanager.cpp:274 +#, kde-format +msgid "Resource creation failed" +msgstr "Неуспело стварање ресурса" + +#: widgets/standardactionmanager.cpp:277 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Желите ли заиста да обришете %1 ресурс?" +msgstr[1] "Желите ли заиста да обришете %1 ресурса?" +msgstr[2] "Желите ли заиста да обришете %1 ресурса?" +msgstr[3] "Желите ли заиста да обришете овај ресурс?" + +#: widgets/standardactionmanager.cpp:280 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Обрисати ресурсе?" +msgstr[1] "Обрисати ресурсе?" +msgstr[2] "Обрисати ресурсе?" +msgstr[3] "Обрисати ресурс?" + +#: widgets/standardactionmanager.cpp:283 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Не могу да налепим податке: %1" + +#: widgets/standardactionmanager.cpp:285 +#, kde-format +msgid "Paste failed" +msgstr "Неуспело налепљивање" + +#: widgets/standardactionmanager.cpp:633 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Име фасцикле не може да садржи „/“." + +#: widgets/standardactionmanager.cpp:634 widgets/standardactionmanager.cpp:641 +#, kde-format +msgid "Create new folder error" +msgstr "Грешка у стварању нове фасцикле" + +#: widgets/standardactionmanager.cpp:640 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Име фасцикле не може да почиње нити да се завршава тачком." + +#: widgets/standardactionmanager.cpp:864 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Пре синхронизовања фасцикле „%1“ неопходно је да ресурс буде на вези. Желите " +"ли да га ставите на везу?" + +#: widgets/standardactionmanager.cpp:865 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Налог „%1“ је ван везе" + +#: widgets/standardactionmanager.cpp:866 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "На везу" + +#: widgets/standardactionmanager.cpp:1442 +#, kde-format +msgid "Move to This Folder" +msgstr "Премести у ову фасциклу" + +#: widgets/standardactionmanager.cpp:1442 +#, kde-format +msgid "Copy to This Folder" +msgstr "Копирај у ову фасциклу" + +#: widgets/subscriptiondialog.cpp:103 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to update subscription: %1" +msgstr "Не могу да направим релацију." + +#: widgets/subscriptiondialog.cpp:104 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Локалне претплате" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Локалне претплате" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Тражи:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Само у претплаћеним" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Претплати се" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Откажи претплату" + +#: widgets/tageditwidget.cpp:128 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Не могу да направим нову ознаку." + +#: widgets/tageditwidget.cpp:129 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Грешка при стварању нове ознаке" + +#: widgets/tageditwidget.cpp:178 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Желите ли заиста да уклоните ознаку %1?" + +#: widgets/tageditwidget.cpp:180 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Брисање ознаке" + +#: widgets/tageditwidget.cpp:206 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Обриши ознаку" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "Подесите које ознаке треба применити." + +# >> @action:button +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "Направи нову ознаку" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Manage Tags" +msgid "Manage Tags" +msgstr "Управљање ознакама" + +#: widgets/tagselectioncombobox.cpp:139 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Брисање ознаке" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:58 +#, kde-format +msgid "Clear" +msgstr "Очисти" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, fuzzy, kde-format +#| msgid "Click to Add Tags" +msgid "Click to add tags" +msgstr "Кликните да додате ознаке" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:38 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Претварач из Аконадија у ИксМЛ" + +#: xml/akonadi2xml.cpp:40 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Претвара Аконадијево подстабло збирке у ИксМЛ фајл." + +# |, no-check-markup +#: xml/akonadi2xml.cpp:42 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© 2009, Фолкер Краусе " + +#: xml/xmldocument.cpp:95 +#, kde-format +msgid "No data loaded." +msgstr "Нема учитаних података." + +#: xml/xmldocument.cpp:134 +#, kde-format +msgid "No filename specified" +msgstr "Није наведено име фајла." + +#: xml/xmldocument.cpp:142 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Не могу да отворим фајл са подацима „%1“." + +#: xml/xmldocument.cpp:147 +#, kde-format +msgid "File %1 does not exist." +msgstr "Фајл „%1“ не постоји." + +#: xml/xmldocument.cpp:155 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Не могу рашчланим фајл са подацима „%1“." + +#: xml/xmldocument.cpp:162 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Дефиниција шеме не може да се учита и рашчлани." + +#: xml/xmldocument.cpp:167 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Не могу да направим контекст рашчлањивача шеме." + +#: xml/xmldocument.cpp:172 +#, kde-format +msgid "Unable to create schema." +msgstr "Не могу да направим шему." + +#: xml/xmldocument.cpp:177 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Не могу да направим контекст овере шемом." + +#: xml/xmldocument.cpp:182 +#, kde-format +msgid "Invalid file format." +msgstr "Лош формат фајла." + +#: xml/xmldocument.cpp:190 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Не могу да рашчланим фајл са подацима: %1" + +#: xml/xmldocument.cpp:315 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Не могу да нађем збирку „%1“." diff --git a/po/sv/akonadi_knut_resource.po b/po/sv/akonadi_knut_resource.po new file mode 100644 index 0000000..3a99b18 --- /dev/null +++ b/po/sv/akonadi_knut_resource.po @@ -0,0 +1,91 @@ +# translation of akonadi_knut_resource.po to Swedish +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Stefan Asserhäll , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-07-01 20:58+0200\n" +"Last-Translator: Stefan Asserhäll \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Ingen datafil vald." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Filen '%1' laddades med lyckat resultat." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Välj datafil" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut-datafil" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Inget objekt hittades för fjärridentifieraren %1" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "Överliggande samling hittades inte i DOM-träd." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Kunde inte skriva samling." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "Ändrad samling hittades inte i DOM-träd." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "Borttagen samling hittades inte i DOM-träd." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "Överliggande samling '%1' hittades inte i DOM-träd." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Kunde inte skriva objekt." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "Ändrat objekt hittades inte i DOM-träd." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "Borttaget objekt hittades inte i DOM-träd." + +#~ msgid "Path to the Knut data file." +#~ msgstr "Sökväg till Knut-datafilen." + +#~ msgid "Do not change the actual backend data." +#~ msgstr "Ändra inte gränssnittets verkliga data." diff --git a/po/sv/libakonadi5.po b/po/sv/libakonadi5.po new file mode 100644 index 0000000..ec442f1 --- /dev/null +++ b/po/sv/libakonadi5.po @@ -0,0 +1,2800 @@ +# translation of libakonadi.po to Swedish +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Stefan Asserhäll , 2007, 2008, 2009, 2010. +# Stefan Asserhall , 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 07:18+0100\n" +"Last-Translator: Stefan Asserhäll \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.08.1\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Stefan Asserhäll" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "stefan.asserhall@bredband.net" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "För närvarande finns inget konto inställt." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Kontointegrering stöds inte" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Kan inte registrera objektet via D-bus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 av typ %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Agent-identifierare" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi-agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Klar" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Nerkopplad" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Synkroniserar..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Fel." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Inte inställd" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Resursidentifierare" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi-resurs" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Ogiltigt objekt hämtades" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Fel när objekt skulle skapas: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Fel vid uppdatering av samling: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Uppdatering av lokal samling misslyckades: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Uppdatering av lokala objekt misslyckades: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Kan inte hämta objekt i nerkopplat läge." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Synkroniserar katalog '%1'" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Misslyckades hämta samlingen för synkronisering." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Misslyckades hämta samling för synkronisering av egenskaper" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Det begärda objektet finns inte längre" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Jobb avbrutet." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Samlingen finns inte." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Hittade övergivna samlingar utan upplösning" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Hittade inte något annat objekt för konflikthantering" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Kunde inte komma åt den skapade agentens D-Bus gränssnitt." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Tidsgräns överskreds när instans av agenten skulle skapas." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Kunde inte hämta agenttyp '%1'." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Kunde inte skapa instans av agenten." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Ogiltig samlingsinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Ogiltig resursinstans." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Kunde inte erhålla D-Bus gränssnitt för resursen '%1'" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Tidsgräns överskreds vid egenskapssynkronisering för samlingen." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Ogiltig samling att kopiera" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Ogiltig målsamling" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Ogiltig överliggande objekt" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Misslyckades tolka samling från svaret" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Ogiltig samling" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Ogiltig samling angiven." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Inga objekt angivna att flytta" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Inget giltigt mål angivet" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Ogiltig samling." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Ogiltig överliggande samling" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Kan inte ansluta till Akonadi-tjänsten." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi-serverns protokollversion är inte kompatibel. Försäkra dig om att du " +"har installerat en kompatibel version." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Användaren avbröt åtgärden." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Okänt fel." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Oväntat svar" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Misslyckades skapa relation." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Tidsgräns överskreds vid resurssynkronisering." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Kan inte lista rotsamlingen för resursen %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Ingen resursidentifikation angiven." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Ogiltig resursidentifierare '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Misslyckades anpassa förvald resurs via D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Misslyckades hämta resurssamlingen." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Tidsgräns överskriden vid försök att erhålla lås." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Misslyckades skapa etikett." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Flytta till papperskorgens samling misslyckades, avbryter åtgärden flytta " +"till papperskorgen" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Ogiltiga objekt skickades" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Ogiltig samling skickades" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Ingen giltig samling eller tom objektlista" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Kunde inte hitta återställningssamlingen och återställningsresursen är inte " +"tillgänglig" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Namn" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Laddar..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Fel" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"Målsamlingen '%1' innehåller redan en\n" +"samling som heter '%2'." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Namn" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Kunde inte kopiera objekt: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kunde inte kopiera samling: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Kunde inte flytta objekt: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Kunde inte flytta samling: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Kunde inte skapa länk till post: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Fel" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Favoritkorgar" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Totalt antal brev" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Olästa brev" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kvot" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Lagringsstorlek" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Underkatalogens lagringsstorlek" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Olästa" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Totalt" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Storlek" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etikett" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Kunde inte hämta objekt för index" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Index inte längre tillgängligt" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Nyttolastdelen '%1' är inte tillgänglig för detta index" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Ingen session tillgänglig för detta index" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Inget objekt tillgängligt för detta index" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Namnlöst insticksprogram" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Ingen beskrivning tillgänglig" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi-serverns protokollversion skiljer sig från protokollversionen som " +"används av programmet.\n" +"Om du nyligen har uppdaterat systemet, logga ut och tillbaka igen för att " +"försäkra dig om att alla program använder korrekt protokollversion." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Det finns inte några Akonadi-agenter tillgängliga. Verifiera installationen " +"av KDE PIM." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Protokollversioner motsvarar inte varandra. Serverversionen är äldre (%1) än " +"vår (%2). Om du nyligen uppdaterat systemet, starta då om Akonadi-servern." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Protokollversioner motsvarar inte varandra. Serverversionen är nyare (%1) än " +"vår (%2). Om du nyligen uppdaterat systemet, starta då om alla KDE PIM-" +"programmen." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi självtest" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Kontrollerar och rapporterar tillståndet hos Akonadi-servern" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Ny agentinstans..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "&Ta bort agentinstans" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Anpassa agentinstans" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Ny agentinstans" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Kunde inte skapa instans av agenten: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Misslyckades skapa instans av agenten" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Ta bort agentinstans?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Vill du verkligen ta bort den markerade instansen av agenten?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Inställning av %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Handbok %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Om %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Inställningsdialogrutan har öppnas i ett annat fönster" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Inställningen för %1 är redan öppnad någon annanstans." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Misslyckades registrera inställningsdialog %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minut" +msgstr[1] "minuter" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Hämtning" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Använd alternativ från överliggande korg eller konto" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Synkronisera när den här korgen markeras" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Synkronisera automatiskt efter:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Aldrig" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "minuter" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Lokala delar lagrade i cache" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Hämtningsalternativ" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Hä&mta alltid hela brev" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "Hämta b&revtexter på begäran" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Behåll brevtexter lokalt under:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "För alltid" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Sök" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Använd normalt korg" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Ny delkatalog..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Skapa en ny delkatalog i katalogen som för närvarande är markerad" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Ny katalog" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Namn" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Kunde inte skapa katalog: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Misslyckades skapa katalog" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Allmänt" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Ett objekt" +msgstr[1] "%1 objekt" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Namn:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "An&vänd egen ikon:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "katalog" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Statistik" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Innehåll:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 objekt" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Storlek:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 byte" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Kom ihåg att indexering kan ta några minuter." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Underhåll" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Fel när antal indexerade objekt skulle hämtas" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Indexerade %1 objekt i korgen" +msgstr[1] "Indexerade %1 objekt i korgen" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Beräknar indexerade objekt..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Filer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Korgtyp:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "okänd" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Objekt" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Totalt antal objekt:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Olästa objekt:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Indexerar" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Aktivera fullständig textindexering" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Hämtar antal indexerade objekt..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Indexera om korg" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Ingen korg" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Öppna samlingsdialogruta" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Välj en samling" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Flytta hit" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Kopiera hit" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Avbryt" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Ändringstid" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Flaggor" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Egenskap: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Konfliktupplösning" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Ta min version" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Ta deras version" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Behåll båda versioner" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Dina ändringar strider mot de som någon annan har gjort under tiden." +"
Om inte en version bara kan slängas, måste ändringarna integreras för " +"hand.
Klicka på Öppna texteditor för att " +"behålla en kopia av texterna, därefter välja vilken som är mest riktig, och " +"sedan åter öppna den så att den kan ändras igen för att lägga till det som " +"saknas." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Uppgifter" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Startar Akonadi-servern..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Stoppar Akonadi-servern..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Flytta hit" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Kopiera hit" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "&Länka hit" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Avbryt" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Kan inte ansluta till tjänsten för personlig informationshantering.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Tjänst för personlig informationshantering startas..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Tjänst för personlig informationshantering stängs av..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" +"Tjänst för personlig informationshantering utför en uppgradering av " +"databasen." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Tjänst för personlig informationshantering utför en uppgradering av " +"databasen.\n" +"Det sker efter en uppdatering av programvara och är nödvändig för att " +"optimera prestanda.\n" +"Beroende på mängden personlig information, kan det ta några minuter." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi-tjänsten för personlig informationshantering kör inte. Detta program " +"kan inte användas utan den." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Starta" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi-ramverket för personlig informationshantering är inte " +"funktionsdugligt.\n" +"Klicka på \"Detaljinformation...\" för att få detaljerad information om " +"problemet." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" +"Akonadi-tjänsten för personlig informationshantering är inte funktionsduglig." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Detaljinformation..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Vill du ta bort kontot '%1'?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Ta bort konto?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Inkommande konton (lägg till minst ett):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "Lä&gg till..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "Än&dra..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "&Ta bort" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Starta om" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Senaste katalog" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Byt namn på favorit" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Namn:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi-server självtest" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Spara rappport..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Kopiera rapport till klippbordet" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"QtSQL-drivrutinen '%1' krävs av den nuvarande inställningen av Akonadi-" +"servern, och hittades på systemet." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"QtSQL-drivrutinen '%1' krävs av den nuvarande inställningen av Akonadi-" +"servern.\n" +"Följande drivrutiner är installerade: %2.\n" +"Försäkra dig om att den nödvändiga drivrutinen är installerad." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Databasdrivrutin hittades." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Databasdrivrutin hittades inte." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL-serverns körbara fil inte testad." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Den nuvarande inställningen kräver inte en intern MySQL-server." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"För närvarande är Akonadi inställd att använda MySQL-servern '%1'.\n" +"Försäkra dig om att MySQL-servern är installerad, ställ in riktig sökväg och " +"försäkra dig om att du har nödvändiga läs- och körrättigheter för serverns " +"körbara fil. Serverns körbara fil kallas oftast 'mysqld', dess plats " +"varierar beroende på distribution." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL-server hittades inte." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL-server inte läsbar." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL-server inte körbar." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL hittades med oväntat namn." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL-server hittades." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL-server hittades: %1." + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL-server är körbar." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Körning av MySQL-servern '%1' misslyckades med följande felmeddelande: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Körning av MySQL-servern misslyckades." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL-serverns fellogg inte testad." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Någon aktuell MySQL fellogg hittades inte." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL-servern rapporterade inte några fel under den här starten. Loggen " +"finns i '%1'." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL-serverns fellogg inte läsbar." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "En MySQL-server felloggsfil hittades men är inte läsbar: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL-serverns logg innehåller fel." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL-serverns felloggfil '%1' innehåller fel." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL-serverns logg innehåller varningar." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL-serverns loggfil '%1' innehåller varningar." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL-serverns logg innehåller inte några fel." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL-serverns loggfil '%1' innehåller inte några fel eller varningar." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL-serverns inställning inte testad." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL-serverns standardinställning hittades." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "Standardinställningen för MySQL-servern hittades och är läsbar i %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL-serverns standardinställning hittades inte." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Standardinställningen för MySQL-servern hittades inte eller är inte läsbar. " +"Kontrollera att installationen av Akonadi är fullständig och att du har alla " +"åtkomsträttigheter som krävs." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Egen inställning av MySQL-servern inte tillgänglig." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "Egen inställning av MySQL-servern hittades inte men är valfri." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Egen inställning av MySQL-servern hittades." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "Den egna inställningen av MySQL-servern hittades i %1 och är läsbar" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Egen inställning av MySQL-servern inte läsbar." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Den egna inställningen av MySQL-servern hittades i %1 men är inte läsbar. " +"Kontrollera dina åtkomsträttigheter." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL-serverns inställning hittades inte eller är inte läsbar." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL-serverns inställning hittades inte eller är inte läsbar." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL-serverns inställning går att använda." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "MySQL-serverns inställning hittades i %1 och är läsbar." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Kan inte ansluta till PostgreSQL-servern." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL-server hittades." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL-servern hittades och anslutningen fungerar." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "Hittade inte akonadictl" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Programmet 'akonadictl' måste vara tillgängligt i sökvägen. Försäkra dig om " +"att du har installerat Akonadi-servern." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "Hittade användbar akonadictl" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Programmet '%1' för att styra Akonadi-servern hittades och kunde köras med " +"lyckat resultat.\n" +"Resultat:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "Hittade icke användbar akonadictl" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Programmet '%1' för att styra Akonadi-servern hittades men kunde inte köras " +"med lyckat resultat.\n" +"Resultat:\n" +"%2\n" +"Försäkra dig om att Akonadi-servern är riktigt installerad." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi-kontrollprocessen registrerad av D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-kontrollprocessen är registrerad av D-Bus, vilket typiskt indikerar " +"att den är operativ." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi-kontrollprocessen inte registrerad av D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-kontrollprocessen inte registrerad av D-Bus, vilket typiskt betyder " +"att den inte startades eller stötte på ett allvarligt fel under start." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi-serverprocessen registrerad av D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi-serverprocessen är registrerad av D-Bus, vilket typiskt indikerar " +"att den är operativ." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi-serverprocessen inte registrerad av D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi-serverprocessen inte registrerad av D-Bus, vilket typiskt betyder " +"att den inte startades eller stötte på ett allvarligt fel under start." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Kontroll av protokollversion inte möjlig." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Utan en anslutning till servern är det inte möjligt att kontrollera om " +"protokollversionen uppfyller kraven." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Serverns protokollversion är för gammal." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Serverns protokollversion är %1, men minst version %2 krävs av klienten. Om " +"du nyligen uppdaterat KDE PIM, försäkra dig om att du startar om både " +"Akonadi och KDE PIM-programmen." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Serverns protokollversion är för ny." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Serverns protokollversion matchar." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Aktuell protokollversion är %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Resursagenter hittades." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Minst en resursagent har hittats." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Några resursagenter hittades inte." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Några resursagenter hittades inte. Akonadi är inte användbart utan minst en. " +"Det betyder oftast att inga resursagenter har installerats eller att det " +"finns ett inställningsproblem. Följande sökvägar har sökts igenom: '%1'. " +"Miljövariabeln XDG_DATA_DIRS är inställd till '%2'. Försäkra dig om att det " +"omfattar alla sökvägar där Akonadi-agenter är installerade." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Någon aktuell Akonadi-server fellogg hittades inte." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi-servern rapporterade inte några fel under aktuell start." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Aktuell Akonadi-server fellogg hittades." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-servern rapporterade fel under aktuell start. Loggen finns i %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Någon föregående Akonadi-server fellogg hittades inte." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi-servern rapporterade inte några fel under föregående start." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Föregående Akonadi-server fellogg hittades." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi-servern rapporterade fel under föregående start. Loggen finns i %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Någon aktuell Akonadi-kontrollfellogg hittades inte." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Akonadi-kontrollprocessen rapporterade inte några fel under aktuell start." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Aktuell Akonadi-kontrollfellogg hittades." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi-kontrollprocessen rapporterade fel under aktuell start. Loggen finns " +"i %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Någon föregående Akonadi-kontrollfellogg hittades inte." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Akonadi-kontrollprocessen rapporterade inte några fel under föregående start." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Föregående Akonadi-kontrollfellogg hittades." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi-kontrollprocessen rapporterade fel under föregående start. Loggen " +"finns i %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi startades som systemadministratör" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Att köra program som vänder sig mot Internet som systemadministratör (root) " +"exponerar dig för många säkerhetsrisker. MySQL som används av den här " +"installationen av Akonadi, tillåter inte att det körs som " +"systemadministratör för att skydda dig mot dessa risker." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi kör inte som systemadministratör" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi kör inte som systemadministratör (root användare), vilket är den " +"rekommenderade inställningen för ett säkert system." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Spara testrapport" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Fel" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Kunde inte öppna filen '%1'" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Ett fel uppstod vid start av Akonadi-servern. Följande självtester är " +"avsedda att hjälpa till att spåra och lösa problemet. Vid begäran om support " +"eller felrapportering, inkludera alltid den här rapporten." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Detaljinformation" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

För fler felsökningstips hänvisas till userbase.kde.org/Akonadi.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Ny katalog..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Ny" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Ta bort katalog" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Ta bort" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Synkronisera katalog" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Synkronisera" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Katalog&egenskaper" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Egenskaper" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "K&listra in" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Klistra in" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Hantera lokala &prenumerationer..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Hantera lokala prenumerationer" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Lägg till i favoritkorgar" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Lägg till i favoriter" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Ta bort från favoritkorgar" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Ta bort från favoriter" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Byt namn på favorit..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Byt namn" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Kopiera korg till..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopiera till" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Kopiera objekt till..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Flytta objekt till..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Flytta till" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Flytta korg till..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Klipp u&t objekt" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Klipp ut" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Klipp u&t katalog" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Skapa resurs" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Ta bort resurs" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Resursegenskaper" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Synkronisera resurs" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Arbeta nerkopplad" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Synkronisera katalog rekursivt" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Synkronisera rekursivt" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Flytta katalog till papperskorgen" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Flytta katalog till papperskorgen" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Flytta objekt till papperskorgen" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Flytta objekt till papperskorgen" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Åte&rställ katalog från papperskorgen" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Återställ katalog från papperskorgen" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Åte&rställ objekt från papperskorgen" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Återställ objekt från papperskorgen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Åte&rställ samling från papperskorgen" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Återställ samling från papperskorgen" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Synkronisera favoritkataloger" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Synkronisera favoritkataloger" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Synkronisera katalogträd" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Kopiera katalog" +msgstr[1] "&Kopiera %1 kataloger" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Kopiera objekt" +msgstr[1] "&Kopiera %1 objekt" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Klipp u&t objekt" +msgstr[1] "Klipp u&t %1 objekt" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Klipp u&t katalog" +msgstr[1] "Klipp u&t %1 kataloger" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Ta bort objekt" +msgstr[1] "&Ta bort %1 objekt" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Ta bort katalog" +msgstr[1] "&Ta bort %1 kataloger" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Synkronisera katalog" +msgstr[1] "&Synkronisera %1 kataloger" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "&Ta bort resurs" +msgstr[1] "&Ta bort %1 resurser" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "&Synkronisera resurs" +msgstr[1] "&Synkronisera %1 resurser" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Kopiera katalog" +msgstr[1] "Kopiera %1 kataloger" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Kopiera objekt" +msgstr[1] "Kopiera %1 objekt" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Klipp ut objekt" +msgstr[1] "Klipp ut %1 objekt" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Klipp ut katalog" +msgstr[1] "Klipp ut %1 kataloger" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Ta bort objekt" +msgstr[1] "Ta bort %1 objekt" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Ta bort katalog" +msgstr[1] "Ta bort %1 kataloger" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Synkronisera katalog" +msgstr[1] "Synkronisera %1 kataloger" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Ta bort resurs" +msgstr[1] "Ta bort %1 resurser" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Synkronisera resurs" +msgstr[1] "Synkronisera %1 resurser" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Namn" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Vill du verkligen ta bort katalogen och alla dess underkataloger?" +msgstr[1] "" +"Vill du verkligen ta bort %1 kataloger och alla deras underkataloger?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Ta bort katalog?" +msgstr[1] "Ta bort kataloger?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Kunde inte ta bort katalog: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Misslyckades ta bort katalog" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Egenskaper för katalogen %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Vill du verkligen ta bort det markerade objektet?" +msgstr[1] "Vill du verkligen ta bort %1 markerade objekt?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Ta bort objekt?" +msgstr[1] "Ta bort objekt?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Kunde inte ta bort objekt: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Misslyckades ta bort objekt" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Byt namn på favorit" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Namn:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Ny resurs" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Kunde inte skapa resurs: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Misslyckades skapa resurs" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Vill du verkligen ta bort resursen?" +msgstr[1] "Vill du verkligen ta bort %1 resurser?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Ta bort resurs?" +msgstr[1] "Ta bort resurser?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Kunde inte klistra in data: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Misslyckades klistra in" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Det går inte att använda \"/\" i katalognamn." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Fel vid skapa ny katalog" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" +"Det går inte att lägga till \".\" i början eller slutet av katalognamn." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Innan katalogen \"%1\" kan synkroniseras måste resursen vara uppkopplad. " +"Vill du koppla upp den?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Kontot \"%1\" är nerkopplat" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Koppla upp" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Flytta till denna korg" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Kopiera till denna korg" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Misslyckades uppdatera prenumeration: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Prenumerationsfel" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Lokala prenumerationer" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Sök:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "Prenumererar &bara" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "&Prenumerera" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "Sl&uta prenumerera" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Misslyckades skapa ny etikett" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Ett fel uppstod när en ny etikett skulle skapas" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Vill du verkligen ta bort etiketten %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Ta bort etikett" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Ta bort etikett" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Välj etiketter som ska tillämpas." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Skapa ny etikett" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Hantera etiketter" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Välj etiketter..." + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Välj etiketter" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Rensa" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Klicka för att lägga till etiketter" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi till XML-konvertering" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Konverterar ett delträd i en Akonadi-samling till en XML-fil." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Ingen data inläst." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Ingen filnamn angivet" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Kan inte öppna datafilen '%1'." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Filen %1 finns inte." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Kan inte tolka datafilen '%1'." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Schema-definition kunde inte läsas in eller tolkas." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Kan inte skapa schema-tolkens sammanhang." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Kan inte skapa schema." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Kan inte skapa schema-valideringssammanhang." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Ogiltigt filformat." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Kan inte tolka datafilen: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Kan inte hitta samlingen %1" + +#~ msgid "Id" +#~ msgstr "Id" + +#~ msgid "Remote Id" +#~ msgstr "Fjärr-id" + +#~ msgid "MimeType" +#~ msgstr "Mime-typ" + +#~ msgid "Form" +#~ msgstr "Formulär" + +#~ msgid "Default Name" +#~ msgstr "Standardnamn" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Ta bort" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "Avbryt" + +#~ msgctxt "@action:button" +#~ msgid "Open text editor" +#~ msgstr "Öppna texteditor" + +#~ msgid "Take left one" +#~ msgstr "Ta den vänstra" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "Två uppdateringar har konflikter med varandra.Välj vilken eller " +#~ "vilka uppdateringar som ska utföras." + +#~ msgid "uknown" +#~ msgstr "okänd" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-resurs" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Oläst" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Totalt" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Storlek" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "Namn" + +#~ msgid "Invalid collection specified" +#~ msgstr "Ogiltig samling angiven" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "Protokollversion %1 hittades, förväntade minst %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Serverns protokollversion är tillräckligt aktuell." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Serverns protokollversion är %1, vilket är samma som eller nyare än " +#~ "version %2 som krävs." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Inkonsekvent lokalt samlingsträd detekterat." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Fjärrsamling utan rotavslutad kedja av överliggande objekt " +#~ "tillhandahållen, resursen är skadad." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE testprogram" + +#~ msgid "Cannot list root collection." +#~ msgstr "Kan inte lista rotsamlingen." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk-söktjänst registrerad av D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Nepomuk-söktjänsten är registrerad av D-Bus, vilket typiskt indikerar att " +#~ "den är operativ." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk-söktjänsten inte registrerad av D-Bus." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Nepomuk-söktjänsten inte registrerad av D-Bus, vilket typiskt betyder att " +#~ "den inte startades eller stötte på ett allvarligt fel under start." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuk-söktjänsten använder ett olämpligt bakgrundsprogram." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Nepomuk-söktjänsten använder bakgrundsprogrammet '%1', som inte är " +#~ "rekommenderat att användas med Akonadi." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuk-söktjänsten använder ett lämpligt bakgrundsprogram. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "" +#~ "Nepomuk-söktjänsten använder ett av de rekommenderade bakgrundsprogrammen." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "Insticksprogrammet \"%1\" är inte inbyggt statiskt (builtin static). Ange " +#~ "denna information i felrapporten." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Insticksprogram ej statiskt byggt" + +#~ msgid "Fetch Job Error" +#~ msgstr "Fel för hämtningsjobb" + +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "Ny katalog..." + +#~| msgid "&Resource Properties" +#~ msgid "Resource Properties" +#~ msgstr "Resursegenskaper" + +#~ msgid "Cache" +#~ msgstr "Cache" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Ärv cache-policy från överliggande objekt" + +#~ msgid "Cache Policy" +#~ msgstr "Cache-policy" + +#~ msgid "Interval check time:" +#~ msgstr "Tidskontrollintervall:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Tidsgräns för lokal cache:" + +#~ msgid "Synchronize on demand" +#~ msgstr "Synkronisera vid behov" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Hantera vilka kataloger du vill se i katalogträdet" + +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Sök" + +#~ msgid "Available Folders" +#~ msgstr "Tillgängliga kataloger" + +#~ msgid "Current Changes" +#~ msgstr "Aktuella ändringar" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Sluta prenumerera på markerad katalog" + +#~ msgid "Multiple Agent Deletion" +#~ msgstr "Borttagning av flera agenter" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Akonadi-servern rapporterade fel under start i %1." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "Akonadi-kontrollprocessen rapporterade fel under start i '%1'." + +#~ msgid "TODO" +#~ msgstr "ATT GÖRA" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi är inte funktionsdugligt.
Detaljinformation...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi-resurs" + +#~ msgid "Nepomuk search service uses Sesame2 backend. " +#~ msgstr "Nepomuk-söktjänsten använder bakgrundsprogrammet Sesame2" + +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "Klipp u&t samling" +#~ msgstr[1] "Klipp u&t %1 samlingar" + +#~ msgid "Copy failed" +#~ msgstr "Kopiering misslyckades" diff --git a/po/tr/akonadi_knut_resource.po b/po/tr/akonadi_knut_resource.po new file mode 100644 index 0000000..54b948c --- /dev/null +++ b/po/tr/akonadi_knut_resource.po @@ -0,0 +1,87 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# obsoleteman , 2009. +# Volkan Gezer , 2014, 2015. +# Kaan Ozdincer , 2014. +msgid "" +msgstr "" +"Project-Id-Version: kdepimlibs-kde4\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2015-02-09 11:20+0100\n" +"Last-Translator: Volkan Gezer \n" +"Language-Team: Turkish \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 1.5\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Hiçbir veri dosyası seçilmedi." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "'%1' dosyası başarılı bir şekilde yüklendi." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Veri Dosyası Seç" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut Veri Dosyası" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "%1 uzak kimliği için hiç öge bulunamadı" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM ağacında üst koleksiyon bulunamadı." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Koleksiyon yazılamadı." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM ağacında değiştirilmiş koleksiyon bulunamadı." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM ağacında silinmiş koleksiyon bulunamadı." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM ağacında üst koleksiyon '%1' bulunamadı." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Öge yazılamıyor." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM ağacında değiştirilmiş öge bulunamadı." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM ağacında silinmiş öge bulunamadı." diff --git a/po/tr/libakonadi5.po b/po/tr/libakonadi5.po new file mode 100644 index 0000000..ecd24da --- /dev/null +++ b/po/tr/libakonadi5.po @@ -0,0 +1,2839 @@ +# translation of libakonadi.po to +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Serdar Soytetir , 2008, 2009, 2012. +# Serhat Demirkol , 2009. +# H. İbrahim Güngör , 2010, 2011. +# Ozan Çağlayan , 2010. +# Volkan Gezer , 2013, 2014, 2015. +# Kaan Ozdincer , 2014. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2017-06-05 12:48+0000\n" +"Last-Translator: Mesutcan \n" +"Language-Team: Turkish \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 1.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Serdar Soytetir, Volkan Gezer" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "tulliana@gmail.com, volkangezer@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Nesne dbus'ta kaydedilemedi: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%2 tipindeki %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Temsilci tanımlayıcı" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Aracı" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Hazır" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Çevrimdışı" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Eşzamanlanıyor..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Hata." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Yapılandırılmamış" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Kaynak tanımlayıcı" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi Kaynağı" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Geçersiz öge alındı" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Öge oluştururken hata: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Yerel koleksiyon güncellenirken hata: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Yerel koleksiyon güncellenemedi: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Yerel ögeler güncellenemedi: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Çevrimdışı kipte ögeler alınamaz." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "'%1' dizini eşzamanlanıyor" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Eşleme için koleksiyon alınamadı." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Öznitelik eşlemesi için koleksiyon alınamadı." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "İstenilen öge artık yok" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Görev iptal edildi." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Böyle bir koleksiyon yok." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Çözümlenemeyen öksüz koleksiyonlar bulundu" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Çakışma çözümü için gerekli diğer öge bulunamadı" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Oluşturulan aracın D-Bus arayüzüne erişilemiyor." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Temsilci süreci oluşturma işlemi zaman aşımına uğradı." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Araç türü '%1' alınamıyor." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Temsil isteği oluşturulamıyor." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Geçersiz koleksiyon örneği." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Geçersiz kaynak isteği." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "'%1' kaynağı için D-Bus arayüzü edinilemedi" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Nitelik eşzamanlama koleksiyonu zaman aşımına uğradı." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Kopyalama için geçersiz koleksiyon" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Geçersiz hedef koleksiyonu" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Geçersiz üst öge" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Yanıttan alınan Koleksiyon ayrıştırılamadı" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Geçersiz koleksiyon" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Geçersiz koleksiyon girildi." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Taşımak için bir öge belirtilmedi" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Geçerli bir hedef belirtilmedi" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Geçersiz koleksiyon." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Geçersiz üst koleksiyon" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Akonadi servisine bağlanılamadı." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Akonadi sunucu protokolünün sürümü uyumsuz. Uygun sürümü yüklediğinizden " +"emin olun." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Kullanıcı tarafından iptal edilmiş işlem." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Bilinmeyen hata." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Beklenmedik yanıt" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "İlişki oluşturma başarısız oldu." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Eşzamanlama işlemi zaman aşımına uğradı." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "%1 kaynağının kök koleksiyonu alınamadı." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Hiç kaynak ID belirtilmedi." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Geçersiz kaynak tanımlayıcı '%1'" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "D-Bus aracılığıyla öntanımlı kaynak yapılandırması başarısız oldu." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Kaynak koleksiyonun alınması başarısız oldu." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Kilitleme işlemi zaman aşımına uğradı." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Etiket oluşturma başarısız." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "Koleksiyonu çöpe taşıma başarısız, silme işlemi durduruluyor" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Geçersiz ögeler geçirildi" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Geçersiz koleksiyon geçirildi" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Geçerli koleksiyon yok veya boş liste" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "Kurtarma koleksiyonu bulunamadı ve kurtarma kaynağı kullanılamıyor" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "İsim" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "Yükleniyor..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@window:title" +msgid "Error" +msgstr "Hata" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"'%1' hedef koleksiyonu '%2' adıyla\n" +"zaten mevcut." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "İsim" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Öge kopyalanamadı:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Kolleksiyon kopyalanamadı:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Öge taşınamadı:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Koleksiyon taşınamadı:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Varlık bağlanamadı:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgid "Error" +msgctxt "@title:window" +msgid "Error" +msgstr "Hata" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Yer İmi Dizinleri" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Toplam İleti Sayısı" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Okunmamış İletiler" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Kota" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Depolama Boyutu" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Alt Dizin Depolama Boyutu" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Okunmamış" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Toplam" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Boyut" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Etiket" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Bu öge dizin için getirilemedi" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Dizin artık kullanılabilir değil" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Bu dizin için '%1' veri kısmı mevcut değil." + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Bu dizin için bir oturum yok" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "Bu dizin için kullanılabilir öge yok" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "İsimsiz eklenti" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Açıklama yok" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akanadi sunucusunun protokol sürümü ile bu uygulama tarafından kullanılan " +"protokolün sürümü farklı.\n" +"Sisteminizi yeni güncellediyseniz, tüm uygulamaların doğru protokol sürümünü " +"kullandığından emin olmak için, çıkış yapıp tekrar giriş yapın." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Kullanılabilir Akonadi Aracı yok. Lütfen, KDE PIM kurulumunuzu kontrol edin." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Protokol sürümü eşleşmiyor. Sunucu sürümü (%1) bizimkinden (%2) daha eski. " +"Sisteminizi yeni güncellediyseniz Akonadi sunucusunu yeniden başlatın." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Protokol sürümü eşleşmiyor. Sunucu sürümü (%1) bizimkinden (%2) daha yeni. " +"Sisteminizi yeni güncellediyseniz tüm KDE PIM uygulamalarını yeniden " +"başlatın." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi Kendi Kendine Denetim" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi sunucusunu kontrol eder ve durumunu raporlar" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "&Yeni Araç Örneği..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "A&raç Örneğini Sil" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "Ara&ç Örneğini Yapılandır" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Yeni Araç Örneği" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Araç örneği oluşturulamadı: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Araç örneği oluşturma başarısız" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Araç Örneği Silinsin Mi?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Gerçekten seçili araç örneğini silmek istiyor musunuz?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to register %1 configuration dialog." +msgstr "İlişki oluşturma başarısız oldu." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "dakika" +msgstr[1] "dakika" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Alım" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Üst dizinin veya hesabın ayarlarını kullan" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Bu dizin seçildiğinde eşzamanla" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Otomatik olarak eşzamanla:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Asla" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "dakika" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Yerel Olarak Önbelleklenmiş Bölümler" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Alım Seçenekleri" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Her zaman tüm &iletileri getir" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "İleti gövdelerini istenildiği zaman &getir" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "İleti gövdelerini yerelde sakla:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Her Zaman" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Ara" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Klasörü öntanımlı olarak kullan" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "Ye&ni Alt Dizin..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Seçili dizinin altına yeni bir alt dizin oluştur" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Yeni Dizin" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "İsim" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Dizin oluşturulamadı: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Dizin oluşturma işlemi başarısız oldu" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Genel" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "Bir nesne" +msgstr[1] "%1 nesne" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&İsim:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "&Özel simge kullan:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "dizin" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "İstatistikler" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "İçerik:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 nesne" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Boyut:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 Bayt" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "İndekslemenin biraz zaman alacağını unutmayın." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Bakım" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "İndekslenmiş ögelerin sayısı alınırken hata oluştu" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Bu klasörde %1 indekslenmiş öge var" +msgstr[1] "Bu klasörde %1 indekslenmiş öge var" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "İndekslenmiş ögeler hesaplanıyor..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Dosyalar" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Klasör türü:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "bilinmeyen" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Ögeler" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Toplam Öge:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Okunmamış ögeler:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "İndeksleme" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Tam metin indekslemeyi etkinleştir" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "İndekslenmiş ogelerin sayısı alınıyor ..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Klasörü yeniden indeksle" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Dizin Yok" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Koleksiyon penceresini aç" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Koleksiyon seçin" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "Buraya &taşı" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "Buraya &kopyala" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "İptal" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Değiştirilme Zamanı" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Bayraklar" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Özellik: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Çakışma Çözümü" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "Sağdakini al" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "İkisini de sakla" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Veri" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi sunucusu başlatılıyor..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi sunucusu durduruluyor..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "Buraya &Taşı" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Buraya Kopyala" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "Bu&raya Bağ Koy" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "İ&ptal" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Kişisel bilgi yönetimi servisine bağlanılamıyor.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Kişisel bilgi yönetim servisi başlatılıyor..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Kişisel bilgi yönetim servisi kapatılıyor..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Kişisel bilgi yönetim servisi bir veritabanı yükseltmesi yapıyor." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Kişisel bilgi yönetim servisi bir veritabanı yükseltmesi yapıyor.\n" +"Bu bir yazılım güncellemesinden sonra yapılır ve başarımı eniyileştirmek " +"için gereklidir.\n" +"Kişisel bilginin miktarına bağlı olarak birkaç dakika sürebilir." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Akonadi kişisel bilgi yönetim servisi çalışmıyor. Bu uygulama Akonadi " +"olmadan kullanılamaz." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Başla" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi kişisel bilgi yönetimi çalışma ortamı kullanıma hazır değil.\n" +"Sorun hakkında ayrıntılı bilgi almak için \"Ayrıntılar...\" düğmesine " +"tıklayın." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi kişisel bilgi yönetimi servisi kullanılmaya hazır değil." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Ayrıntılar..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "'%1' hesabını kaldırma istiyor musunuz?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Hesabı kaldır?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Gelen hesaplar (en az bir tane ekleyin):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "E&kle..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Değiştir..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "K&aldır" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Yeniden Başlat" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Yakın Zamandaki Klasör" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "Yer İmini Yeniden Adlandır" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "İsim:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi Sunucusu Kendi Kendine Denetim" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Raporu Kaydet..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Raporu Panoya Kopyala" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"QtSQL sürücüsü '%1' geçerli Akonadi sunucu yapılandırması için gerekiyor " +"ancak sisteminizde bulunamadı." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"QtSQL sürücüsü '%1' geçerli Akonadi sunucu yapılandırması için gerekiyor.\n" +"Şu sürücüler yüklü: %2.\n" +"Gerekli sürücünün yüklü olduğundan emin olun." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Veritabanı sürücüsü bulundu." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Veritabanı sürücüsü bulunamadı." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL sunucusu çalıştırılabilir dosyası denenmedi." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "Geçerli yapılandırma bir iç MySQL sunucu gerektirmiyor." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Geçerli olarak Akonadi'yi MySQL sunucusu '%1' ile yapılandırmışsınız.\n" +"MySQL sunucusunun kurulu olduğundan, doğru yolun ayarlandığından ve sunucu " +"çalıştırılabilir dosyası üzerinde gerekli okuma ve çalıştırma haklarına " +"sahip olduğunuzdan emin olun. Sunucu çalıştırılabilir dosyası genel olarak " +"'mysqld' olarak adlandırılmıştır; konumu dağıtıma göre değişiklik " +"göstermektedir." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "MySQL sunucusu bulunamadı." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL sunucusu okunabilir değil." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL sunucusu çalıştırılabilir değil." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "MySQL beklenmeyen bir isimle bulundu." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "MySQL sunucusu bulundu." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "MySQL sunucusu bulundu: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL sunucusu çalıştırılabilir durumda." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "'%1' MySQL sunucusu şu hata iletisini vererek başarısız oldu: '%2'" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "MySQL sunucusunu çalıştırma işlemi başarısız oldu." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL sunucusunun hata günlük kaydı denenmedi." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Geçerli MySQL hata günlük kaydı bulunamadı." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"MySQL sunucusu bu başlangıçta bir hata bildirmedi. Günlük '%1' içerisinde " +"bulunabilir." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL sunucusu hata günlük kaydı okunabilir değil." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "Bir MySQL sunucusu hata kaydı bulundu ancak okunabilir değil: %1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL sunucu hata günlük kaydı hatalar içeriyor." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL sunucu hata günlük kaydı dosyası '%1' hatalar içeriyor." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL sunucusu günlük kaydı içerisinde uyarılar var." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL sunucusu günlük kaydı '%1' içerisinde uyarılar var." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL sunucusu günlük kaydı hiç hata içermiyor." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL sunucusu günlük kaydı '%1' hiç hata ya da uyarı içermiyor." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL sunucu yapılandırması denenmedi." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "MySQL sunucunun öntanımlı yapılandırması bulundu." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"MySQL öntanımlı sunucu yapılandırması %1 konumunda bulundu ve okunabilir " +"durumda." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "MySQL sunucunun öntanımlı yapılandırması bulunamadı." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"MySQL sunucusunun öntanımlı yapılandırması bulunamadı veya okunabilir değil. " +"Akonadi kurulumunuzun eksiksiz olduğunu ve tüm gerekli erişim haklarına " +"sahip olduğunuzu kontrol edin." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL sunucunun özel yapılandırması yok." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "MySQL sunucunun özel yapılandırması bulunamadı ancak isteğe bağlıdır." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "MySQL sunucunun özel yapılandırması bulundu." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"MySQL sunucusunun özel yapılandırması bulundu ve %1 konumunda okunabilir." + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL sunucunun özel yapılandırması okunabilir değil." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"MySQL sunucusunun özel yapılandırması %1 konumunda bulundu ancak okunabilir " +"değil. Erişim haklarınızı kontrol edin." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "MySQL sunucunun yapılandırması bulunamadı ya da okunabilir değil." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "MySQL sunucunun yapılandırması bulunamadı ya da okunabilir değil." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL sunucunun yapılandırması kullanılabilir durumda." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"MySQL sunucu yapılandırması %1 konumunda bulundu ve kullanılabilir durumda." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "PostgreSQL sunucusuna bağlanılamadı." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "PostgreSQL sunucusu bulundu." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL sunucusu bulundu ve bağlantı çalışıyor." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl bulunamadı" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"'akonadictl' uygulamasının $PATH içerisinden erişilebilir olması gerekiyor. " +"Akonadi sunucusunu yüklediğinizden emin olun." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl bulundu ve kullanılabilir durumda" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Akonadi sunucusunu kontrol etmek için '%1' programı bulundu ve başarılı bir " +"şekilde yürütülebildi.\n" +"Sonuç:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl bulundu ama kullanılabilir durumda değil" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Akonadi sunucusunu kontrol etmek için '%1' programı bulundu ve başarılı bir " +"şekilde yürütülemedi.\n" +"Sonuç:\n" +"%2\n" +"Akonadi sunucusunun doğru bir şekilde kurulduğundan emin olun." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi denetim süreci D-Bus'a kaydedildi." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi kontrol süreci, sıklıkla kontrol sürecinin işlevsel olduğunu " +"bildiren D-Bus'a kayıtlıdır." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi denetim süreci D-Bus'a kaydedilmedi." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi kontrol süreci, açılışta sıklıkla kontrol sürecinin başlatılmadığını " +"veya ölümcül bir hatayla karşılaştığını ifade eden D-Bus'a kayıtlı değildir." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi sunucu süreci D-Bus'a kaydedildi." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Akonadi sunucu süreci, sıklıkla sunucu sürecinin işlevsel olduğunu bildiren " +"D-Bus'a kayıtlıdır." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi sunucu süreci D-Bus'a kaydedilmedi." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi sunucu süreci, açılışta sıklıkla sunucu sürecinin başlatılmadığını " +"veya ölümcül bir hatayla karşılaştığını ifade eden D-Bus'a kayıtlı değildir." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Protokol sürüm denetimi mümkün değil." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Sunucuya bağlantı olmadan protokol sürümünün gereklilikleri karşılayıp " +"karşılamadığını kontrol etmek mümkün değildir." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Sunucu protokol sürümü çok eski." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Sunucu protokol sürümü %1, ancak en az %2 sürümü gerekli. Yakında KDE PIM'i " +"güncellediyseniz hem Akonadi hem de KDE PIM uygulamalarını yeniden başlatın." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Sunucu protokol sürümü çok yeni." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Sunucu protokol sürümü eşleşiyor." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Geçerli Protokol sürümü %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Kaynak aracı bulundu." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "En azından bir kaynak temsili bulunmuş olmalı." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Hiç kaynak aracı bulunamadı." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Kaynak aracı bulunamadı. Akonadi en az bir tanesi olmadan kullanılabilir " +"değildir. Bu, hiçbir kaynak aracının olmadığını veya bir kurulum problemi " +"olduğunu belirtir. Şu yollarda arama yapıldı: '%1'. XDG_DATA_DIRS ortam " +"değişkeni '%2' olarak ayarlanmış, bu değişkenin Akonadi araçlarının kurulu " +"olduğu tüm yolları dahil ettiğinden emin olun." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Geçerli Akonadi sunucusunun hata günlük kaydı yok." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi sunucusu geçerli açılışında hiç hata bildirmedi." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Geçerli Akonadi sunucusunun hata günlük kaydı bulundu." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Akonadi sunucusu mevcut başlatmada meydana gelen hataları raporladı. Kayıt " +"şurada bulunabilir: %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Önceki Akonadi sunucu hata kaydı bulunmadı." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi sunucusu önceki açılışında hiç hata bildirmedi." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Akonadi sunucusunun önceki hata günlük kaydı bulundu." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Akonadi sunucusu önceki başlatmada meydana gelen hataları raporladı. Kayıt " +"şurada bulunabilir: %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Geçerli Akonadi kontrol hata kaydı bulunmadı." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "Akonadi kontrol süreci geçerli açılışında hiç hata bildirmedi." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Geçerli Akonadi kontrol hata kaydı bulundu." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Akonadi kontrol süreci mevcut başlatmada meydana gelen hataları raporladı. " +"Kayıt şurada bulunabilir: %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Akonadi denetiminin önceki hata günlük kaydı bulunamadı." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "Akonadi kontrol süreci önceki açılışında hiç hata bildirmedi." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Akonadi denetiminin önceki hata günlük kaydı bulundu." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Akonadi kontrol süreci önceki başlatmada meydana gelen hataları raporladı. " +"Kayıt şurada bulunabilir: %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi root olarak başlatıldı" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"İnternet ile bağlantıda olan uygulamaları root/yönetici olarak çalıştırmak, " +"sizi çoğu güvenlik risklerine karşı savunmasız yapabilir. Bu Akonadi " +"kurulumu için kullanılan MySQL, sizi bu risklerden korumak için yönetici " +"olarak çalışmasına izin vermeyecek." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi root olarak çalışmıyor" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi root/yetkili kullanıcı haklarıyla çalışmıyor, ki güvenli bir sistem " +"için önerilen çalışma şekli budur." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Deneme Raporunu Kaydet" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Hata" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "'%1' dosyası açılamadı" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Akonadi sunucusu başlatılırken bir hata meydana geldi. Aşağıdaki testlerin " +"bu problemi takip etme ve çözmede yardımcı olacağı farz edilir. Destek talep " +"ederken veya hata bildirirken, lütfen her zaman bu raporu dahil edin." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Ayrıntılar" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Sorun giderme ile ilgili daha fazla ipucu için lütfen userbase.kde.org/Akonadi adresine bakın.

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "Ye&ni Dizin..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Yeni" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "Dizini &Sil" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Sil" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "&Dizini Eşzamanla" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Eşzamanla" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "Dizin &Özellikleri" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Özellikler" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Yapıştır" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Yapıştır" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Yerel &Üyelikleri Yönet..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Yerel Üyelikleri Yönet" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Yer İmi Dizinlerine Ekle" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Yer İmlerine Ekle" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Yer İmi Dizinlerinden Kaldır" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Yer İmlerinden Kaldır" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Yer İmini Yeniden Adlandır..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Yeniden Adlandır" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Dizini Buraya Kopyala..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Kopyala" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Ögeyi Buraya Kopyala..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Ögeyi Buraya Taşı..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Taşı" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Dizini Buraya Taşı..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "Ögeyi &Kes" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Kes" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "Dizini &Kes" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Kaynak Oluştur" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "Kaynağı Sil" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "&Kaynak Özellikleri" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "Kaynağı Eşzamanla" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Çevrimdışı Çalış" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Dizinleri Özyinelemeli Eşzamanla" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Özyinelemeli Eşzamanla" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "Dizini &Çöp Kutusuna Taşı" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Dizini Çöp Kutusuna Taşı" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "Ögeyi &Çöp Kutusuna Taşı" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Ögeyi Çöp Kutusuna Taşı" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "&Klasörü Çöpten Geri Al" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Klasörü Çöpten Geri Al" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "&Ögeyi Çöpten Geri Al" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Ögeyi Çöpten Geri Al" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "&Koleksiyonu Çöpten Geri Al" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Koleksiyonu Çöpten Geri Al" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Yer İmi Dizinlerini Eşzamanla" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Yer İmi Dizinlerini Eşzamanla" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Klasör Ağacını Eşzamanla" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "Dizini &Kopyala" +msgstr[1] "%1 Dizini &Kopyala" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "Öge &Kopyala" +msgstr[1] "%1 Öge &Kopyala" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Ögeyi &Kes" +msgstr[1] "%1 Ögeyi &Kes" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Dizini &Kes" +msgstr[1] "%1 Dizini &Kes" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "Öge &Sil" +msgstr[1] "%1 Öge &Sil" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "Dizini &Sil" +msgstr[1] "%1 Dizini &Sil" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Dizini Eşzamanla" +msgstr[1] "%1 &Dizini Eşzamanla" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Kaynağı &Sil" +msgstr[1] "%1 Kaynağı &Sil" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Kaynağı &Eşzamanla" +msgstr[1] "%1 Kaynağı &Eşzamanla" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Dizini Kopyala" +msgstr[1] "%1 Dizini Kopyala" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Ögeyi Kopyala" +msgstr[1] "%1 Ögeyi Kopyala" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Ögeyi Kes" +msgstr[1] "%1 Ögeyi Kes" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Dizini Kes" +msgstr[1] "%1 Dizini Kes" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Ögeyi Sil" +msgstr[1] "%1 Ögeyi Sil" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Dizini Sil" +msgstr[1] "%1 Dizini Sil" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Dizini Eşzamanla" +msgstr[1] "%1 Dizini Eşzamanla" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Kaynağı Sil" +msgstr[1] "%1 Kaynağı Sil" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Kaynağı Eşzamanla" +msgstr[1] "%1 Kaynağı Eşzamanla" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "İsim" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "" +"Bu dizini ve tüm alt dizinlerini silmek istediğinizden emin misiniz?" +msgstr[1] "" +"%1 dizini ve tüm alt dizinlerini silmek istediğinizden emin misiniz?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Dizin silinsin mi?" +msgstr[1] "Dizinler silinsin mi?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Dizin silinemedi: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Dizin silme işlemi başarısız oldu" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "%1 Dizininin Özellikleri" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Seçili ögeyi gerçekten silmek istediğinize emin misiniz?" +msgstr[1] "%1 ögeyi gerçekten silmek istediğinize emin misiniz?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Öge silinsin mi?" +msgstr[1] "Ögeler silinsin mi?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Öge silinemedi: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Öge silme işlemi başarısız" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Yer İmini Yeniden Adlandır" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "İsim:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Yeni Kaynak" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Kaynak oluşturulamadı: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Kaynak oluşturma işlemi başarısız" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Bu kaynağı silmek istediğinizden emin misiniz?" +msgstr[1] "%1 kaynağı silmek istediğinizden emin misiniz?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Kaynak Silinsin mi?" +msgstr[1] "Kaynaklar Silinsin mi?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Veri yapıştırılamadı: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Yapıştırma işlemi başarısız oldu" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Dizin adına \"/\" eklenemez." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Yeni dizin oluşturma hatası" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Dizin adının başına veya sonuna \".\" ekleyemeyiz." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"\"%1\" klasörünü eşzamanlamadan önce, kaynakların çevrimiçi olması " +"gereklidir. Çevrimiçi yapmak ister misiniz?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "\"%1\" hesabı çevrimdışı" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Çevrimiçi Ol" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Bu Dizine Taşı" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Bu Dizine Kopyala" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to update subscription: %1" +msgstr "İlişki oluşturma başarısız oldu." + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "Yerel Üyelikler" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "Yerel Üyelikler" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Ara:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "Sadece üye olunanlar" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "Abone ol" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "Üyelikten Ayrıl" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Yeni etiket oluşturma başarısız" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Etiket oluşturulurken bnir hata oluştu" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" +"%1 etiketini gerçekten silmek istediğinizden emin " +"misiniz?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Etiketi sil" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Etiketi sil" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "Hangi etiketlerin uygulanacağını yapılandır." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "Yeni etiket oluştur" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Manage Tags" +msgid "Manage Tags" +msgstr "Etiketleri Yönet" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Etiketi sil" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Temizle" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, fuzzy, kde-format +#| msgid "Click to Add Tags" +msgid "Click to add tags" +msgstr "Etiket Eklemek için Tıklayın" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi 'den XML 'e dönüştürücü" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Akonadi altağaç koleksiyonunu XML dosyasına dönüştür." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Hiçbir veri yüklenmedi." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Dosya adı belirtilmedi" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "'%1' veri dosyası açılamıyor." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "%1 dosyası yok." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "'%1' veri dosyası ayrıştırılamıyor." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Şema tanımı yüklenemedi ve ayrıştırılamadı." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Şema ayrıştırıcı bağlamı oluşturulamıyor." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Şema oluşturulamıyor." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Şema geçerleme bağlamı oluşturulamıyor." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Geçersiz dosya biçimi." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Veri dosyası %1 ayrıştırılamadı." + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "%1 koleksiyonu bulunamıyor" + +#~ msgid "Id" +#~ msgstr "Kimlik" + +#~ msgid "Remote Id" +#~ msgstr "Uzak Kimlik" + +#~ msgid "MimeType" +#~ msgstr "MimeTürü" + +#~ msgid "Default Name" +#~ msgstr "Öntanımlı İsim" + +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "Sil" + +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "İptal" + +#~ msgid "Take left one" +#~ msgstr "Soldaki al" + +#~ msgctxt "@label" +#~ msgid "" +#~ "Two updates conflict with each other.Please choose which update(s) " +#~ "to apply." +#~ msgstr "" +#~ "İki güncelleme birbirleriyle çakışıyorlar.Lütfen hangi güncellemenin " +#~ "uygulanacağını seçin." + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "Okunmamış" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "Toplam" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "Boyut" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi Kaynağı" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "İsim" + +#~ msgid "Invalid collection specified" +#~ msgstr "Geçersiz koleksiyon girildi" + +#~ msgid "Protocol version %1 found, expected at least %2" +#~ msgstr "%1 protokolü bulundu, beklenen ise en az %2" + +#~ msgid "Server protocol version is recent enough." +#~ msgstr "Sunucu protokol sürümü yeterince yeni." + +#~ msgid "" +#~ "The server Protocol version is %1, which equal or newer than the required " +#~ "version %2." +#~ msgstr "" +#~ "Sunucu Protokol sürümü %1, gerekli %2 sürümüne denk veya daha yeni bir " +#~ "sürüm." + +#~ msgid "Inconsistent local collection tree detected." +#~ msgstr "Tutarsız yerel koleksiyon ağacı belirlendi." + +#~ msgid "" +#~ "Remote collection without root-terminated ancestor chain provided, " +#~ "resource is broken." +#~ msgstr "" +#~ "Kök-sonlu ata zinciri olmayan uzak koleksiyon verildi, özkaynak kırık." + +#~ msgid "KDE Test Program" +#~ msgstr "KDE Sınama Programı" + +#~ msgid "Cannot list root collection." +#~ msgstr "Kök koleksiyon listelenemedi." + +#~ msgid "Nepomuk search service registered at D-Bus." +#~ msgstr "Nepomuk arama servisi D-Bus'a kaydedildi." + +#~ msgid "" +#~ "The Nepomuk search service is registered at D-Bus which typically " +#~ "indicates it is operational." +#~ msgstr "" +#~ "Nepomuk arama servisi D-Bus'a kayıtlı. Bu, servisin işlevsel olduğunu " +#~ "gösterir." + +#~ msgid "Nepomuk search service not registered at D-Bus." +#~ msgstr "Nepomuk arama servisi D-Bus'a kaydedilmedi." + +#~ msgid "" +#~ "The Nepomuk search service is not registered at D-Bus which typically " +#~ "means it was not started or encountered a fatal error during startup." +#~ msgstr "" +#~ "Nepomuk arama servisi D-Bus'a kayıtlı değil. Bu, tipik olarak servisin " +#~ "açılışta ölümcül bir hatayla karşılaştığı anlamına gelir." + +#~ msgid "Nepomuk search service uses inappropriate backend." +#~ msgstr "Nepomuk arama servisi uyumsuz arka uç kullanıyor." + +#~ msgid "" +#~ "The Nepomuk search service uses the '%1' backend, which is not " +#~ "recommended for use with Akonadi." +#~ msgstr "" +#~ "Nepomuk arama servisi Akonadi ile birlikte kullanılması önerilmeyen '%1' " +#~ "arka ucunu kullanıyor." + +#~ msgid "Nepomuk search service uses an appropriate backend. " +#~ msgstr "Nepomuk arama servisi uyumsuz bir arka uç kullanıyor. " + +#~ msgid "The Nepomuk search service uses one of the recommended backends." +#~ msgstr "Nepomuk arama servisi önerilen arka uçlardan birisini kullanıyor." + +#~ msgid "" +#~ "Plugin \"%1\" is not builtin static, please specify this information in " +#~ "the bug report." +#~ msgstr "" +#~ "\"%1\" eklentisi dahili durağan değil, lütfen bu bilgiyi hata raporunda " +#~ "belirtin." + +#~ msgid "Plugin Not Built Statically" +#~ msgstr "Eklenti Statik olarak Derlenmemiş" + +#, fuzzy +#~| msgid "&New Folder..." +#~ msgid "New Folder..." +#~ msgstr "Ye&ni Dizin..." + +#, fuzzy +#~| msgid "Folder &Properties" +#~ msgid "Resource Properties" +#~ msgstr "Dizin &Özellikleri" + +#~ msgid "Cache" +#~ msgstr "Önbellek" + +#~ msgid "Inherit cache policy from parent" +#~ msgstr "Önbellek politikasını üst ögeden al" + +#~ msgid "Cache Policy" +#~ msgstr "Önbellekleme Politikası" + +#~ msgid "Interval check time:" +#~ msgstr "Kontrol etme aralığı:" + +#~ msgid "Local cache timeout:" +#~ msgstr "Yerel önbellek zaman aşımı:" + +#~ msgid "Synchronize on demand" +#~ msgstr "İstenildiğinde eşzamanla" + +#~ msgid "Manage which folders you want to see in the folder tree" +#~ msgstr "Dizin ağacında hangi dizinleri görmek istediğinizi ayarlayın" + +#, fuzzy +#~| msgctxt "search folder" +#~| msgid "Search:" +#~ msgctxt "@label:textbox The clickMessage of a search line edit" +#~ msgid "Search" +#~ msgstr "Ara:" + +#~ msgid "Available Folders" +#~ msgstr "Kullanılabilir Dizinler" + +#~ msgid "Current Changes" +#~ msgstr "Geçerli Değişiklikler" + +#~ msgid "Unsubscribe from selected folder" +#~ msgstr "Seçilen dizinin üyeliğinden ayrıl" + +#~ msgid "The Akonadi server did report error during startup into %1." +#~ msgstr "Akonadi sunucusu açılışta %1 konumuna hiç hata bildirdi." + +#~ msgid "The Akonadi control process did report error during startup into %1." +#~ msgstr "Akonadi kontrol süreci açılışta %1 konumuna ' hata bildirdi." + +#~ msgid "TODO" +#~ msgstr "YAPILACAK" + +#~ msgid "" +#~ "

Akonadi not operational.
Details...

" +#~ msgstr "" +#~ "

Akonadi kullanıma hazır değil.
Ayrıntılar...

" + +#~ msgctxt "@title, application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi Kaynağı" + +#~ msgid "Nepomuk search service uses Sesame2 backend. " +#~ msgstr "Nepomuk arama servisi Sesame2 arka ucu kullanıyor. " + +#, fuzzy +#~| msgid "no collection" +#~ msgid "&Cut Collection" +#~ msgid_plural "&Cut %1 Collections" +#~ msgstr[0] "koleksiyon yok" + +#~ msgid "Copy failed" +#~ msgstr "Kopyalama işlemi başarısız oldu" diff --git a/po/ug/akonadi_knut_resource.po b/po/ug/akonadi_knut_resource.po new file mode 100644 index 0000000..6df17a4 --- /dev/null +++ b/po/ug/akonadi_knut_resource.po @@ -0,0 +1,84 @@ +# Uyghur translation for akonadi_knut_resource. +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Sahran , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2013-09-08 07:05+0900\n" +"Last-Translator: Gheyret Kenji \n" +"Language-Team: Uyghur Computer Science Association \n" +"Language: ug\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "سانلىق-مەلۇمات ھۆججىتى تاللانمىدى." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "ھۆججەت ‹%1› مۇۋەپپەقىيەتلىك ئوقۇلدى." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "سانلىق-مەلۇمات ھۆججىتى تاللاش" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut سانلىق-مەلۇمات ھۆججىتى" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "يىراقتىكى كىملىك %1 نىڭ تۈرى تېپىلمىدى." + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "DOM دەرىخىدە(tree) باش توپلام(collection ) يوق." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "توپلام(collection ) غا يازغىلى بولمىدى." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "DOM دەرىخىدە(tree) ئۆزگەرتىلگەن توپلام(collection) يوق." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "DOM دەرىخىدە(tree) ئۆچۈرۈلگەن توپلام(collection ) يوق." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "DOM دەرىخىدە(tree) باش توپلام(collection ) ‹%1› يوق." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "تۈرنى يازغىلى بولمىدى." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "DOM دەرىخىدە(tree) ئۆزگەرتىلگەن تۈر يوق." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "DOM دەرىخىدە(tree) ئۆچۈرۈلگەن تۈر يوق." diff --git a/po/ug/libakonadi5.po b/po/ug/libakonadi5.po new file mode 100644 index 0000000..8cf95d3 --- /dev/null +++ b/po/ug/libakonadi5.po @@ -0,0 +1,2531 @@ +# Uyghur translation for libakonadi. +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Sahran , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2013-09-08 07:05+0900\n" +"Last-Translator: Gheyret Kenji \n" +"Language-Team: Uyghur Computer Science Association \n" +"Language: ug\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "ئابدۇقادىر ئابلىز, غەيرەت كەنجى" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "sahran.ug@gmail.com, gheyret@gmail.com" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "تەييار" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "توردا يوق" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "قەدەمداشلاۋاتىدۇ…" + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "خاتا." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "" + +#: agentbase/resourcebase.cpp:531 +#, fuzzy, kde-format +#| msgctxt "@title application name" +#| msgid "Akonadi Resource" +msgid "Akonadi Resource" +msgstr "Akonadi مەنبەسى" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "بۇنداق توپلام يوق." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "ئىشلەتكۈچى مەشغۇلاتنى بىكار قىلدى." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "نامەلۇم خاتالىق." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "ئاتى" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "ئوقۇۋاتىدۇ…" + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "خاتا." + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "ئاتى" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "خاتا." + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "ئوقۇلمىغان ئۇچۇرلار" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "نورما" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "ساقلىغۇچ چوڭلۇقى" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "ئوقۇلمىغان" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "جەمئىي" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "چوڭلۇقى" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "ئاتسىز قىستۇرما" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "چۈشەندۈرۈشى يوق" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, fuzzy, kde-format +#| msgid "Akonadi Agent" +msgid "Akonadi Self Test" +msgstr "Akonadi Agent" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "مىنۇت" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "ھەرگىز" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "مىنۇت" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "ئېلىش تاللانمىلىرى" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "مەڭگۈ" + +#: widgets/collectiondialog.cpp:56 +#, fuzzy, kde-format +#| msgctxt "" +#| "@info/plain Displayed grayed-out inside the textbox, verb to search" +#| msgid "Search" +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "ئىزدە" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "يېڭى تارماق مۇندەرىجە(&N)…" + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "نۆۋەتتە تاللانغان قىسقۇچ ئاستىغا يېڭى تارماق قىسقۇچ قۇرىدۇ" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "يېڭى قىسقۇچ" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "ئاتى" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "قىسقۇچ قۇرالمىدى: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "قىسقۇچ قۇرۇش مەغلۇپ بولدى" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "ئادەتتىكى" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "ئاتى(&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "ئىختىيارى سىنبەلگە ئىشلەت(&U):" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "قىسقۇچ" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "ستاتىستىكا" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "مەزمۇنى:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "چوڭلۇقى:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "%1 تۈرنى كەس" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "ئوقۇلمىغان ئۇچۇرلار" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Delete folder?" +#| msgid_plural "Delete folders?" +msgid "Reindex folder" +msgstr "قىسقۇچلار ئۆچۈرەمسىز؟" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "No such collection." +msgctxt "@title:window" +msgid "Select a collection" +msgstr "بۇنداق توپلام يوق." + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "بۇ جايغا يۆتكە(&M)" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "بۇ جايغا كۆچۈر(&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "ئەمەلدىن قالدۇر" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "بەلگىلەر" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "توقۇنۇش ھەل قىلىش" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "سانلىق-مەلۇمات" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "" + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "" + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "بۇ جايغا يۆتكە(&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "بۇ جايغا كۆچۈر(&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "بۇ يەرگە ئۇلا(&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "ۋاز كەچ(&A)" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "باشلاش" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "تەپسىلاتلار…" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "باشلاش" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "ئاتى:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Agent" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi Agent" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "" + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "خاتا." + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "ھۆججەت «%1» نى ئاچالمايدۇ" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "تەپسىلاتلار" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "يېڭى قىسقۇچ(&N)…" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "يېڭى" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "%1 قىسقۇچ ئۆچۈر(&D)" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "ئۆچۈر" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "قەدەمداش" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "خاسلىق" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "چاپلا(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "چاپلا" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "" + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "ئات ئۆزگەرت" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "كۆچۈر" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "يۆتكە" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "" + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "كەس" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "%1 مەنبە ئۆچۈر" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "تورسىز خىزمەت" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "" + +#: widgets/standardactionmanager.cpp:254 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgid "Synchronize Folder Tree" +msgstr "قەدەمداش" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "%1 تۈرنى كەس" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "Cut Folder" +#| msgid_plural "Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "%1 قىسقۇچنى كەس" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "%1 قىسقۇچ ئۆچۈر(&D)" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "قەدەمداش" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "%1 مەنبە ئۆچۈر(&D)" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "%1 قىسقۇچ كۆچۈر" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "%1 تۈر كۆچۈر" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "%1 تۈرنى كەس" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "%1 قىسقۇچنى كەس" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "%1 تۈر ئۆچۈر" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "%1 قىسقۇچ ئۆچۈر" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "%1 مەنبە ئۆچۈر" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "قەدەمداش" + +#: widgets/standardactionmanager.cpp:361 +#, fuzzy, kde-format +#| msgctxt "@title:column, name of a thing" +#| msgid "Name" +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "ئاتى" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "قىسقۇچ %1 ۋە ئۇنىڭ ئىچىدىكى بارلىق تارماق قىسقۇچلارنى ئۆچۈرەمسىز؟" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "قىسقۇچلار ئۆچۈرەمسىز؟" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "قىسقۇچنى ئۆچۈرگىلى بولمىدى: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "قىسقۇچ ئۆچۈرۈش مەغلۇپ بولدى" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "قىسقۇچ ‹%1› نىڭ خاسلىقى" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "ئاتى:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "چاپلاش مەغلۇپ بولدى" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "يەرلىك مۇشتەرىلەر" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "يەرلىك مۇشتەرىلەر" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "ئىزدە:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "&Subscribed only" +msgstr "مۇشتەرى" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "مۇشتەرى" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "مۇشتەرىلىكتىن ئايرىلىش" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "" + +#: widgets/tageditwidget.cpp:165 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@title" +msgid "Delete tag" +msgstr "%1 تۈر ئۆچۈر" + +#: widgets/tageditwidget.cpp:189 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@info" +msgid "Delete tag" +msgstr "%1 تۈر ئۆچۈر" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgid "Delete Item" +#| msgid_plural "Delete %1 Items" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "%1 تۈر ئۆچۈر" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "" + +#: xml/xmldocument.cpp:131 +#, fuzzy, kde-format +#| msgid "Could not open file '%1'" +msgid "Unable to open data file '%1'." +msgstr "ھۆججەت «%1» نى ئاچالمايدۇ" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "" + +#~ msgid "Id" +#~ msgstr "Id" + +#, fuzzy +#~| msgid "Delete" +#~ msgctxt "@action:button" +#~ msgid "Delete" +#~ msgstr "ئۆچۈر" + +#, fuzzy +#~| msgid "Cancel" +#~ msgctxt "@action:button" +#~ msgid "Cancel" +#~ msgstr "ئەمەلدىن قالدۇر" + +#~ msgctxt "@title:column, number of unread messages" +#~ msgid "Unread" +#~ msgstr "ئوقۇلمىغان" + +#~ msgctxt "@title:column, total number of messages" +#~ msgid "Total" +#~ msgstr "جەمئىي" + +#~ msgctxt "@title:column, total size (in bytes) of the collection" +#~ msgid "Size" +#~ msgstr "چوڭلۇقى" + +#~ msgctxt "@title application description" +#~ msgid "Akonadi Resource" +#~ msgstr "Akonadi مەنبەسى" + +#~ msgctxt "@label:textbox name of a thing" +#~ msgid "Name" +#~ msgstr "ئاتى" + +#~ msgid "KDE Test Program" +#~ msgstr "KDE سىناق پروگراممىسى" diff --git a/po/uk/akonadi_knut_resource.po b/po/uk/akonadi_knut_resource.po new file mode 100644 index 0000000..6536001 --- /dev/null +++ b/po/uk/akonadi_knut_resource.po @@ -0,0 +1,87 @@ +# Translation of akonadi_knut_resource.po to Ukrainian +# Copyright (C) 2009-2010 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Yuri Chornoivan , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-06-29 19:09+0300\n" +"Last-Translator: Yuri Chornoivan \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.1\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "Не обрано файла з даними." + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "Файл «%1» було успішно завантажено." + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "Оберіть файл даних" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Файл даних Knut Akonadi" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "Елемента для віддаленого ідентифікатора %1 не знайдено" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "У ієрархії DOM не знайдено батьківської збірки." + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "Не вдалося виконати запис збірки." + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "У ієрархії DOM не знайдено зміненої збірки." + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "У ієрархії DOM не знайдено вилученої збірки." + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "У ієрархії DOM не знайдено батьківської збірки «%1»." + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "Не вдалося записати елемент." + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "У ієрархії DOM не знайдено зміненого елемента." + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "У ієрархії DOM не знайдено вилученого елемента." diff --git a/po/uk/libakonadi5.po b/po/uk/libakonadi5.po new file mode 100644 index 0000000..fb43b1c --- /dev/null +++ b/po/uk/libakonadi5.po @@ -0,0 +1,2660 @@ +# Translation of libakonadi5.po to Ukrainian +# Copyright (C) 2018-2021 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# Ivan Petrouchtchak , 2007, 2008. +# Yuri Chornoivan , 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi5\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-03-01 09:22+0200\n" +"Last-Translator: Yuri Chornoivan \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 20.11.70\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Юрій Чорноіван" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "yurchor@ukr.net" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "Зараз не налаштовано жодного облікового запису." + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "Підтримки інтеграції облікових записів не передбачено" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "Не вдалося зареєструвати об’єкт у D-Bus: %1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "%1 типу %2" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "Ідентифікатор агента" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Агент Akonadi" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "Готовий до обробки запитів" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "Поза мережею" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "Синхронізація..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "Помилка." + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "Не налаштовано" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "Ідентифікатор ресурсу" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Ресурс Akonadi" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "Отримано некоректний запис" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "Помилка під час створення запису: %1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "Помилка під час спроби оновлення збірки: %1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "Спроба оновлення локальної збірки зазнала невдачі: %1." + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "Спроба оновлення локальних записів зазнала невдачі: %1." + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "Неможливо отримати елемент в автономному режимі." + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "Синхронізація теки «%1»" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "Не вдалося отримати збірку для синхронізації." + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "Не вдалося отримати збірку для синхронізації атрибутів." + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "Потрібного елемента вже не існує" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "Виконання завдання скасовано." + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "Немає такої збірки." + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "Знайдено непов’язані збірки" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "Не вдалося знайти інший запис для усування конфлікту" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "Не вдалося отримати доступ до інтерфейсу D-Bus створеного агента." + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "Створення агента прострочено." + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "Не вдалося отримати дані щодо типу агента «%1»." + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "Не вдалося створити екземпляр агента." + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "Некоректний екземпляр збірки." + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "Некоректний екземпляр ресурсу." + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "Не вдалося отримати інтерфейс D-Bus для ресурсу «%1»" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "Час очікування синхронізації атрибутів збірки перевищено." + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "Некоректна збірка для копіювання" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "Некоректна збірка призначення" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "Некоректний кореневий елемент" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "Не вдалося визначити збірку за відповіддю" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "Некоректна збірка" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "Вказано некоректну збірку." + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "Не вказано об’єктів для пересування" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "Не вказано коректного призначення" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "Некоректна збірка." + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "Некоректна батьківська збірка" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "Не вдалося зв’язатися зі службою Akonadi." + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "" +"Версія протоколу сервера Akonadi не сумісна з вашою. Переконайтеся, що ви " +"встановили сумісну версію." + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "Користувач скасував дію." + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "Невідома помилка." + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "Неочікувана відповідь" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "Не вдалося створити зв’язок." + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "Час очікування синхронізації ресурсу перевищено." + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "Не вдалося отримати кореневу збірку ресурсу %1." + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "Не вказано ідентифікатора ресурсу." + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "Некоректний ідентифікатор ресурсу «%1»" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "Не вдалося налаштувати типовий ресурс за допомогою D-Bus." + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "Не вдалося отримати збірку ресурсів." + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "Перевищення часу очікування блокування." + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "Не вдалося створити мітку." + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "" +"Спроба пересунути збірку до смітника зазнала невдачі, дію з пересування " +"перервано" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "Передано некоректні об’єкти" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "Передано некоректну збірку" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "Не виявлено коректної збірки або порожній список об’єктів" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "" +"Не вдалося знайти збірку для відновлення, ресурс відновлення недоступний." + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "Назва" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "Завантаження…" + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "Помилка" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"У збірці призначення, «%1», вже міститься\n" +"збірка з назвою «%2»." + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "Назва" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "Не вдалося скопіювати запис: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "Не вдалося скопіювати збірку: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "Не вдалося пересунути запис: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "Не вдалося пересунути збірку: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "Не вдалося пов'язати елемент: %1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "Помилка" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "Теки улюблених" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "Загалом повідомлень" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "Непрочитаних повідомлень" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "Квота" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "Об’єм сховища" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "Об’єм сховища підтек" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "Непрочитаних" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "Всього" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "Розмір" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "Мітка" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "Не вдалося отримати запис для індексування" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "Покажчик недоступний" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "Не вдалося отримати доступ до змістовної частини «%1» цього покажчика" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "Не вдалося встановити сеансу для цього покажчика" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "У цьому покажчику немає пунктів" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "Додаток без назви" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "Опис відсутній" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Версія протоколу сервера Akonadi відрізняється від версію протоколу, яка " +"використовується цією програмою.\n" +"Якщо вашу систему було нещодавно оновлено, будь ласка, вийдіть із облікового " +"запису і увійдіть до нього знову, щоб усі програми могли скористатися " +"належною версією протоколу." + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" +"Немає доступних агентів Akonadi. Будь ласка, перевірте, чи правильно " +"встановлено KDE PIM у вашій системі." + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"Не збігаються версії протоколів. Версія на сервері є старішою (%1) за " +"клієнтську (%2). Якщо вашу систему було нещодавно оновлено, будь ласка, " +"перезапустіть сервер Akonadi." + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"Не збігаються версії протоколів. Версія на сервері є новішою (%1) за " +"клієнтську (%2). Якщо вашу систему було нещодавно оновлено, будь ласка, " +"перезапустіть усі програми KDE PIM." + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Самоперевірка Akonadi" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Перевіряє сервер Akonadi і повідомляє про його стан" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "© Volker Krause , 2008" + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "С&творити екземпляр агента…" + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "Ви&лучити екземпляр агента" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "&Налаштувати екземпляр агента" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "Новий екземпляр агента" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "Не вдалося створити екземпляр агента: %1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "Спроба створення агента зазнала невдачі" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "Вилучити екземпляр агента?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "Ви справді бажаєте вилучити позначений екземпляр агента?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "Налаштування %1" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "Підручник з %1" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "Про %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "Вікно налаштувань вже відкрито у іншому вікні програми" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "Вікно налаштувань для %1 вже десь відкрито." + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "Не вдалося зареєструвати вікно налаштувань %1." + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "хвилина" +msgstr[1] "хвилини" +msgstr[2] "хвилин" +msgstr[3] "хвилина" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "Отримання" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "Використовувати параметри батьківської теки або облікового запису" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "Виконувати синхронізацію у разі позначення цієї теки" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "Автоматично синхронізувати кожні:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "Ніколи" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "хв." + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "Локально кешовані частини" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "Параметри отримання пошти" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "Завжди &отримувати повідомлення повністю" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "О&тримувати вміст повідомлень на вимогу" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "Зберігати вміст повідомлень локально протягом:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "Нескінченний" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "Шукати" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "Зробити теку типовою" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "&Нова підтека..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "Створити нову підтеку у вибраній теці" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "Нова тека" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "Назва" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "Не вдалося створити теку: %1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "Помилка створення теки" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "Загальні" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 об'єкт" +msgstr[1] "%1 об'єкти" +msgstr[2] "%1 об'єктів" +msgstr[3] "%1 об'єкт" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "&Назва:" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "В&икористовувати нетипову теку:" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "тека" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "Статистика" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "Вміст:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 об’єктів" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "Розмір:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 бутів" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "Зважте на те, що індексування може тривати декілька хвилин." + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "Обслуговування" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "Помилка під час спроби отримати кількість індексованих записів" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "Індексовано %1 запис у цій теці" +msgstr[1] "Індексовано %1 записи у цій теці" +msgstr[2] "Індексовано %1 записів у цій теці" +msgstr[3] "Індексовано один запис у цій теці" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "Обчислюємо кількість індексованих записів…" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "Файли" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "Тип теки:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "невідомо" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "Записи" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "Загалом записів:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "Непрочитаних записів:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "Індексування" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "Увімкнути повнотекстове індексування" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "Отримуємо кількість індексованих записів…" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "Повторно індексувати теку" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "Без теки" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "Відкрити діалогове вікно збірки" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "Виберіть збірку" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "&Пересунути сюди" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "&Скопіювати сюди" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "Скасувати" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "Час внесення змін" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "Прапорці" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "Атрибут: %1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "Розв'язання конфліктів" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "Використати вашу версію" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "Використати сторонню версію" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "Зберегти обидві версії" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"Внесені вами зміни суперечать змінам, які було внесено кимось іншим." +"
Якщо одну з цих версій не буде відкинуто, вам доведеться здійснити " +"узгодження внесених змін вручну.
Натисніть кнопку «Відкрити текстовий редактор» для створення копії тексту, потім " +"виберіть найпридатнішу з версій, повторно відкрийте її і додайте пропущені " +"фрагменти." + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "Дані" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Запуск сервера Akonadi..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Зупинка сервера Akonadi..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "&Пересунути сюди" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "&Скопіювати сюди" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "С&творити посилання" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "&Скасувати" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"Не вдалося зв’язатися зі службою керування особистими даними.\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "Запуск служби керування особистою інформацією…" + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "Завершення роботи служби керування особистою інформацією…" + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "Служба керування особистими даними виконує оновлення бази даних." + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"Служба керування особистих даних виконує оновлення бази даних.\n" +"Таке оновлення виконується після оновлення програмного забезпечення, " +"оновлення потрібне для оптимізації швидкодії роботи з базою даних.\n" +"Оновлення може тривати декілька хвилин." + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "" +"Службу керування особистою інформацією Akonadi не запущено. Ця програма не " +"зможе працювати без відповідної служби." + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "Запустити" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Оболонка керування особистою інформацією Akonadi недієздатна.\n" +"Натисніть кнопку «Подробиці...», щоб побачити докладніші відомості щодо цієї " +"проблеми." + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Служба керування особистою інформацією Akonadi недієздатна." + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "Подробиці…" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "Хочете вилучити обліковий запис «%1»?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "Вилучити обліковий запис?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "Вхідні облікові записи (додайте хоча б один):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "&Додати…" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "&Змінити…" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "Ви&лучити" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "Перезапустити" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "Нещодавня тека" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "Перейменування улюбленого" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "Назва:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Самоперевірка сервера Akonadi" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "Зберегти звіт..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "Копіювати звіт до буфера" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"Ваші поточні налаштування Akonadi вимагають встановлення драйвера QtSQL " +"«%1». Цього драйвера не було знайдено у системі." + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"Ваші поточні налаштування Akonadi вимагають встановлення драйвера QtSQL " +"«%1».\n" +"Встановлено такі драйвери: %2.\n" +"Переконайтеся, що встановлено потрібний драйвер." + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "Знайдено драйвер бази даних." + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "Не знайдено драйвера бази даних." + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "Виконуваний файл сервера MySQL не було перевірено." + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "За поточних налаштувань внутрішній сервер MySQL не потрібен." + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"Зараз Akonadi налаштовано на використання сервера MySQL «%1».\n" +"Переконайтеся, що ви встановили сервер MySQL, вкажіть правильний шлях і " +"переконайтеся, що у вас є потрібні права на читання і виконання файла " +"програми сервера. Типовим виконуваним файлом є «mysqld», його розташування " +"може бути різним у різних дистрибутивах." + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "Не знайдено сервера MySQL." + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "Сервер MySQL неможливо прочитати." + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "Сервер MySQL неможливо виконати." + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "Знайдено MySQL з неочікуваною назвою." + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "Знайдено сервер MySQL." + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "Знайдено сервер MySQL: %1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "Сервер MySQL можна виконувати." + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "" +"Спроба виконання сервера MySQL «%1» завершилася невдало, було повідомлено " +"про помилку: «%2»" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "Спроба виконання сервера MySQL зазнала невдачі." + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "Журнал помилок сервера MySQL не було перевірено." + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "Не знайдено поточного журналу помилок сервера MySQL." + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "" +"Сервер MySQL не повідомляв про помилки під час запуску. Журнал збережено до " +"«%1»." + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "Журнал помилок сервера MySQL неможливо прочитати." + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "" +"Було знайдено файл журналу помилок MySQL, але він не придатний для читання: " +"%1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "У журналі помилок сервера MySQL є записи про помилки." + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "У файлі журналу помилок сервера MySQL «%1» містяться помилки." + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "У журналі помилок сервера MySQL містяться попередження." + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "У файлі журналу помилок сервера MySQL «%1» містяться попередження." + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "У журналі помилок сервера MySQL немає записів про помилки." + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "" +"У файлі журналу помилок сервера MySQL «%1» немає записів про помилки або " +"попередження." + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "Налаштування сервера MySQL не було перевірено." + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "Було знайдено типові налаштування сервера MySQL." + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "" +"Типові налаштування сервера MySQL було знайдено, вони придатні для читання і " +"знаходяться у %1." + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "Типових налаштувань сервера MySQL не знайдено." + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"Типових налаштувань сервера MySQL не було знайдено, або ці налаштування не " +"придатні для читання. Перевірте, чи було завершено встановлення Akonadi, і " +"чи маєте ви всі потрібні права доступу." + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "Нетипових налаштувань сервера MySQL не знайдено." + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "" +"Нетипових налаштувань сервера MySQL не було знайдено, але вони і не є " +"обов’язковими." + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "Знайдено нетипові налаштування сервера MySQL." + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "" +"Було знайдено нетипові налаштування сервера MySQL, вони придатні для читання " +"і знаходяться у %1" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "Нетипові налаштування сервера MySQL неможливо прочитати." + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"Було знайдено нетипові налаштування сервера MySQL у %1, але ці налаштування " +"не придатні для читання. Перевірте ваші права на доступ до налаштувань." + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "" +"Налаштування сервера MySQL не було знайдено, або ці налаштування не придатні " +"для читання." + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "" +"Налаштування сервера MySQL не було знайдено, або ці налаштування не придатні " +"для читання." + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "Налаштування сервера MySQL придатні для використання." + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "" +"Було знайдено налаштування сервера MySQL, вони знаходяться у %1 і придатні " +"для читання." + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "Не вдалося зв’язатися з сервером PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "Знайдено сервер PostgreSQL." + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "Було знайдено сервер PostgreSQL, з’єднання є працездатним." + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "akonadictl не знайдено" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"Програма «akonadictl» має знаходитися у одному з каталогів, описаних у " +"змінній $PATH. Переконайтеся, що встановлено сервер Akonadi." + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "akonadictl знайдено, програма придатна до використання" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"Було знайдено і встановлено можливість успішного виконання програми «%1», " +"яка керує сервером Akonadi.\n" +"Результат:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl знайдено, але програма непридатна до використання" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"Було знайдено програму «%1», яка керує сервером Akonadi, але цю програму " +"неможливо успішно виконати.\n" +"Результат:\n" +"%2\n" +"Переконайтеся, що сервер Akonadi встановлено належним чином." + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Процес керування Akonadi зареєстровано у D-Bus." + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Процес керування Akonadi зареєстровано у D-Bus, це зазвичай означає, що він " +"є працездатним." + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Процес керування Akonadi не зареєстровано у D-Bus." + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Процес керування Akonadi не зареєстровано у D-Bus, що зазвичай означає, що " +"його не було запущено або під час запуску цей процес завершився у аварійному " +"режимі." + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Процес сервера Akonadi зареєстровано у D-Bus." + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "" +"Процес сервера Akonadi зареєстровано у D-Bus, що зазвичай означає, що він " +"працездатний." + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Процес сервера Akonadi не зареєстровано у D-Bus." + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Процес сервера Akonadi не зареєстровано у D-Bus, що зазвичай означає, що " +"його не було запущено або під час запуску цей процес завершився у аварійному " +"режимі." + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "Перевірка версії протоколу є неможливою." + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "" +"Без з’єднання з сервером неможливо визначити, чи задовольняє версія " +"протоколу вимогам." + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "Версія протоколу сервера є застарілою." + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"Номер версії протоколу сервера — %1, для роботи клієнта потрібен протокол " +"версії %2. Якщо нещодавно виконувалося оновлення системи, переконайтеся, що " +"програми KDE PIM та Akonadi було перезапущено після оновлення." + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "Версія протоколу сервера є надто новою." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "Версії протоколу збігаються." + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "Поточною версією протоколу є %1." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "Знайдено агенти ресурсу." + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "Знайдено принаймні один агент ресурсів." + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "Не знайдено жодного агента ресурсів." + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"Не знайдено жодного агента ресурсів, Akonadi не придатний до використання " +"без принаймні одного. Це, зазвичай, означає, що не було встановлено жодного " +"агента ресурсів або під час встановлення виникли проблеми. Пошук було " +"проведено у таких теках: %1. Змінну середовища XDG_DATA_DIRS встановлено у " +"значення «%2», перевірте чи є там всі шляхи, за якими встановлено агентів " +"Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "Не знайдено поточного журналу помилок сервера Akonadi." + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "" +"Під час поточного запуску сервер Akonadi не повідомляв про будь-які помилки." + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "Знайдено поточний журнал помилок сервера Akonadi." + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "" +"Під час поточного запуску сервер Akonadi повідомляв помилки. Журнал помилок " +"записано до %1." + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "Не знайдено попереднього журналу помилок сервера Akonadi." + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "" +"Під час попереднього запуску сервер Akonadi не повідомляв про будь-які " +"помилки." + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "Знайдено попередній журнал помилок сервера Akonadi." + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "" +"Під час попереднього запуску сервер Akonadi повідомляв помилки. Журнал " +"помилок записано до %1." + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "Не знайдено поточного журналу помилок керування Akonadi." + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "" +"Під час поточного запуску процес керування Akonadi не повідомляв про будь-" +"які помилки." + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "Знайдено поточний журнал помилок керування Akonadi." + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "" +"Під час поточного запуску процес керування Akonadi повідомляв помилки. " +"Журнал помилок записано до %1." + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "Не знайдено попереднього журналу помилок керування Akonadi." + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "" +"Під час попереднього запуску процес керування Akonadi не повідомляв про будь-" +"які помилки." + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "Знайдено попередній журнал помилок керування Akonadi." + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "" +"Під час попереднього запуску процес керування Akonadi повідомляв помилки. " +"Журнал помилок записано до %1." + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi було запущено від імені root" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"Запуск програм, які працюють з інтернетом від імені користувача root або " +"адміністратора системи призводить до вразливості системи. Щоб запобігти " +"можливим негативним наслідкам, MySQL, сервер, який використовує ця версія " +"Akonadi, не дозволяє запускати себе від імені користувача root." + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi не запущено від імені root" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"Akonadi не запущено від імені користувача root або адміністратора системи. " +"Це рекомендовані налаштування, які забезпечують безпеку системи." + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "Збереження звіту про перевірку" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "Помилка" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "Не вдалося відкрити файл «%1»" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"Під час спроби запуску сервера Akonadi сталася помилка. Наступні внутрішні " +"перевірки можуть допомогти виявити і розв’язати цю проблему. Якщо ви маєте " +"намір звернутися по підтримку або повідомити про ваду, будь ласка, завжди " +"долучайте до свого повідомлення цей звіт." + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "Подробиці" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

Додаткові поради з усунення негараздів можна знайти за адресою userbase.kde.org/Akonadi_(uk)." +"

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "&Нова тека..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "Створити" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "&Вилучити теку" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "Вилучити" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "&Синхронізувати теку" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "Синхронізувати" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "В&ластивості теки" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "Властивості" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "&Вставити" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "Вставити" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "Керування локальними &підписками..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "Керування локальними підписками" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "Додати до тек улюблених" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "Додати до улюблених" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "Вилучити з тек улюблених" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "Вилучити з улюблених" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "Перейменувати улюблене..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "Перейменувати" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "Скопіювати теку до..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "Скопіювати до" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "Скопіювати запис до..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "Пересунути запис до..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "Пересунути до" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "Пересунути теку до..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "Ви&різати запис" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "Вирізати" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "Ви&різати теку" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "Створити ресурс" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "Вилучити ресурс" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "В&ластивості ресурсу" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "Синхронізувати ресурс" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "Працювати автономно" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "&Синхронізувати теку рекурсивно" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "Синхронізувати рекурсивно" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "&Пересунути теку до смітника" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "Пересунути теку до смітника" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "&Пересунути об’єкт до смітника" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "Пересунути об’єкт до смітника" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "Ві&дновити теку зі смітника" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "Відновити теку зі смітника" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "Ві&дновити об’єкт зі смітника" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "Відновити об’єкт зі смітника" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "Ві&дновити збірку зі смітника" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "Відновити збірку зі смітника" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "&Синхронізувати теки улюблених" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "Синхронізація тек улюблених" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "Синхронізувати ієрархію тек" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "&Скопіювати %1 теку" +msgstr[1] "&Скопіювати %1 теки" +msgstr[2] "&Скопіювати %1 тек" +msgstr[3] "&Скопіювати %1 теку" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "&Скопіювати %1 елемент" +msgstr[1] "&Скопіювати %1 елементи" +msgstr[2] "&Скопіювати %1 елементів" +msgstr[3] "&Скопіювати %1 елемент" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "Ви&різати %1 запис" +msgstr[1] "Ви&різати %1 записи" +msgstr[2] "Ви&різати %1 записів" +msgstr[3] "Ви&різати запис" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "Ви&різати %1 теку" +msgstr[1] "Ви&різати %1 теки" +msgstr[2] "Ви&різати %1 тек" +msgstr[3] "Ви&різати теку" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "&Вилучити %1 елемент" +msgstr[1] "&Вилучити %1 елементи" +msgstr[2] "&Вилучити %1 елементів" +msgstr[3] "&Вилучити %1 елемент" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "&Вилучити %1 теку" +msgstr[1] "&Вилучити %1 теки" +msgstr[2] "&Вилучити %1 тек" +msgstr[3] "&Вилучити теку" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "&Синхронізувати %1 теку" +msgstr[1] "&Синхронізувати %1 теки" +msgstr[2] "&Синхронізувати %1 тек" +msgstr[3] "&Синхронізувати теку" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "Ви&лучити %1 ресурс" +msgstr[1] "Ви&лучити %1 ресурси" +msgstr[2] "Ви&лучити %1 ресурсів" +msgstr[3] "Ви&лучити ресурс" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "Син&хронізувати %1 ресурс" +msgstr[1] "Син&хронізувати %1 ресурси" +msgstr[2] "Син&хронізувати %1 ресурсів" +msgstr[3] "Син&хронізувати ресурс" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "Копіювати %1 теку" +msgstr[1] "Копіювати %1 теки" +msgstr[2] "Копіювати %1 тек" +msgstr[3] "Копіювати теку" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "Копіювати %1 елемент" +msgstr[1] "Копіювати %1 елементи" +msgstr[2] "Копіювати %1 елементів" +msgstr[3] "Копіювати елемент" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "Вирізати %1 елемент" +msgstr[1] "Вирізати %1 елементи" +msgstr[2] "Вирізати %1 елементів" +msgstr[3] "Вирізати елемент" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "Вирізати %1 теку" +msgstr[1] "Вирізати %1 теки" +msgstr[2] "Вирізати %1 тек" +msgstr[3] "Вирізати теку" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "Вилучити %1 елемент" +msgstr[1] "Вилучити %1 елементи" +msgstr[2] "Вилучити %1 елементів" +msgstr[3] "Вилучити елемент" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "Вилучити %1 теку" +msgstr[1] "Вилучити %1 теки" +msgstr[2] "Вилучити %1 тек" +msgstr[3] "Вилучити теку" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "Синхронізувати %1 теку" +msgstr[1] "Синхронізувати %1 теки" +msgstr[2] "Синхронізувати %1 тек" +msgstr[3] "Синхронізувати теку" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "Вилучити %1 ресурс" +msgstr[1] "Вилучити %1 ресурси" +msgstr[2] "Вилучити %1 ресурсів" +msgstr[3] "Вилучити ресурс" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "Синхронізувати %1 ресурс" +msgstr[1] "Синхронізувати %1 ресурси" +msgstr[2] "Синхронізувати %1 ресурсів" +msgstr[3] "Синхронізувати ресурс" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "Назва" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "Ви справді бажаєте вилучити %1 теку і всі їхні підтеки?" +msgstr[1] "Ви справді бажаєте вилучити %1 теки і всі їхні підтеки?" +msgstr[2] "Ви справді бажаєте вилучити %1 тек і всі їхні підтеки?" +msgstr[3] "Ви справді бажаєте вилучити цю теку і всі її підтеки?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "Вилучити теки?" +msgstr[1] "Вилучити теки?" +msgstr[2] "Вилучити теки?" +msgstr[3] "Вилучити теку?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "Не вдалося вилучити теку: %1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "Помилка вилучення теки" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "Властивості теки %1" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "Ви справді бажаєте вилучити %1 позначений пункт?" +msgstr[1] "Ви справді бажаєте вилучити %1 позначені пункти?" +msgstr[2] "Ви справді бажаєте вилучити %1 позначених пунктів?" +msgstr[3] "Ви справді бажаєте вилучити позначений пункт?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "Вилучити пункти?" +msgstr[1] "Вилучити пункти?" +msgstr[2] "Вилучити пункти?" +msgstr[3] "Вилучити пункт?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "Не вдалося вилучити пункт: %1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "Помилка вилучення пункту" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "Перейменування улюбленого" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "Назва:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "Новий ресурс" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "Не вдалося створити ресурс: %1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "Спроба створення ресурсу зазнала невдачі" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "Ви справді бажаєте вилучити %1 ресурс?" +msgstr[1] "Ви справді бажаєте вилучити %1 ресурси?" +msgstr[2] "Ви справді бажаєте вилучити %1 ресурсів?" +msgstr[3] "Ви справді бажаєте вилучити цей ресурс?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "Вилучити ресурси?" +msgstr[1] "Вилучити ресурси?" +msgstr[2] "Вилучити ресурси?" +msgstr[3] "Вилучити ресурс?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "Не вдалося вставити дані: %1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "Спроба вставлення зазнала невдачі" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "Не можна додавати «/» до назви теки." + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "Помилка створення теки" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "Не можна додавати «.» на початку або в кінці назви теки." + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "" +"Для того, щоб розпочати синхронізацію теки «%1», ресурс має бути переведено " +"у режим роботи у мережі. Бажаєте перевести його у такий режим?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "Обліковий запис «%1» перебуває поза мережею" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "Увійти в мережу" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "Пересунути до цієї теки" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "Скопіювати в цю теку" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "Не вдалося оновити підписку: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "Помилка, пов'язана із підпискою" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "Локальні підписки" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "Пошук:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "&Лише підписані" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "П&ідписатися" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "&Відписатися" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "Не вдалося створити мітку" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "Під час спроби створення мітки сталася помилка" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "Ви справді хочете вилучити мітку %1?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "Вилучення мітки" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "Вилучити мітку" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "Виберіть мітки, які слід застосувати." + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "Створити мітку" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "Керування мітками" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "Виберіть мітки…" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "Вибір міток" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "Спорожнити" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "Клацніть, щоб додати мітки" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "…" + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Засіб перетворення даних Akonadi на XML" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "Перетворює ієрархію збірки Akonadi на файл XML." + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "© Volker Krause , 2009" + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "Дані не завантажено." + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "Не вказано назви файла" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "Не вдалося відкрити файл даних «%1»." + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "Файла %1 не існує." + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "Не вдалося обробити файл даних «%1»." + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "Не вдалося завантажити і обробити визначення схеми." + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "Не вдалося створити контекст обробки схеми." + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "Не вдалося створити схему." + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "Не вдалося створити контекст перевірки чинності схеми." + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "Некоректний формат файла." + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "Не вдалося обробити файл даних: %1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "Не вдалося знайти збірку %1" diff --git a/po/zh_CN/akonadi_knut_resource.po b/po/zh_CN/akonadi_knut_resource.po new file mode 100644 index 0000000..daf9a9d --- /dev/null +++ b/po/zh_CN/akonadi_knut_resource.po @@ -0,0 +1,89 @@ +# translation of akonadi_knut_resource.po to 简体中文 +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Ni Hui , 2009. +msgid "" +msgstr "" +"Project-Id-Version: kdeorg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2021-07-26 13:50\n" +"Last-Translator: \n" +"Language-Team: Chinese Simplified\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: kdeorg\n" +"X-Crowdin-Project-ID: 269464\n" +"X-Crowdin-Language: zh-CN\n" +"X-Crowdin-File: /kf5-stable/messages/akonadi/akonadi_knut_resource.pot\n" +"X-Crowdin-File-ID: 3104\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "未选择数据文件。" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "成功装入文件“%1”。" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "选择数据文件" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut 数据文件" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "找不到对应远程 ID %1 的项目" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "在 DOM 树中找不到上级收藏项。" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "无法写入收藏。" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "在 DOM 树中找不到修改过的收藏项。" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "在 DOM 树中找不到已删除的收藏项。" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "在 DOM 树中找不到上级收藏项“%1”。" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "无法写入项目。" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "在 DOM 树中找不到修改过的项目。" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "在 DOM 树中找不到已删除的项目。" diff --git a/po/zh_CN/libakonadi5.po b/po/zh_CN/libakonadi5.po new file mode 100644 index 0000000..275fe8b --- /dev/null +++ b/po/zh_CN/libakonadi5.po @@ -0,0 +1,2500 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Xuetian Weng , 2014, 2015. +# Weng Xuetian , 2015. +msgid "" +msgstr "" +"Project-Id-Version: kdeorg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2021-07-26 13:50\n" +"Last-Translator: \n" +"Language-Team: Chinese Simplified\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: kdeorg\n" +"X-Crowdin-Project-ID: 269464\n" +"X-Crowdin-Language: zh-CN\n" +"X-Crowdin-File: /kf5-stable/messages/akonadi/libakonadi5.pot\n" +"X-Crowdin-File-ID: 3177\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "KDE 中国" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kde-china@kde.org" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "目前没有配置账户。" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "暂不支持账户集成" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "无法在 dbus 上注册对象:%1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "类型 %2 的 %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "代理标识符" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi 代理" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "就绪" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "离线" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "正在同步..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "错误。" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "未配置" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "资源标识符" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi 资源" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "获得了无效的项目" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "创建项目时发生错误:%1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "更新收藏时发生错误:%1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "更新本地收藏失败:%1。" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "更新本地项目失败:%1。" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "无法在离线模式下获取项目。" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "正在同步文件夹“%1”" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "为同步获取收藏失败。" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "为属性同步获取收藏失败。" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "请求的项目不存在" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "任务已取消。" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "没有这个收藏。" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "发现无法解析的孤立收藏" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "无法找到需要处理冲突的其他对象" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "无法访问 D-Bus 接口来创建代理。" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "代理实例创建超时。" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "无法获得代理类型“%1”。" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "无法创建代理实例。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "无效的收藏实例。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "无效的资源实例。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "无法为资源“%1”获得 D-Bus 接口" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "收藏属性同步超时。" + +#: core/jobs/collectioncopyjob.cpp:55 +#, kde-format +msgid "Invalid collection to copy" +msgstr "复制无效的收藏" + +#: core/jobs/collectioncopyjob.cpp:61 +#, kde-format +msgid "Invalid destination collection" +msgstr "无效的目标收藏" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "无效的父类" + +#: core/jobs/collectioncreatejob.cpp:98 +#, kde-format +msgid "Failed to parse Collection from response" +msgstr "从响应解析收藏失败" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "无效的收藏" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "给出的收藏无效。" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "没有指定要移动的对象" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "未指定有效的目标" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "无效的收藏。" + +#: core/jobs/itemcreatejob.cpp:107 +#, kde-format +msgid "Invalid parent collection" +msgstr "无效的父收藏" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "无法连接到 Akonadi 服务。" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "和当前 Akonadi 服务器使用的协议版本不兼容。请确认您安装的是兼容版本。" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "用户取消了操作。" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "未知错误。" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "意外的响应" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "创建关系失败。" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "资源同步超时。" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "无法获取资源 %1 的收藏根路径。" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "未给出资源 ID。" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "无效的资源标识符“%1”" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "通过 D-Bus 配置默认资源失败。" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "获取资源收藏失败。" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "尝试获得锁时超时。" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "无法创建标签。" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "移动到回收站收藏失败,中止操作" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "传递了无效的项目" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "传递了无效的收藏" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "无效的收藏或空项目列表" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "无法找到恢复收藏并且恢复资源不可用" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "名称" + +#: core/models/entitytreemodel.cpp:197 +#, kde-format +msgctxt "@info:status" +msgid "Loading..." +msgstr "正在加载..." + +#: core/models/entitytreemodel.cpp:489 +#, kde-format +msgctxt "@window:title" +msgid "Error" +msgstr "错误" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "" +"目标集合“%1”已包含\n" +"名为“%2”的集合。" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "名称" + +#: core/models/entitytreemodel_p.cpp:1331 +#, kde-format +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "无法复制项目: %1" + +#: core/models/entitytreemodel_p.cpp:1333 +#, kde-format +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "无法复制收藏集: %1" + +#: core/models/entitytreemodel_p.cpp:1335 +#, kde-format +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "无法移动项目: %1" + +#: core/models/entitytreemodel_p.cpp:1337 +#, kde-format +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "无法移动收藏集: %1" + +#: core/models/entitytreemodel_p.cpp:1339 +#, kde-format +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "无法链接实体:%1" + +#: core/models/entitytreemodel_p.cpp:1341 +#, kde-format +msgctxt "@title:window" +msgid "Error" +msgstr "错误" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "收藏夹" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "全部消息" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "未读消息" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "配额" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "存储大小" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "子文件夹存储大小" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "未读" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "总共" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "大小" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "标签" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "无法获取用于索引的项目" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "索引不可用" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "装载部件“%1”无法用于此索引" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "没有可供索引的会话" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "没有可供索引的项目" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "未命名插件" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "无可用的描述" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" +"Akonadi 服务器的协议版本与此应用程序使用的协议版本不同。如果您最近更新了系" +"统,请注销并重新登录,并确认所有的应用使用正确的协议版本。" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "没有可用的的 Akonadi 代理。请验证您的 KDE PIM 安装。" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" +"协议版本不匹配。服务器的版本 (%1) 新于我们的版本 (%2)。如果您最近更新了请重" +"启 Akonadi 服务器。" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" +"协议版本不匹配。服务器的版本 (%1) 老于我们的版本 (%2)。如果您最近更新了请重启" +"所有 KDE PIM 应用程序。" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi 自检" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "检查并汇报 Akonadi 服务器状态" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "新建代理实例(&N)..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "删除代理实例(&D)" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "配置代理实例(&C)" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "新建代理实例" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "无法创建代理实例:%1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "代理实例创建失败" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "删除代理实例?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "您真的想要删除选中的代理实例吗?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "%1 设置" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "%1 手册" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "关于 %1" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "配置对话框已在另一个窗口打开" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "%1 的配置已打开。" + +#: widgets/agentconfigurationwidget.cpp:106 +#, kde-format +msgid "Failed to register %1 configuration dialog." +msgstr "注册 %1 配置对话框失败。" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "分钟" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "获取" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "使用父目录或账户的选项" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "选中此文件夹时同步" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "自动更新间隔:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "永不" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "分钟" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "本地缓存的部分" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "获取选项" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, kde-format +msgid "Always retrieve full &messages" +msgstr "总是获取完整信件(&M)" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, kde-format +msgid "&Retrieve message bodies on demand" +msgstr "按需获取信件主体(&R)" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "保持信件主体在本地:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "永远" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "搜索" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "默认使用此文件夹" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "新建子文件夹(&N)..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "在当前所选文件夹下创建新子文件夹" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "新建文件夹" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "名称" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "无法创建文件夹:%1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "文件夹创建失败" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "常规" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 个对象" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "名称(&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "使用自定义图标(&U):" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "文件夹" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "统计" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "内容:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 个对象" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "大小:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 字节" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "请记得,索引可能会需要几分钟。" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "维护" + +#: widgets/collectionmaintenancepage.cpp:129 +#, kde-format +msgid "Error while retrieving indexed items count" +msgstr "检索索引项计数时出错" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "已经索引此文件夹中的 %1 项" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "正在计算索引项..." + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "文件" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, kde-format +msgid "Folder type:" +msgstr "文件夹类型:" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "未知算法" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, kde-format +msgid "Items" +msgstr "项目" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, kde-format +msgid "Total items:" +msgstr "总项数:" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, kde-format +msgid "Unread items:" +msgstr "未读项:" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "索引" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "启用全文索引" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "正在检索索引项计数..." + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, kde-format +msgid "Reindex folder" +msgstr "重新索引文件夹" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "无件夹" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "打开收藏对话框" + +#: widgets/collectionrequester.cpp:143 +#, kde-format +msgctxt "@title:window" +msgid "Select a collection" +msgstr "选择收藏" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "移动到此处(&M)" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "复制到此处(&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "取消" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "修改时间" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "标记" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "属性:%1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "冲突解决" + +#: widgets/conflictresolvedialog.cpp:192 +#, kde-format +msgctxt "@action:button" +msgid "Take my version" +msgstr "采用我的版本" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "采用他们的版本" + +#: widgets/conflictresolvedialog.cpp:204 +#, kde-format +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "保留两个版本" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" +"您的更改会与其他人的更改发生冲突。
除非丢弃一个版本,否则您必须手动合" +"并这些更改。
单击“打开文本编辑器”以保留文本" +"副本,然后选择哪个版本是最正确的,之后重新打开并再次修改它以添加缺少的内容。" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "数据" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "正在启动 Akonadi 服务器..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "正在停止 Akonadi 服务器..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "移动到此处(&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "复制到此处(&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "链接到此处(&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "取消(&A)" + +#: widgets/erroroverlay.cpp:227 +#, kde-format +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "" +"无法连接到个人信息管理服务。\n" +"\n" +"%1" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "个人信息管理服务正在启动..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "个人信息管理服务正在关闭..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "个人信息管理服务正在进行数据库更新。" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"个人信息管理服务正在进行数据库更新。\n" +"这通常在软件更新之后进行,并且对性能优化是必要的。\n" +"取决于个人信息的大小,这一过程可能会花费数分钟。" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "Akonadi 个人信息管理框架服务未运行。没有该服务,此应用程序无法使用。" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "启动" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi 个人信息管理框架无法工作。\n" +"请单击“详情...”以获取问题相关的进一步信息。" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi 个人信息管理框架服务无法操作。" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "详情..." + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Do you want to remove account '%1'?" +msgstr "您真的要删除账户“%1”吗?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "是否删除账户?" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "接收账户(至少添加一个):" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "添加(&D)..." + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "修改(&M)..." + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "删除(&E)" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, kde-format +msgid "Restart" +msgstr "重启" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "最近文件夹" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, kde-format +msgid "Rename Favorite" +msgstr "重命名收藏夹" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, kde-format +msgid "Name:" +msgstr "名称:" + +#: widgets/selftestdialog.cpp:60 +#, kde-format +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi 服务器自检" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "保存报告..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "将报告复制到剪贴板" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "根据您目前的 Akonadi 服务器配置,所需的 QtSQL 驱动“%1”已经找到。" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"根据您目前的 Akonadi 服务器配置,我们需要 QtSQL 驱动“%1”。\n" +"这些是您已经安装的驱动:%2。\n" +"请确认所需的驱动已经安装。" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "已找到数据库驱动。" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "找不到数据库驱动。" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL 服务器程序尚未测试。" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "目前的配置不需要调用内部的 MySQL 服务器。" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"您已经将 Akonadi 配置为使用 MySQL 服务器“%1”。\n" +"请确认您已经安装了 MySQL 服务器,已设定了正确的路径,并确保您对服务器程序拥有" +"必要的读和执行权限。服务器端程序一般称为“mysqld”;其所处位置取决于您使用的操" +"作系统发行版本。" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "找不到 MySQL 服务器。" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "MySQL 服务器程序不可读。" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "MySQL 服务器程序不可执行。" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "发现不可预料的 MySQL 程序名。" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "已找到 MySQL 服务器。" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "已找到 MySQL 服务器:%1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL 服务器程序可执行。" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "执行 MySQL 服务器程序“%1”失败,错误消息如下:“%2”" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "执行 MySQL 服务器程序失败。" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL 服务器错误日志未检验。" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "找不到 MySQL 错误日志。" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "MySQL 服务器程序在启动过程中没有汇报任何错误信息。日志文件位于“%1”。" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "MySQL 错误日志不可读。" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "已找到 MySQL 服务器错误日志文件,但它不可读:%1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL 服务器日志包含错误信息。" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL 服务器错误日志文件“%1”包含错误信息。" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL 服务器日志包含警告信息。" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL 服务器日志文件“%1”包含警告信息。" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL 服务器日志中没有错误信息。" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL 服务器日志文件“%1”中不包含任何错误或警告信息。" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL 服务器配置未经测试。" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "已找到 MySQL 服务器默认配置。" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "已找到位于 %1 的 MySQL 服务器默认配置,并且可读。" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "找不到 MySQL 服务器默认配置。" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"找不到 MySQL 服务器默认配置,或是配置不可读取。请检查 Akonadi 的安装是否完" +"整,并且已经授予了所有需要的访问权限。" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "MySQL 服务器的自定义配置不可用。" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "找不到 MySQL 服务器的自定义配置,但这不是必需的。" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "已找到 MySQL 服务器的自定义配置。" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "已找到位于 %1 的 MySQL 服务器自定义配置,并且配置可读。" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "MySQL 服务器的自定义配置不可读。" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "" +"已找到位于 %1 的 MySQL 服务器自定义配置,但配置不可读。请检查您的访问权限。" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "找不到 MySQL 服务器配置,或配置不可读。" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "找不到 MySQL 服务器配置,或配置不可读。" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL 服务器配置可用。" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "已找到位于 %1 的 MySQL 服务器配置,并且配置可读。" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "无法连接到 PostgreSQL 服务器。" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "找到 PostgreSQL 服务器。" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "已找到 PostgreSQL 服务器,且连接正常。" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "找不到 akonadictl 程序" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"“akonadictl”程序需要能在 $PATH 路径中访问到。请确认您已经安装了 Akonadi 服务" +"器。" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "已找到可用的 akonadictl 程序" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"成功找到可用的 Akonadi 服务器控制程序“%1”。\n" +"结果:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "已经找到 akonadictl,但此程序不可用" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"已经找到 Akonadi 服务器控制程序“%1”,但无法成功执行。\n" +"结果:\n" +"%2\n" +"请确认您已经正确安装了 Akonadi 服务器。" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi 控制进程已经注册到 D-Bus。" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "Akonadi 控制进程注册到 D-Bus 通常意味着它已经可以工作了。" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi 控制进程未能注册到 D-Bus。" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi 控制进程未能注册到 D-Bus 通常意味着它还没有启动,或者在启动过程中遇到" +"了严重错误。" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi 服务器进程已经注册到 D-Bus。" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "Akonadi 服务器进程注册到 D-Bus 通常意味着它已经可以工作了。" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi 服务器进程未能注册到 D-Bus。" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi 服务器进程未能注册到 D-Bus 通常意味着它还没有启动,或者在启动过程中遇" +"到了严重错误。" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "无法进行协议版本检查。" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "如果不和服务器建立连接,就无法检查协议的版本是否符合要求。" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "服务器的协议版本过旧。" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, kde-format +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"服务器的协议版本是 %1,但客户端需要的版本至少是 %2。如果您最近更新了 KDE " +"PIM,请确认重新启动 Akonadi 和 KDE PIM 应用程序。" + +#: widgets/selftestdialog.cpp:454 +#, kde-format +msgid "Server protocol version is too new." +msgstr "服务器的协议版本过新。" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "Server protocol version matches." +msgstr "服务器的协议版本匹配。" + +#: widgets/selftestdialog.cpp:460 +#, kde-format +msgid "The current Protocol version is %1." +msgstr "当前协议版本为 %1。" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "已经找到资源代理。" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "已经找到至少一种资源代理。" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "找不到资源代理。" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"找不到任何资源代理,这样的话 Akonadi 是没有作用的。这通常意味着还没有安装过资" +"源代理,或者这是一个设置问题。已经搜索过下面的路径:“%1”;XDG_DATA_DIRS 环境" +"变量的值是“%2”,请确认这里面包含所有 Akonadi 代理的安装目录。" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "找不到当前的 Akonadi 服务器错误日志。" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "在 Akonadi 服务器的启动过程中没有报告任何错误。" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "已经找到当前的 Akonadi 服务器错误日志。" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "在 Akonadi 服务器的启动过程中报告了错误。日志可以在 %1 中找到。" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "找不到前一个 Akonadi 服务器错误日志。" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "在 Akonadi 服务器的前一次启动过程中没有报告任何错误。" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "已经找到前一个 Akonadi 服务器错误日志。" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "在 Akonadi 服务器的前一次启动过程中报告了错误。日志可以在 %1 中找到。" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "找不到当前的 Akonadi 控制器错误日志。" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "在 Akonadi 控制器的启动过程中没有报告任何错误。" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "已经找到当前的 Akonadi 控制器错误日志。" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "在 Akonadi 控制器的启动过程中报告了错误。日志可以在 %1 中找到。" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "找不到前一个 Akonadi 控制器错误日志。" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "在 Akonadi 控制器的前一次启动过程中没有报告任何错误。" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "已经找到前一个 Akonadi 控制器错误日志。" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "在 Akonadi 控制器的前一次启动过程中报告了错误。日志可以在 %1 中找到。" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi 以 root 启动" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"以 root/管理员身份运行面向 Internet 的应用该程序将会将您暴露于许多安全隐患之" +"下。为安全起见,此 Akonadi 安装所使用的 MySQL 无法允许其自身以 root 身份运" +"行。" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi 未以 root 启动" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "" +"为系统安全起见,我们建议您将 Akonadi 设置为以 root/管理员用户身份运行。但目前" +"系统的设置与此建议并不相符。" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "保存检测报告" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Error" +msgstr "错误" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "无法打开文件“%1”" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"启动 Akonadi 服务器的过程中出错,接下来的自检会帮助您追查并解决这个问题。如果" +"您想请求我们的支持或报告错误,请一定要包含这段报告内容。" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "详情" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, kde-format +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

要获得更多和排错有关的提示,请访问userbase.kde.org/Akonadi。

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "新建文件夹...(&N)" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "新建" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "&Delete Folder" +msgstr "删除文件夹(&D)" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "删除" + +#: widgets/standardactionmanager.cpp:80 +#, kde-format +msgid "&Synchronize Folder" +msgstr "同步文件夹(&S)" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "同步" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "文件夹属性(&P)" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "属性" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "粘贴(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "粘贴" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "管理本地订阅(&S)..." + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "管理本地订阅" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "添加到收藏夹" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "添加到收藏夹" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "从收藏夹中移除" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "从收藏夹中移除" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "重命名收藏夹..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "重命名" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "复制文件夹到..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "复制到" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "复制项目到..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "移动项目到..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "移动到" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "移动文件夹到..." + +#: widgets/standardactionmanager.cpp:148 +#, kde-format +msgid "&Cut Item" +msgstr "剪切项目(&C)" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "剪切" + +#: widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "&Cut Folder" +msgstr "剪切文件夹(&C)" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "创建资源" + +#: widgets/standardactionmanager.cpp:151 +#, kde-format +msgid "Delete Resource" +msgstr "删除资源" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "资源属性(&R)" + +#: widgets/standardactionmanager.cpp:161 +#, kde-format +msgid "Synchronize Resource" +msgstr "同步资源" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "离线工作" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "递归同步文件夹(&S)" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "递归同步" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "移动文件夹到回收站(&M)" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "移动文件夹到回收站" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "移动项目到回收站(&M)" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "移动项目到回收站" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "从回收站恢复文件夹(&R)" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "从回收站恢复文件夹" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "从回收站恢复项目(&R)" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "从回收站恢复项目" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "从回收站恢复收藏(&R)" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "从回收站恢复收藏" + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "同步收藏夹(&S)" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "同步收藏夹" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "同步文件夹树" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "复制 %1 个文件夹(&C)" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "复制 %1 项(&C)" + +#: widgets/standardactionmanager.cpp:342 +#, kde-format +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "剪切 %1 个项目(&C)" + +#: widgets/standardactionmanager.cpp:343 +#, kde-format +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "剪切 %1 个文件夹(&C)" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "删除 %1 项(&D)" + +#: widgets/standardactionmanager.cpp:345 +#, kde-format +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "删除 %1 个文件夹(&D)" + +#: widgets/standardactionmanager.cpp:346 +#, kde-format +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "同步 %1 个文件夹(&S)" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "删除 %1 个资源(&D)" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "同步 %1 个资源(&S)" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "复制 %1 个文件夹" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "复制 %1 项" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "剪切 %1 项" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "剪切 %1 个文件夹" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "删除 %1 项" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "删除 %1 个文件夹" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "同步 %1 个文件夹" + +#: widgets/standardactionmanager.cpp:357 +#, kde-format +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "删除 %1 个资源" + +#: widgets/standardactionmanager.cpp:358 +#, kde-format +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "同步 %1 个资源" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "名称" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "您确定要删除 %1 个文件夹及其子文件夹吗?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "删除文件夹吗?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "无法删除文件夹:%1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "文件夹删除失败" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "文件夹 %1 的属性" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "您确定要删除 %1 项吗?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "删除项目吗?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "无法删除项目:%1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "项目删除失败" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "重命名收藏夹" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "名称:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "新建资源" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "无法创建资源:%1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "资源创建失败" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "您确定要删除 %1 个资源吗?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "删除资源吗?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "无法粘贴数据:%1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "粘贴失败" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "无法在文件夹名称中添加“/”。" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "创建新文件夹错误" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "无法在文件夹名称开头或结尾添加“.”。" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "在同步文件夹“%1”前需要让资源在线。您想让它在线吗?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "账户“%1”离线" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "转为在线" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "移动到此文件夹" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "复制到此文件夹" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgid "Failed to update subscription: %1" +msgstr "更新订阅失败: %1" + +#: widgets/subscriptiondialog.cpp:93 +#, kde-format +msgctxt "@title" +msgid "Subscription Error" +msgstr "订阅错误" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, kde-format +msgid "Local Subscriptions" +msgstr "本地订阅" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "搜索:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, kde-format +msgid "&Subscribed only" +msgstr "只显示已订阅的(&S)" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, kde-format +msgid "Su&bscribe" +msgstr "订阅(&B)" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, kde-format +msgid "&Unsubscribe" +msgstr "退订(&U)" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "新建标签失败" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "新建标签时发生错误" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "您确定要删除标签 %1吗?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "删除标签" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "删除标签" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, kde-format +msgid "Select tags that should be applied." +msgstr "选择要应用的标签。" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, kde-format +msgid "Create new tag" +msgstr "新建标签" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, kde-format +msgid "Manage Tags" +msgstr "管理标签" + +#: widgets/tagselectioncombobox.cpp:124 +#, kde-format +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "选择标签" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "选择标签" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "清除" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "点击添加标签" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi XML 转换器" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "转换 Akonadi 收藏子树为 XML 文件。" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "数据未载入。" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "未指定文件名" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "无法打开数据文件“%1”。" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "文件 %1 不存在。" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "无法解析数据文件“%1”。" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "表模式定义无法加载和解析。" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "无法创建表模式解析上下文。" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "无法创建表模式。" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "无法创建表模式验证上下文。" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "无效的文件格式。" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "无法解析数据文件:%1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "无法找到收藏 %1" diff --git a/po/zh_TW/akonadi_knut_resource.po b/po/zh_TW/akonadi_knut_resource.po new file mode 100644 index 0000000..d2e70c5 --- /dev/null +++ b/po/zh_TW/akonadi_knut_resource.po @@ -0,0 +1,85 @@ +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# +# Frank Weng (a.k.a. Franklin) , 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: akonadi_knut_resource\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-01-27 03:05+0100\n" +"PO-Revision-Date: 2010-07-08 16:24+0800\n" +"Last-Translator: Frank Weng (a.k.a. Franklin) \n" +"Language-Team: Chinese Traditional \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 1.0\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: knutresource.cpp:60 +#, kde-format +msgid "No data file selected." +msgstr "沒有選取資料檔案。" + +#: knutresource.cpp:77 +#, kde-format +msgid "File '%1' loaded successfully." +msgstr "檔案 %1 已成功載入。" + +#: knutresource.cpp:104 +#, kde-format +msgid "Select Data File" +msgstr "選取資料檔" + +#: knutresource.cpp:106 +#, kde-format +msgctxt "Filedialog filter for Akonadi data file" +msgid "Akonadi Knut Data File" +msgstr "Akonadi Knut 資料檔" + +#: knutresource.cpp:148 knutresource.cpp:168 knutresource.cpp:321 +#, kde-format +msgid "No item found for remoteid %1" +msgstr "找不到遠端代碼 %1 的項目" + +#: knutresource.cpp:186 +#, kde-format +msgid "Parent collection not found in DOM tree." +msgstr "在 DOM 樹中沒有找到父收藏。" + +#: knutresource.cpp:194 +#, kde-format +msgid "Unable to write collection." +msgstr "無法寫入收藏。" + +#: knutresource.cpp:206 +#, kde-format +msgid "Modified collection not found in DOM tree." +msgstr "在 DOM 樹中沒有找到變更的收藏。" + +#: knutresource.cpp:236 +#, kde-format +msgid "Deleted collection not found in DOM tree." +msgstr "在 DOM 樹中沒有找到刪除的收藏。" + +#: knutresource.cpp:250 knutresource.cpp:308 knutresource.cpp:315 +#, kde-format +msgid "Parent collection '%1' not found in DOM tree." +msgstr "在 DOM 樹中沒有找到父收藏 %1。" + +#: knutresource.cpp:258 knutresource.cpp:328 +#, kde-format +msgid "Unable to write item." +msgstr "無法寫入項目。" + +#: knutresource.cpp:272 +#, kde-format +msgid "Modified item not found in DOM tree." +msgstr "在 DOM 樹中沒有找到變更的項目。" + +#: knutresource.cpp:287 +#, kde-format +msgid "Deleted item not found in DOM tree." +msgstr "在 DOM 樹中沒有找到刪除的項目。" diff --git a/po/zh_TW/libakonadi5.po b/po/zh_TW/libakonadi5.po new file mode 100644 index 0000000..6d6d41b --- /dev/null +++ b/po/zh_TW/libakonadi5.po @@ -0,0 +1,2578 @@ +# translation of libakonadi.po to Chinese Traditional +# Copyright (C) YEAR This_file_is_part_of_KDE +# This file is distributed under the same license as the PACKAGE package. +# Franklin Weng , 2010, 2011, 2012, 2013, 2014, 2015. +# +# Franklin Weng , 2007, 2008. +# Frank Weng (a.k.a. Franklin) , 2008, 2009, 2010. +msgid "" +msgstr "" +"Project-Id-Version: libakonadi\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2021-07-16 01:42+0000\n" +"PO-Revision-Date: 2018-03-29 13:00+0800\n" +"Last-Translator: Cheng-Chia Tseng \n" +"Language-Team: Chinese Traditional \n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.0.6\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Franklin Weng" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "franklin@goodhorse.idv.tw" + +#: agentbase/accountsintegration.cpp:86 +#, kde-format +msgid "There is currently no account configured." +msgstr "" + +#: agentbase/accountsintegration.cpp:103 +#, kde-format +msgid "Accounts integration is not supported" +msgstr "" + +#: agentbase/agentbase.cpp:359 agentbase/preprocessorbase_p.cpp:26 +#, kde-format +msgid "Unable to register object at dbus: %1" +msgstr "無法在 dbus 註冊物件:%1" + +#: agentbase/agentbase.cpp:448 +#, kde-format +msgctxt "Name and type of Akonadi resource" +msgid "%1 of type %2" +msgstr "型態為 %2 的 %1" + +#: agentbase/agentbase.cpp:897 +#, kde-format +msgid "Agent identifier" +msgstr "代理程式辨識器" + +#: agentbase/agentbase.cpp:903 +#, kde-format +msgid "Akonadi Agent" +msgstr "Akonadi Agent" + +#: agentbase/agentbase_p.h:49 agentbase/resourcescheduler.cpp:281 +#, kde-format +msgctxt "@info:status Application ready for work" +msgid "Ready" +msgstr "已就緒" + +#: agentbase/agentbase_p.h:51 +#, kde-format +msgctxt "@info:status" +msgid "Offline" +msgstr "離線" + +#: agentbase/agentbase_p.h:56 +#, kde-format +msgctxt "@info:status" +msgid "Syncing..." +msgstr "同步中..." + +#: agentbase/agentbase_p.h:61 +#, kde-format +msgctxt "@info:status" +msgid "Error." +msgstr "錯誤:" + +#: agentbase/agentbase_p.h:66 +#, kde-format +msgctxt "@info:status" +msgid "Not configured" +msgstr "未設定" + +#: agentbase/resourcebase.cpp:525 +#, kde-format +msgctxt "@label command line option" +msgid "Resource identifier" +msgstr "資源辨識器" + +#: agentbase/resourcebase.cpp:531 +#, kde-format +msgid "Akonadi Resource" +msgstr "Akonadi 資源" + +#: agentbase/resourcebase.cpp:579 +#, kde-format +msgctxt "@info" +msgid "Invalid item retrieved" +msgstr "取得不合法的項目" + +#: agentbase/resourcebase.cpp:602 +#, kde-format +msgctxt "@info" +msgid "Error while creating item: %1" +msgstr "建立項目時發生錯誤:%1" + +#: agentbase/resourcebase.cpp:626 +#, kde-format +msgctxt "@info" +msgid "Error while updating collection: %1" +msgstr "更新收藏時發生錯誤:%1" + +#: agentbase/resourcebase.cpp:713 +#, kde-format +msgctxt "@info" +msgid "Updating local collection failed: %1." +msgstr "更新本地收藏失敗:%1。" + +#: agentbase/resourcebase.cpp:718 +#, kde-format +msgctxt "@info" +msgid "Updating local items failed: %1." +msgstr "更新本地項目失敗:%1。" + +#: agentbase/resourcebase.cpp:737 +#, kde-format +msgctxt "@info" +msgid "Cannot fetch item in offline mode." +msgstr "無法於離線模式抓取項目。" + +#: agentbase/resourcebase.cpp:916 +#, kde-format +msgctxt "@info:status" +msgid "Syncing folder '%1'" +msgstr "同步資料夾 %1 中" + +#: agentbase/resourcebase.cpp:936 agentbase/resourcebase.cpp:943 +#, kde-format +msgid "Failed to retrieve collection for sync." +msgstr "無法抓取收藏以同步。" + +#: agentbase/resourcebase.cpp:983 +#, kde-format +msgid "Failed to retrieve collection for attribute sync." +msgstr "無法抓取收藏以同步屬性。" + +#: agentbase/resourcebase.cpp:1037 +#, kde-format +msgid "The requested item no longer exists" +msgstr "此要求已不存在" + +#: agentbase/resourcescheduler.cpp:500 agentbase/resourcescheduler.cpp:506 +#, kde-format +msgctxt "@info" +msgid "Job canceled." +msgstr "工作已取消。" + +#: core/collectionpathresolver.cpp:99 core/collectionpathresolver.cpp:118 +#, kde-format +msgid "No such collection." +msgstr "沒有此收藏。" + +#: core/collectionsync.cpp:508 core/collectionsync.cpp:652 +#, kde-format +msgid "Found unresolved orphan collections" +msgstr "找到未解決的孤單收藏" + +#: core/conflicthandler.cpp:53 +#, kde-format +msgid "Did not find other item for conflict handling" +msgstr "處理衝突時找不到另一個項目" + +#: core/jobs/agentinstancecreatejob.cpp:71 +#, kde-format +msgid "Unable to access D-Bus interface of created agent." +msgstr "無法存取建立代理的 D-Bus 介面。" + +#: core/jobs/agentinstancecreatejob.cpp:93 +#, kde-format +msgid "Agent instance creation timed out." +msgstr "建立代理程式實體時逾時。" + +#: core/jobs/agentinstancecreatejob.cpp:154 +#, kde-format +msgid "Unable to obtain agent type '%1'." +msgstr "無法取得代理程式型態 %1。" + +#: core/jobs/agentinstancecreatejob.cpp:162 +#, kde-format +msgid "Unable to create agent instance." +msgstr "無法建立代理程式實體。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:76 +#, kde-format +msgid "Invalid collection instance." +msgstr "不合法的收藏實體。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:83 +#: core/jobs/resourcesynchronizationjob.cpp:91 +#, kde-format +msgid "Invalid resource instance." +msgstr "不合法的資源實體。" + +#: core/jobs/collectionattributessynchronizationjob.cpp:105 +#: core/jobs/resourcesynchronizationjob.cpp:116 +#, kde-format +msgid "Unable to obtain D-Bus interface for resource '%1'" +msgstr "無法取得資源 %1 的 D-Bus 介面" + +#: core/jobs/collectionattributessynchronizationjob.cpp:128 +#, kde-format +msgid "Collection attributes synchronization timed out." +msgstr "收藏屬性同步逾時。" + +#: core/jobs/collectioncopyjob.cpp:55 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid collection to copy" +msgstr "不合法的收藏" + +#: core/jobs/collectioncopyjob.cpp:61 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid destination collection" +msgstr "不合法的收藏" + +#: core/jobs/collectioncreatejob.cpp:50 +#, kde-format +msgid "Invalid parent" +msgstr "不合法的父資源" + +#: core/jobs/collectioncreatejob.cpp:98 +#, fuzzy, kde-format +#| msgid "Failed to retrieve collection for sync." +msgid "Failed to parse Collection from response" +msgstr "無法抓取收藏以同步。" + +#: core/jobs/collectiondeletejob.cpp:52 +#, kde-format +msgid "Invalid collection" +msgstr "不合法的收藏" + +#: core/jobs/collectionfetchjob.cpp:212 +#, kde-format +msgid "Invalid collection given." +msgstr "給定了不合法的收藏。" + +#: core/jobs/collectionmovejob.cpp:52 core/jobs/itemmovejob.cpp:90 +#, kde-format +msgid "No objects specified for moving" +msgstr "沒有指定要移動的物件" + +#: core/jobs/collectionmovejob.cpp:59 core/jobs/itemmovejob.cpp:97 +#: core/jobs/linkjobimpl_p.h:38 +#, kde-format +msgid "No valid destination specified" +msgstr "沒有指定合法的目標" + +#: core/jobs/invalidatecachejob.cpp:58 +#, kde-format +msgid "Invalid collection." +msgstr "不合法的收藏。" + +#: core/jobs/itemcreatejob.cpp:107 +#, fuzzy, kde-format +#| msgid "Invalid collection" +msgid "Invalid parent collection" +msgstr "不合法的收藏" + +#: core/jobs/job.cpp:337 +#, kde-format +msgid "Cannot connect to the Akonadi service." +msgstr "無法連線到 Akonadi 服務。" + +#: core/jobs/job.cpp:340 +#, kde-format +msgid "" +"The protocol version of the Akonadi server is incompatible. Make sure you " +"have a compatible version installed." +msgstr "此 Akonadi 伺服器的協定版本不相容。請確定您安裝了相容的版本。" + +#: core/jobs/job.cpp:343 +#, kde-format +msgid "User canceled operation." +msgstr "使用者取消操作。" + +#: core/jobs/job.cpp:348 +#, kde-format +msgid "Unknown error." +msgstr "未知的錯誤。" + +#: core/jobs/job.cpp:387 +#, kde-format +msgid "Unexpected response" +msgstr "" + +#: core/jobs/relationcreatejob.cpp:42 core/jobs/relationdeletejob.cpp:42 +#, kde-format +msgid "Failed to create relation." +msgstr "建立關聯時失敗。" + +#: core/jobs/resourcesynchronizationjob.cpp:142 +#, kde-format +msgid "Resource synchronization timed out." +msgstr "資源同步逾時。" + +#: core/jobs/specialcollectionshelperjobs.cpp:146 +#, kde-format +msgid "Could not fetch root collection of resource %1." +msgstr "無法抓取收藏 %1 的根收藏。" + +#: core/jobs/specialcollectionshelperjobs.cpp:193 +#, kde-format +msgid "No resource ID given." +msgstr "沒有給定資源代碼。" + +#: core/jobs/specialcollectionshelperjobs.cpp:330 +#, kde-format +msgid "Invalid resource identifier '%1'" +msgstr "不合法的資源辨識 %1。" + +#: core/jobs/specialcollectionshelperjobs.cpp:346 +#: core/jobs/specialcollectionshelperjobs.cpp:354 +#, kde-format +msgid "Failed to configure default resource via D-Bus." +msgstr "從 D-Bus 取得設定預設資源失敗。" + +#: core/jobs/specialcollectionshelperjobs.cpp:419 +#, kde-format +msgid "Failed to fetch the resource collection." +msgstr "無法抓取此資源收藏。" + +#: core/jobs/specialcollectionshelperjobs.cpp:606 +#, kde-format +msgid "Timeout trying to get lock." +msgstr "取得鎖定時發生逾時。" + +#: core/jobs/tagcreatejob.cpp:49 +#, kde-format +msgid "Failed to create tag." +msgstr "建立標籤時失敗。" + +#: core/jobs/trashjob.cpp:155 +#, kde-format +msgid "Move to trash collection failed, aborting trash operation" +msgstr "移到垃圾桶的動作失敗。放棄操作。" + +#: core/jobs/trashjob.cpp:218 core/jobs/trashrestorejob.cpp:155 +#, kde-format +msgid "Invalid items passed" +msgstr "不合法的項目已通過" + +#: core/jobs/trashjob.cpp:259 core/jobs/trashrestorejob.cpp:205 +#, kde-format +msgid "Invalid collection passed" +msgstr "不合法的收藏已通過" + +#: core/jobs/trashjob.cpp:376 core/jobs/trashrestorejob.cpp:347 +#, kde-format +msgid "No valid collection or empty itemlist" +msgstr "沒有合法收藏,或項目清單為空" + +#: core/jobs/trashrestorejob.cpp:90 +#, kde-format +msgid "Could not find restore collection and restore resource is not available" +msgstr "找不到儲存收藏,或儲存收藏無法存取" + +#: core/models/agentinstancemodel.cpp:186 +#, kde-format +msgctxt "@title:column, name of a thing" +msgid "Name" +msgstr "名稱" + +#: core/models/entitytreemodel.cpp:197 +#, fuzzy, kde-format +#| msgid "Loading..." +msgctxt "@info:status" +msgid "Loading..." +msgstr "載入中..." + +#: core/models/entitytreemodel.cpp:489 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@window:title" +msgid "Error" +msgstr "錯誤:" + +#: core/models/entitytreemodel.cpp:490 +#, kde-format +msgid "" +"The target collection '%1' contains already\n" +"a collection with name '%2'." +msgstr "目標收藏 %1 已包含了名為 %2 的收藏。" + +#: core/models/entitytreemodel.cpp:659 +#, kde-format +msgctxt "@title:column Name of a thing" +msgid "Name" +msgstr "名稱" + +#: core/models/entitytreemodel_p.cpp:1331 +#, fuzzy, kde-format +#| msgid "Could not copy item:" +msgctxt "@info" +msgid "Could not copy item: %1" +msgstr "無法複製項目:" + +#: core/models/entitytreemodel_p.cpp:1333 +#, fuzzy, kde-format +#| msgid "Could not copy collection:" +msgctxt "@info" +msgid "Could not copy collection: %1" +msgstr "無法複製收藏:" + +#: core/models/entitytreemodel_p.cpp:1335 +#, fuzzy, kde-format +#| msgid "Could not move item:" +msgctxt "@info" +msgid "Could not move item: %1" +msgstr "無法移動項目:" + +#: core/models/entitytreemodel_p.cpp:1337 +#, fuzzy, kde-format +#| msgid "Could not move collection:" +msgctxt "@info" +msgid "Could not move collection: %1" +msgstr "無法移動收藏:" + +#: core/models/entitytreemodel_p.cpp:1339 +#, fuzzy, kde-format +#| msgid "Could not link entity:" +msgctxt "@info" +msgid "Could not link entity: %1" +msgstr "無法連結實體:" + +#: core/models/entitytreemodel_p.cpp:1341 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgctxt "@title:window" +msgid "Error" +msgstr "錯誤:" + +#: core/models/favoritecollectionsmodel.cpp:408 +#, kde-format +msgid "Favorite Folders" +msgstr "我的最愛資料夾" + +#: core/models/statisticsproxymodel.cpp:86 +#, kde-format +msgid "Total Messages" +msgstr "信件總數" + +#: core/models/statisticsproxymodel.cpp:88 +#, kde-format +msgid "Unread Messages" +msgstr "未讀信件" + +#: core/models/statisticsproxymodel.cpp:98 +#, kde-format +msgid "Quota" +msgstr "大小限制" + +#: core/models/statisticsproxymodel.cpp:105 +#, kde-format +msgid "Storage Size" +msgstr "儲存大小" + +#: core/models/statisticsproxymodel.cpp:111 +#, kde-format +msgid "Subfolder Storage Size" +msgstr "子資料夾儲存大小" + +#: core/models/statisticsproxymodel.cpp:228 +#, kde-format +msgctxt "number of unread entities in the collection" +msgid "Unread" +msgstr "未讀" + +#: core/models/statisticsproxymodel.cpp:229 +#, kde-format +msgctxt "number of entities in the collection" +msgid "Total" +msgstr "總計" + +#: core/models/statisticsproxymodel.cpp:230 +#, kde-format +msgctxt "collection size" +msgid "Size" +msgstr "大小" + +#: core/models/tagmodel.cpp:67 +#, kde-format +msgid "Tag" +msgstr "標籤" + +#: core/partfetcher.cpp:49 +#, kde-format +msgid "Unable to fetch item for index" +msgstr "無法抓取項目以建立索引" + +#: core/partfetcher.cpp:64 +#, kde-format +msgid "Index is no longer available" +msgstr "索引已無法使用" + +#: core/partfetcher.cpp:117 +#, kde-format +msgid "Payload part '%1' is not available for this index" +msgstr "此索引的資料部份 %1 已無法使用" + +#: core/partfetcher.cpp:126 +#, kde-format +msgid "No session available for this index" +msgstr "此索引沒有工作階段可用" + +#: core/partfetcher.cpp:135 +#, kde-format +msgid "No item available for this index" +msgstr "此索引沒有可用的項目" + +#: core/pluginloader.cpp:145 +#, kde-format +msgid "Unnamed plugin" +msgstr "未命名的外掛程式" + +#: core/pluginloader.cpp:151 +#, kde-format +msgid "No description available" +msgstr "沒有可用的描述" + +#: core/servermanager.cpp:268 +#, kde-format +msgid "" +"The Akonadi server protocol version differs from the protocol version used " +"by this application.\n" +"If you recently updated your system please log out and back in to make sure " +"all applications use the correct protocol version." +msgstr "" + +#: core/servermanager.cpp:285 +#, kde-format +msgid "" +"There are no Akonadi Agents available. Please verify your KDE PIM " +"installation." +msgstr "" + +#: core/session.cpp:186 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is older (%1) than ours (%2). If " +"you updated your system recently please restart the Akonadi server." +msgstr "" + +#: core/session.cpp:195 +#, kde-format +msgid "" +"Protocol version mismatch. Server version is newer (%1) than ours (%2). If " +"you updated your system recently please restart all KDE PIM applications." +msgstr "" + +#: selftest/main.cpp:19 +#, kde-format +msgid "Akonadi Self Test" +msgstr "Akonadi 自我測試" + +#: selftest/main.cpp:21 +#, kde-format +msgid "Checks and reports state of Akonadi server" +msgstr "Akonadi 伺服器檢查與報告狀態" + +#: selftest/main.cpp:23 +#, kde-format +msgid "(c) 2008 Volker Krause " +msgstr "(c) 2008 Volker Krause " + +#: widgets/agentactionmanager.cpp:34 +#, kde-format +msgid "&New Agent Instance..." +msgstr "新增代理程式實體(&N)..." + +#: widgets/agentactionmanager.cpp:35 +#, kde-format +msgid "&Delete Agent Instance" +msgstr "刪除代理程式實體(&D)" + +#: widgets/agentactionmanager.cpp:36 +#, kde-format +msgid "&Configure Agent Instance" +msgstr "設定代理程式實體(&C)" + +#: widgets/agentactionmanager.cpp:52 +#, kde-format +msgctxt "@title:window" +msgid "New Agent Instance" +msgstr "新增代理程式實體" + +#: widgets/agentactionmanager.cpp:54 +#, kde-format +msgid "Could not create agent instance: %1" +msgstr "無法建立代理程式實體:%1" + +#: widgets/agentactionmanager.cpp:56 +#, kde-format +msgid "Agent instance creation failed" +msgstr "建立代理程式實體失敗。" + +#: widgets/agentactionmanager.cpp:58 +#, kde-format +msgctxt "@title:window" +msgid "Delete Agent Instance?" +msgstr "要刪除代理程式實體嗎?" + +#: widgets/agentactionmanager.cpp:62 +#, kde-format +msgid "Do you really want to delete the selected agent instance?" +msgstr "您確定要刪除所有選取的代理程式實體嗎?" + +#: widgets/agentconfigurationdialog.cpp:56 +#, kde-format +msgctxt "%1 = agent name" +msgid "%1 Configuration" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:85 +#, kde-format +msgid "%1 Handbook" +msgstr "" + +#: widgets/agentconfigurationdialog.cpp:86 +#, kde-format +msgid "About %1" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:97 +#, kde-format +msgid "The configuration dialog has been opened in another window" +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:104 +#, kde-format +msgid "Configuration for %1 is already opened elsewhere." +msgstr "" + +#: widgets/agentconfigurationwidget.cpp:106 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to register %1 configuration dialog." +msgstr "建立關聯時失敗。" + +#: widgets/cachepolicypage.cpp:41 widgets/cachepolicypage.cpp:46 +#, kde-format +msgid "minute" +msgid_plural "minutes" +msgstr[0] "分" + +#: widgets/cachepolicypage.cpp:63 +#, kde-format +msgid "Retrieval" +msgstr "收取" + +#. i18n: ectx: property (text), widget (QCheckBox, inherit) +#: widgets/cachepolicypage.ui:20 +#, kde-format +msgid "Use options from parent folder or account" +msgstr "使用父資料夾或帳號的選項" + +#. i18n: ectx: property (text), widget (QCheckBox, syncOnDemand) +#: widgets/cachepolicypage.ui:33 +#, kde-format +msgid "Synchronize when selecting this folder" +msgstr "選取此資料夾時同步" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/cachepolicypage.ui:42 +#, kde-format +msgid "Automatically synchronize after:" +msgstr "多久後自動同步:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, checkInterval) +#: widgets/cachepolicypage.ui:49 +#, kde-format +msgctxt "never check the cache" +msgid "Never" +msgstr "從不" + +#. i18n: ectx: property (suffix), widget (QSpinBox, checkInterval) +#. i18n: ectx: property (suffix), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:52 widgets/cachepolicypage.ui:162 +#, kde-format +msgid "minutes" +msgstr "分" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#. i18n: ectx: property (title), widget (KEditListWidget, localParts) +#: widgets/cachepolicypage.ui:89 widgets/cachepolicypage.ui:101 +#, kde-format +msgid "Locally Cached Parts" +msgstr "本地快取部件" + +#. i18n: ectx: property (title), widget (QGroupBox, retrievalOptionsGroupBox) +#: widgets/cachepolicypage.ui:127 +#, kde-format +msgid "Retrieval Options" +msgstr "收取選項" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveFullMessages) +#: widgets/cachepolicypage.ui:133 +#, fuzzy, kde-format +#| msgid "Always retrieve full messages" +msgid "Always retrieve full &messages" +msgstr "總是取出完整的信件" + +#. i18n: ectx: property (text), widget (QRadioButton, retrieveOnlyHeaders) +#: widgets/cachepolicypage.ui:143 +#, fuzzy, kde-format +#| msgid "Retrieve message bodies on demand" +msgid "&Retrieve message bodies on demand" +msgstr "只在必要時才取出信件的內文" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/cachepolicypage.ui:152 +#, kde-format +msgid "Keep message bodies locally for:" +msgstr "信件內文在本地端保存多久:" + +#. i18n: ectx: property (specialValueText), widget (QSpinBox, localCacheTimeout) +#: widgets/cachepolicypage.ui:159 +#, kde-format +msgctxt "no cache timeout" +msgid "Forever" +msgstr "永遠" + +#: widgets/collectiondialog.cpp:56 +#, kde-format +msgctxt "@info Displayed grayed-out inside the textbox, verb to search" +msgid "Search" +msgstr "搜尋" + +#: widgets/collectiondialog.cpp:64 +#, kde-format +msgid "Use folder by default" +msgstr "預設使用資料夾" + +#: widgets/collectiondialog.cpp:224 +#, kde-format +msgid "&New Subfolder..." +msgstr "新資料夾(&N)..." + +#: widgets/collectiondialog.cpp:226 +#, kde-format +msgid "Create a new subfolder under the currently selected folder" +msgstr "在目前資料夾下新增子資料夾" + +#: widgets/collectiondialog.cpp:264 widgets/standardactionmanager.cpp:360 +#, kde-format +msgctxt "@title:window" +msgid "New Folder" +msgstr "新資料夾" + +#: widgets/collectiondialog.cpp:264 +#, kde-format +msgctxt "@label:textbox, name of a thing" +msgid "Name" +msgstr "名稱" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:362 +#, kde-format +msgid "Could not create folder: %1" +msgstr "無法建立資料夾:%1" + +#: widgets/collectiondialog.cpp:285 widgets/standardactionmanager.cpp:363 +#, kde-format +msgid "Folder creation failed" +msgstr "資料夾建立失敗" + +#: widgets/collectiongeneralpropertiespage.cpp:27 +#, kde-format +msgctxt "@title:tab general properties page" +msgid "General" +msgstr "一般" + +#: widgets/collectiongeneralpropertiespage.cpp:54 +#, kde-format +msgctxt "@label" +msgid "One object" +msgid_plural "%1 objects" +msgstr[0] "%1 個物件" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectiongeneralpropertiespage.ui:16 +#, kde-format +msgid "&Name:" +msgstr "名稱(&N):" + +#. i18n: ectx: property (text), widget (QCheckBox, customIconCheckbox) +#: widgets/collectiongeneralpropertiespage.ui:29 +#, kde-format +msgid "&Use custom icon:" +msgstr "使用自訂圖示(&U):" + +#. i18n: ectx: property (icon), widget (KIconButton, customIcon) +#: widgets/collectiongeneralpropertiespage.ui:39 +#, kde-format +msgid "folder" +msgstr "資料夾" + +#. i18n: ectx: property (title), widget (QGroupBox, statsBox) +#: widgets/collectiongeneralpropertiespage.ui:62 +#, kde-format +msgid "Statistics" +msgstr "統計" + +#. i18n: ectx: property (text), widget (QLabel, label_2) +#: widgets/collectiongeneralpropertiespage.ui:68 +#, kde-format +msgctxt "object names" +msgid "Content:" +msgstr "內容:" + +#. i18n: ectx: property (text), widget (QLabel, countLabel) +#: widgets/collectiongeneralpropertiespage.ui:75 +#, kde-format +msgid "0 objects" +msgstr "0 個物件" + +#. i18n: ectx: property (text), widget (QLabel, label_3) +#: widgets/collectiongeneralpropertiespage.ui:82 +#: widgets/collectionmaintenancepage.ui:50 +#, kde-format +msgid "Size:" +msgstr "大小:" + +#. i18n: ectx: property (text), widget (QLabel, sizeLabel) +#: widgets/collectiongeneralpropertiespage.ui:89 +#, kde-format +msgid "0 Byte" +msgstr "0 位元組" + +#: widgets/collectionmaintenancepage.cpp:45 +#, kde-format +msgid "Remember that indexing can take some minutes." +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:70 +#, kde-format +msgid "Maintenance" +msgstr "" + +#: widgets/collectionmaintenancepage.cpp:129 +#, fuzzy, kde-format +#| msgctxt "@info" +#| msgid "Error while creating item: %1" +msgid "Error while retrieving indexed items count" +msgstr "建立項目時發生錯誤:%1" + +#: widgets/collectionmaintenancepage.cpp:132 +#, kde-format +msgid "Indexed %1 item in this folder" +msgid_plural "Indexed %1 items in this folder" +msgstr[0] "" + +#: widgets/collectionmaintenancepage.cpp:136 +#, kde-format +msgid "Calculating indexed items..." +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, filesGroup) +#: widgets/collectionmaintenancepage.ui:23 +#, kde-format +msgid "Files" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/collectionmaintenancepage.ui:29 +#, fuzzy, kde-format +#| msgid "Folder &Properties" +msgid "Folder type:" +msgstr "資料夾屬性(&P)" + +#. i18n: ectx: property (text), widget (QLabel, folderTypeLbl) +#. i18n: ectx: property (text), widget (QLabel, folderSizeLbl) +#. i18n: ectx: property (text), widget (QLabel, itemsCountLbl) +#. i18n: ectx: property (text), widget (QLabel, unreadItemsCountLbl) +#: widgets/collectionmaintenancepage.ui:36 +#: widgets/collectionmaintenancepage.ui:43 +#: widgets/collectionmaintenancepage.ui:79 +#: widgets/collectionmaintenancepage.ui:93 +#, kde-format +msgid "unknown" +msgstr "" + +#. i18n: ectx: property (title), widget (QGroupBox, itemsGroup) +#: widgets/collectionmaintenancepage.ui:66 +#, fuzzy, kde-format +#| msgid "Cut Item" +#| msgid_plural "Cut %1 Items" +msgid "Items" +msgstr "剪下 %1 個項目" + +#. i18n: ectx: property (text), widget (QLabel, label_5) +#: widgets/collectionmaintenancepage.ui:72 +#, fuzzy, kde-format +#| msgid "Total Messages" +msgid "Total items:" +msgstr "信件總數" + +#. i18n: ectx: property (text), widget (QLabel, label_7) +#: widgets/collectionmaintenancepage.ui:86 +#, fuzzy, kde-format +#| msgid "Unread Messages" +msgid "Unread items:" +msgstr "未讀信件" + +#. i18n: ectx: property (title), widget (QGroupBox, indexingGroup) +#: widgets/collectionmaintenancepage.ui:109 +#, kde-format +msgid "Indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, enableIndexingChkBox) +#: widgets/collectionmaintenancepage.ui:115 +#, kde-format +msgid "Enable fulltext indexing" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, indexedCountLbl) +#: widgets/collectionmaintenancepage.ui:122 +#, kde-format +msgid "Retrieving indexed items count ..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, reindexButton) +#: widgets/collectionmaintenancepage.ui:129 +#, fuzzy, kde-format +#| msgid "Recent Folder" +msgid "Reindex folder" +msgstr "最近使用的資料夾" + +#: widgets/collectionrequester.cpp:113 +#, kde-format +msgid "No Folder" +msgstr "沒有資料夾" + +#: widgets/collectionrequester.cpp:122 +#, kde-format +msgid "Open collection dialog" +msgstr "開啟收藏對話框" + +#: widgets/collectionrequester.cpp:143 +#, fuzzy, kde-format +#| msgid "Select a collection" +msgctxt "@title:window" +msgid "Select a collection" +msgstr "選擇一個收藏" + +#: widgets/collectionview.cpp:220 +#, kde-format +msgid "&Move here" +msgstr "移到這裡(&M)" + +#: widgets/collectionview.cpp:221 +#, kde-format +msgid "&Copy here" +msgstr "複製到這裡(&C)" + +#: widgets/collectionview.cpp:223 +#, kde-format +msgid "Cancel" +msgstr "取消" + +#: widgets/conflictresolvedialog.cpp:129 +#, kde-format +msgid "Modification Time" +msgstr "變更時間" + +#: widgets/conflictresolvedialog.cpp:141 +#, kde-format +msgid "Flags" +msgstr "旗標" + +#: widgets/conflictresolvedialog.cpp:156 widgets/conflictresolvedialog.cpp:164 +#: widgets/conflictresolvedialog.cpp:174 +#, kde-format +msgid "Attribute: %1" +msgstr "屬性:%1" + +#: widgets/conflictresolvedialog.cpp:186 +#, kde-format +msgctxt "@title:window" +msgid "Conflict Resolution" +msgstr "衝突解決" + +#: widgets/conflictresolvedialog.cpp:192 +#, fuzzy, kde-format +#| msgid "Take right one" +msgctxt "@action:button" +msgid "Take my version" +msgstr "使用右邊的" + +#: widgets/conflictresolvedialog.cpp:198 +#, kde-format +msgctxt "@action:button" +msgid "Take their version" +msgstr "" + +#: widgets/conflictresolvedialog.cpp:204 +#, fuzzy, kde-format +#| msgid "Keep both" +msgctxt "@action:button" +msgid "Keep both versions" +msgstr "兩個都保留" + +#: widgets/conflictresolvedialog.cpp:216 +#, kde-format +msgid "" +"Your changes conflict with those made by someone else meanwhile." +"
Unless one version can just be thrown away, you will have to integrate " +"those changes manually.
Click on \"Open text " +"editor\" to keep a copy of the texts, then select which version is most " +"correct, then re-open it and modify it again to add what's missing." +msgstr "" + +#: widgets/conflictresolvedialog.cpp:265 +#, kde-format +msgid "Data" +msgstr "資料" + +#: widgets/controlgui.cpp:230 +#, kde-format +msgid "Starting Akonadi server..." +msgstr "Akonadi 伺服器啟動中..." + +#: widgets/controlgui.cpp:236 +#, kde-format +msgid "Stopping Akonadi server..." +msgstr "Akonadi 伺服器停止中..." + +#: widgets/dragdropmanager.cpp:212 +#, kde-format +msgid "&Move Here" +msgstr "移到這裡(&M)" + +#: widgets/dragdropmanager.cpp:218 +#, kde-format +msgid "&Copy Here" +msgstr "複製到這裡(&C)" + +#: widgets/dragdropmanager.cpp:224 +#, kde-format +msgid "&Link Here" +msgstr "連結到這裡(&L)" + +#: widgets/dragdropmanager.cpp:228 +#, kde-format +msgid "C&ancel" +msgstr "取消(&A)" + +#: widgets/erroroverlay.cpp:227 +#, fuzzy, kde-format +#| msgid "Cannot connect to the Akonadi service." +msgctxt "%1 is a reason why" +msgid "" +"Cannot connect to the Personal information management service.\n" +"\n" +"%1" +msgstr "無法連線到 Akonadi 服務。" + +#: widgets/erroroverlay.cpp:231 widgets/erroroverlay.cpp:232 +#, kde-format +msgid "Personal information management service is starting..." +msgstr "正在啟動個人資訊管理服務..." + +#: widgets/erroroverlay.cpp:236 widgets/erroroverlay.cpp:237 +#, kde-format +msgid "Personal information management service is shutting down..." +msgstr "正在關閉個人資訊管理服務..." + +#: widgets/erroroverlay.cpp:241 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade." +msgstr "個人資訊管理服務政在執行資料庫更新。" + +#: widgets/erroroverlay.cpp:243 +#, kde-format +msgid "" +"Personal information management service is performing a database upgrade.\n" +"This happens after a software update and is necessary to optimize " +"performance.\n" +"Depending on the amount of personal information, this might take a few " +"minutes." +msgstr "" +"個人資訊管理服務正在執行資料庫更新。這個動作在軟體更新後會進行,\n" +"目的是為了將效能最佳化。可能會花一點時間,依據您個人資訊的多寡而定。" + +#. i18n: ectx: property (toolTip), widget (QWidget, notRunningPage) +#. i18n: ectx: property (text), widget (QLabel, notRunningDescription) +#: widgets/erroroverlay.ui:21 widgets/erroroverlay.ui:71 +#, kde-format +msgid "" +"The Akonadi personal information management service is not running. This " +"application cannot be used without it." +msgstr "Akonadi 個人資訊管理服務並未執行。此應用程式沒有它不行。" + +#. i18n: ectx: property (text), widget (QPushButton, startButton) +#: widgets/erroroverlay.ui:116 +#, kde-format +msgctxt "@action:button Start the Akonadi server" +msgid "Start" +msgstr "開始" + +#. i18n: ectx: property (toolTip), widget (QWidget, brokenPage) +#: widgets/erroroverlay.ui:158 +#, kde-format +msgid "" +"The Akonadi personal information management framework is not operational.\n" +"Click on \"Details...\" to obtain detailed information on this problem." +msgstr "" +"Akonadi 個人資訊管理架構無法操作。\n" +"請點擊「詳情...」來查看此問題的詳細資訊。" + +#. i18n: ectx: property (text), widget (QLabel, brokenDescription) +#: widgets/erroroverlay.ui:208 +#, kde-format +msgid "The Akonadi personal information management service is not operational." +msgstr "Akonadi 個人資訊管理服務無法操作。" + +#. i18n: ectx: property (text), widget (QPushButton, selfTestButton) +#: widgets/erroroverlay.ui:259 +#, kde-format +msgid "Details..." +msgstr "詳情..." + +#: widgets/manageaccountwidget.cpp:199 +#, fuzzy, kde-format +#| msgid "Do you really want to delete the search view '%1'?" +msgid "Do you want to remove account '%1'?" +msgstr "您確定要刪除搜尋檢視 %1 嗎?" + +#: widgets/manageaccountwidget.cpp:199 +#, kde-format +msgid "Remove account?" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/manageaccountwidget.ui:19 +#, kde-format +msgid "Incoming accounts (add at least one):" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mAddAccountButton) +#: widgets/manageaccountwidget.ui:36 +#, kde-format +msgid "A&dd..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mModifyAccountButton) +#: widgets/manageaccountwidget.ui:46 +#, kde-format +msgid "&Modify..." +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRemoveAccountButton) +#: widgets/manageaccountwidget.ui:56 +#, kde-format +msgid "R&emove" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, mRestartAccountButton) +#: widgets/manageaccountwidget.ui:73 +#, fuzzy, kde-format +#| msgctxt "@action:button Start the Akonadi server" +#| msgid "Start" +msgid "Restart" +msgstr "開始" + +#: widgets/recentcollectionaction.cpp:43 +#, kde-format +msgid "Recent Folder" +msgstr "最近使用的資料夾" + +#. i18n: ectx: property (windowTitle), widget (QDialog, RenameFavoriteDialog) +#: widgets/renamefavoritedialog.ui:26 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Rename Favorite" +msgid "Rename Favorite" +msgstr "重新命名我的最愛" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/renamefavoritedialog.ui:35 +#, fuzzy, kde-format +#| msgctxt "@label:textbox name of the folder" +#| msgid "Name:" +msgid "Name:" +msgstr "名稱:" + +#: widgets/selftestdialog.cpp:60 +#, fuzzy, kde-format +#| msgid "Akonadi Server Self-Test" +msgctxt "@title:window" +msgid "Akonadi Server Self-Test" +msgstr "Akonadi 伺服器自我測試" + +#: widgets/selftestdialog.cpp:71 +#, kde-format +msgid "Save Report..." +msgstr "儲存報告..." + +#: widgets/selftestdialog.cpp:73 +#, kde-format +msgid "Copy Report to Clipboard" +msgstr "將報告複製到剪貼簿" + +#: widgets/selftestdialog.cpp:189 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration and was found on your system." +msgstr "" +"您現在的 Akonadi 伺服器設定需要 QtSQL 驅動程式 %1,但是您的系統中找不到。" + +#: widgets/selftestdialog.cpp:191 +#, kde-format +msgid "" +"The QtSQL driver '%1' is required by your current Akonadi server " +"configuration.\n" +"The following drivers are installed: %2.\n" +"Make sure the required driver is installed." +msgstr "" +"您現在的 Akonadi 伺服器設定需要 QtSQL 驅動程式 %1。\n" +"目前安裝的是以下的驅動程式:%2。\n" +"請確定您已安裝需要的驅動程式。" + +#: widgets/selftestdialog.cpp:198 +#, kde-format +msgid "Database driver found." +msgstr "已找到資料庫驅動程式。" + +#: widgets/selftestdialog.cpp:200 +#, kde-format +msgid "Database driver not found." +msgstr "未找到資料庫驅動程式。" + +#: widgets/selftestdialog.cpp:208 +#, kde-format +msgid "MySQL server executable not tested." +msgstr "MySQL 伺服器執行檔尚未測試。" + +#: widgets/selftestdialog.cpp:208 widgets/selftestdialog.cpp:249 +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "The current configuration does not require an internal MySQL server." +msgstr "目前的設定不需要內部 MySQL 伺服器" + +#: widgets/selftestdialog.cpp:216 +#, kde-format +msgid "" +"You have currently configured Akonadi to use the MySQL server '%1'.\n" +"Make sure you have the MySQL server installed, set the correct path and " +"ensure you have the necessary read and execution rights on the server " +"executable. The server executable is typically called 'mysqld'; its location " +"varies depending on the distribution." +msgstr "" +"您目前設定 Akonadi 使用 MySQL 伺服器 %1。\n" +"請確定您有安裝 MySQL 伺服器,執行路徑有設定正確,以及您有足夠的權限讀取及執行" +"此伺服器。通常伺服器的執行檔都叫做 mysqld,放置的位置則依散布版不同而有所不" +"同。" + +#: widgets/selftestdialog.cpp:224 +#, kde-format +msgid "MySQL server not found." +msgstr "找不到 MySQL 伺服器。" + +#: widgets/selftestdialog.cpp:226 +#, kde-format +msgid "MySQL server not readable." +msgstr "無法讀取 MySQL 伺服器。" + +#: widgets/selftestdialog.cpp:228 +#, kde-format +msgid "MySQL server not executable." +msgstr "無法執行 MySQL 伺服器。" + +#: widgets/selftestdialog.cpp:230 +#, kde-format +msgid "MySQL found with unexpected name." +msgstr "找到 MySQL,但非預期的名稱。" + +#: widgets/selftestdialog.cpp:232 +#, kde-format +msgid "MySQL server found." +msgstr "已找到 MySQL 伺服器。" + +#: widgets/selftestdialog.cpp:238 +#, kde-format +msgid "MySQL server found: %1" +msgstr "已找到 MySQL 伺服器:%1" + +#: widgets/selftestdialog.cpp:239 +#, kde-format +msgid "MySQL server is executable." +msgstr "MySQL 伺服器可執行。" + +#: widgets/selftestdialog.cpp:241 +#, kde-format +msgid "" +"Executing the MySQL server '%1' failed with the following error message: '%2'" +msgstr "執行 MySQL 伺服器 %1 失敗,錯誤訊息為:%2" + +#: widgets/selftestdialog.cpp:242 +#, kde-format +msgid "Executing the MySQL server failed." +msgstr "執行 MySQL 伺服器失敗。" + +#: widgets/selftestdialog.cpp:249 +#, kde-format +msgid "MySQL server error log not tested." +msgstr "MySQL 伺服器錯誤紀錄尚未測試。" + +#: widgets/selftestdialog.cpp:257 +#, kde-format +msgid "No current MySQL error log found." +msgstr "沒有找到現有的 MySQL 伺服器錯誤紀錄。" + +#: widgets/selftestdialog.cpp:258 +#, kde-format +msgid "" +"The MySQL server did not report any errors during this startup. The log can " +"be found in '%1'." +msgstr "MySQL 伺服器啟動到 %1 時並未報告任何錯誤。紀錄可以在 %1 找到。" + +#: widgets/selftestdialog.cpp:264 +#, kde-format +msgid "MySQL error log not readable." +msgstr "無法讀取 MySQL 錯誤紀錄。" + +#: widgets/selftestdialog.cpp:265 +#, kde-format +msgid "A MySQL server error log file was found but is not readable: %1" +msgstr "有找到 MySQL 伺服器錯誤紀錄檔,但無法讀取:%1" + +#: widgets/selftestdialog.cpp:274 +#, kde-format +msgid "MySQL server log contains errors." +msgstr "MySQL 伺服器紀錄中包含錯誤。" + +#: widgets/selftestdialog.cpp:275 +#, kde-format +msgid "The MySQL server error log file '%1' contains errors." +msgstr "MySQL 伺服器錯誤紀錄檔 %1 中包含錯誤。" + +#: widgets/selftestdialog.cpp:285 +#, kde-format +msgid "MySQL server log contains warnings." +msgstr "MySQL 伺服器紀錄中包含警告。" + +#: widgets/selftestdialog.cpp:286 +#, kde-format +msgid "The MySQL server log file '%1' contains warnings." +msgstr "MySQL 伺服器紀錄檔 %1 中包含警告。" + +#: widgets/selftestdialog.cpp:289 +#, kde-format +msgid "MySQL server log contains no errors." +msgstr "MySQL 伺服器紀錄中並未包含任何錯誤。" + +#: widgets/selftestdialog.cpp:290 +#, kde-format +msgid "The MySQL server log file '%1' does not contain any errors or warnings." +msgstr "MySQL 伺服器紀錄檔 %1 中並未包含任何錯誤或警告。" + +#: widgets/selftestdialog.cpp:300 +#, kde-format +msgid "MySQL server configuration not tested." +msgstr "MySQL 伺服器設定尚未測試。" + +#: widgets/selftestdialog.cpp:309 +#, kde-format +msgid "MySQL server default configuration found." +msgstr "找到 MySQL 伺服器預設設定。" + +#: widgets/selftestdialog.cpp:310 +#, kde-format +msgid "" +"The default configuration for the MySQL server was found and is readable at " +"%1." +msgstr "MySQL 伺服器的預設設定已找到,並且可讀取於 %1" + +#: widgets/selftestdialog.cpp:314 +#, kde-format +msgid "MySQL server default configuration not found." +msgstr "找不到 MySQL 伺服器預設設定。" + +#: widgets/selftestdialog.cpp:315 +#, kde-format +msgid "" +"The default configuration for the MySQL server was not found or was not " +"readable. Check your Akonadi installation is complete and you have all " +"required access rights." +msgstr "" +"找不到 MySQL 伺服器的預設設定,或是有找到但無法讀取。請檢查您的 Akonadi 安裝" +"是否完整,並且是否具有存取的權限。" + +#: widgets/selftestdialog.cpp:323 +#, kde-format +msgid "MySQL server custom configuration not available." +msgstr "無法取得 MySQL 伺服器的自訂設定。" + +#: widgets/selftestdialog.cpp:324 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was not found but is optional." +msgstr "找不到 MySQL 伺服器的自訂設定,不過此設定屬於選擇性的。" + +#: widgets/selftestdialog.cpp:327 +#, kde-format +msgid "MySQL server custom configuration found." +msgstr "找到 MySQL 伺服器的自訂設定。" + +#: widgets/selftestdialog.cpp:328 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found and is readable at %1" +msgstr "找到 MySQL 伺服器的自訂設定,並且可讀取於 %1。" + +#: widgets/selftestdialog.cpp:332 +#, kde-format +msgid "MySQL server custom configuration not readable." +msgstr "無法讀取 MySQL 伺服器的自訂設定。" + +#: widgets/selftestdialog.cpp:333 +#, kde-format +msgid "" +"The custom configuration for the MySQL server was found at %1 but is not " +"readable. Check your access rights." +msgstr "在 %1 找到 MySQL 伺服器的自訂設定,但是無法讀取。請檢查您的權限。" + +#: widgets/selftestdialog.cpp:342 +#, kde-format +msgid "MySQL server configuration not found or not readable." +msgstr "找不到 MySQL 伺服器設定,或是有找到但無法讀取。" + +#: widgets/selftestdialog.cpp:343 +#, kde-format +msgid "The MySQL server configuration was not found or is not readable." +msgstr "找不到 MySQL 伺服器設定,或是有找到但無法讀取。" + +#: widgets/selftestdialog.cpp:346 +#, kde-format +msgid "MySQL server configuration is usable." +msgstr "MySQL 伺服器設定可使用。" + +#: widgets/selftestdialog.cpp:347 +#, kde-format +msgid "The MySQL server configuration was found at %1 and is readable." +msgstr "在 %1 找到 MySQL 伺服器的設定,並且可讀取。" + +#: widgets/selftestdialog.cpp:376 +#, kde-format +msgid "Cannot connect to PostgreSQL server." +msgstr "無法連線到 PostgreSQL 伺服器。" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "PostgreSQL server found." +msgstr "已找到 PostgreSQL 伺服器。" + +#: widgets/selftestdialog.cpp:378 +#, kde-format +msgid "The PostgreSQL server was found and connection is working." +msgstr "PostgreSQL 伺服器已找到並運作中。" + +#: widgets/selftestdialog.cpp:388 +#, kde-format +msgid "akonadictl not found" +msgstr "找不到 akonadictl。" + +#: widgets/selftestdialog.cpp:389 +#, kde-format +msgid "" +"The program 'akonadictl' needs to be accessible in $PATH. Make sure you have " +"the Akonadi server installed." +msgstr "" +"akonadictl 執行檔必須在您的執行路徑中。請確認您已安裝了 Akonadi 伺服器。" + +#: widgets/selftestdialog.cpp:396 +#, kde-format +msgid "akonadictl found and usable" +msgstr "已找到 akonadictl,並且可執行" + +#: widgets/selftestdialog.cpp:397 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found and could be " +"executed successfully.\n" +"Result:\n" +"%2" +msgstr "" +"用於控制 Akonadi 伺服器的程式 %1 已找到,並且可執行。\n" +"結果:\n" +"%2" + +#: widgets/selftestdialog.cpp:403 +#, kde-format +msgid "akonadictl found but not usable" +msgstr "akonadictl 已找到,但無法執行" + +#: widgets/selftestdialog.cpp:404 +#, kde-format +msgid "" +"The program '%1' to control the Akonadi server was found but could not be " +"executed successfully.\n" +"Result:\n" +"%2\n" +"Make sure the Akonadi server is installed correctly." +msgstr "" +"用於控制 Akonadi 伺服器的程式 %1 已找到,但無法執行。\n" +"結果:\n" +"%2 \n" +"請確定 Akonadi 伺服器已正確安裝。" + +#: widgets/selftestdialog.cpp:416 +#, kde-format +msgid "Akonadi control process registered at D-Bus." +msgstr "Akonadi 控制行程已註冊到 D-Bus。" + +#: widgets/selftestdialog.cpp:417 +#, kde-format +msgid "" +"The Akonadi control process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "Akonadi 控制行程已註冊到 D-Bus,通常表示已經可使用。" + +#: widgets/selftestdialog.cpp:420 +#, kde-format +msgid "Akonadi control process not registered at D-Bus." +msgstr "Akonadi 控制行程尚未註冊到 D-Bus。" + +#: widgets/selftestdialog.cpp:421 +#, kde-format +msgid "" +"The Akonadi control process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi 控制行程尚未註冊到 D-Bus,通常表示尚未啟動,或是啟動時遇到嚴重的錯" +"誤。" + +#: widgets/selftestdialog.cpp:427 +#, kde-format +msgid "Akonadi server process registered at D-Bus." +msgstr "Akonadi 伺服器行程已註冊到 D-Bus。" + +#: widgets/selftestdialog.cpp:428 +#, kde-format +msgid "" +"The Akonadi server process is registered at D-Bus which typically indicates " +"it is operational." +msgstr "Akonadi 伺服器行程已註冊到 D-Bus,通常表示已經可使用。" + +#: widgets/selftestdialog.cpp:431 +#, kde-format +msgid "Akonadi server process not registered at D-Bus." +msgstr "Akonadi 伺服器行程尚未註冊到 D-Bus。" + +#: widgets/selftestdialog.cpp:432 +#, kde-format +msgid "" +"The Akonadi server process is not registered at D-Bus which typically means " +"it was not started or encountered a fatal error during startup." +msgstr "" +"Akonadi 伺服器行程尚未註冊到 D-Bus,通常表示尚未啟動,或啟動時遇到嚴重的錯" +"誤。" + +#: widgets/selftestdialog.cpp:441 +#, kde-format +msgid "Protocol version check not possible." +msgstr "無法檢查通訊協定版本。" + +#: widgets/selftestdialog.cpp:442 +#, kde-format +msgid "" +"Without a connection to the server it is not possible to check if the " +"protocol version meets the requirements." +msgstr "若無法連接伺服器,將無法檢查協定版本是否相容。" + +#: widgets/selftestdialog.cpp:447 +#, kde-format +msgid "Server protocol version is too old." +msgstr "伺服器協定版本太舊了。" + +#: widgets/selftestdialog.cpp:448 widgets/selftestdialog.cpp:455 +#, fuzzy, kde-format +#| msgid "" +#| "The server protocol version is %1, but at least version %2 is required. " +#| "Install a newer version of the Akonadi server." +msgid "" +"The server protocol version is %1, but version %2 is required by the client. " +"If you recently updated KDE PIM, please make sure to restart both Akonadi " +"and KDE PIM applications." +msgstr "" +"伺服器協定版本為 %1,但是最少需要 %2 以上。請安裝新版本的 Akonadi 伺服器。" + +#: widgets/selftestdialog.cpp:454 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version is too new." +msgstr "伺服器協定版本太舊了。" + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "Server protocol version matches." +msgstr "伺服器協定版本太舊了。" + +#: widgets/selftestdialog.cpp:460 +#, fuzzy, kde-format +#| msgid "Server protocol version is too old." +msgid "The current Protocol version is %1." +msgstr "伺服器協定版本太舊了。" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "Resource agents found." +msgstr "找到資源代理程式。" + +#: widgets/selftestdialog.cpp:478 +#, kde-format +msgid "At least one resource agent has been found." +msgstr "至少找到了一個資源代理程式。" + +#: widgets/selftestdialog.cpp:481 +#, kde-format +msgid "No resource agents found." +msgstr "沒有找到資源代理程式。" + +#: widgets/selftestdialog.cpp:482 +#, kde-format +msgid "" +"No resource agents have been found, Akonadi is not usable without at least " +"one. This usually means that no resource agents are installed or that there " +"is a setup problem. The following paths have been searched: '%1'. The " +"XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes " +"all paths where Akonadi agents are installed." +msgstr "" +"沒有找到資源代理程式。若沒有至少一個資源代理程式,Akonadi 會無法使用。這通常" +"表示沒有安裝任何資源代理程式,或是設定時發生問題。尋找的路徑如下:%1。" +"XDG_DATA_DIRS 環境變數設定為 %2,請確定它包含了所有 Akonadi 代理程式安裝的路" +"徑。" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "No current Akonadi server error log found." +msgstr "沒有找到現有的 Akonadi 伺服器錯誤紀錄。" + +#: widgets/selftestdialog.cpp:499 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its current startup." +msgstr "Akonadi 伺服器啟動時並未報告任何錯誤。" + +#: widgets/selftestdialog.cpp:503 +#, kde-format +msgid "Current Akonadi server error log found." +msgstr "找到現有的 Akonadi 伺服器錯誤紀錄。" + +#: widgets/selftestdialog.cpp:504 +#, kde-format +msgid "" +"The Akonadi server reported errors during its current startup. The log can " +"be found in %1." +msgstr "Akonadi 伺服器啟動時報告有錯誤。紀錄可以在 %1 找到。" + +#: widgets/selftestdialog.cpp:512 +#, kde-format +msgid "No previous Akonadi server error log found." +msgstr "沒有找到先前的 Akonadi 伺服器錯誤紀錄。" + +#: widgets/selftestdialog.cpp:513 +#, kde-format +msgid "" +"The Akonadi server did not report any errors during its previous startup." +msgstr "Akonadi 伺服器上次啟動時沒有回報錯誤。" + +#: widgets/selftestdialog.cpp:517 +#, kde-format +msgid "Previous Akonadi server error log found." +msgstr "找到先前的 Akonadi 伺服器錯誤紀錄。" + +#: widgets/selftestdialog.cpp:518 +#, kde-format +msgid "" +"The Akonadi server reported errors during its previous startup. The log can " +"be found in %1." +msgstr "Akonadi 伺服器上次啟動時報告有錯誤。紀錄可以在 %1 找到。" + +#: widgets/selftestdialog.cpp:529 +#, kde-format +msgid "No current Akonadi control error log found." +msgstr "沒有找到現有的 Akonadi 控制錯誤紀錄。" + +#: widgets/selftestdialog.cpp:530 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its current " +"startup." +msgstr "Akonadi 控制行程啟動時沒有回報錯誤。" + +#: widgets/selftestdialog.cpp:534 +#, kde-format +msgid "Current Akonadi control error log found." +msgstr "找到現有的 Akonadi 控制錯誤紀錄。" + +#: widgets/selftestdialog.cpp:535 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its current startup. The " +"log can be found in %1." +msgstr "Akonadi 控制行程啟動時報告有錯誤。紀錄可以在 %1 找到。" + +#: widgets/selftestdialog.cpp:543 +#, kde-format +msgid "No previous Akonadi control error log found." +msgstr "沒有找到先前的 Akonadi 控制行程錯誤紀錄。" + +#: widgets/selftestdialog.cpp:544 +#, kde-format +msgid "" +"The Akonadi control process did not report any errors during its previous " +"startup." +msgstr "Akonadi 控制行程先前啟動時沒有回報錯誤。" + +#: widgets/selftestdialog.cpp:548 +#, kde-format +msgid "Previous Akonadi control error log found." +msgstr "找到先前的 Akonadi 控制行程錯誤紀錄。" + +#: widgets/selftestdialog.cpp:549 +#, kde-format +msgid "" +"The Akonadi control process reported errors during its previous startup. The " +"log can be found in %1." +msgstr "Akonadi 控制行程上次啟動時報告有錯誤。紀錄可以在 %1 找到。" + +#: widgets/selftestdialog.cpp:559 +#, kde-format +msgid "Akonadi was started as root" +msgstr "Akonadi 以 root 身份執行" + +#: widgets/selftestdialog.cpp:560 +#, kde-format +msgid "" +"Running Internet-facing applications as root/administrator exposes you to " +"many security risks. MySQL, used by this Akonadi installation, will not " +"allow itself to run as root, to protect you from these risks." +msgstr "" +"以 root / 管理員身份執行一個會存取網際網路的應用程式,可能造成很多安全上的漏" +"洞。Akonadi 的後端資料庫,MySQL,將不會允許以 root 身份執行,以便保護系統安" +"全。" + +#: widgets/selftestdialog.cpp:564 +#, kde-format +msgid "Akonadi is not running as root" +msgstr "Akonadi 未以 root 執行" + +#: widgets/selftestdialog.cpp:565 +#, kde-format +msgid "" +"Akonadi is not running as a root/administrator user, which is the " +"recommended setup for a secure system." +msgstr "Akonadi 並未以 root 或管理員身份執行,這樣設定對系統安全較有保障。" + +#: widgets/selftestdialog.cpp:643 +#, kde-format +msgid "Save Test Report" +msgstr "儲存測試報告" + +#: widgets/selftestdialog.cpp:650 +#, fuzzy, kde-format +#| msgctxt "@info:status" +#| msgid "Error." +msgid "Error" +msgstr "錯誤:" + +#: widgets/selftestdialog.cpp:650 +#, kde-format +msgid "Could not open file '%1'" +msgstr "無法開啟檔案 %1" + +#. i18n: ectx: property (text), widget (QLabel, introductionLabel) +#: widgets/selftestdialog.ui:16 +#, kde-format +msgid "" +"An error occurred during the startup of the Akonadi server. The following " +"self-tests are supposed to help with tracking down and solving this problem. " +"When requesting support or reporting bugs, please always include this report." +msgstr "" +"啟動 Akonadi 伺服器時發生問題。以下的自我測試是用來協助您找出並解決問題。當要" +"求支援或回報錯誤時,請將此份報告放入。" + +#. i18n: ectx: property (title), widget (QGroupBox, detailsGroup) +#: widgets/selftestdialog.ui:39 +#, kde-format +msgid "Details" +msgstr "詳情" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/selftestdialog.ui:61 +#, fuzzy, kde-format +#| msgid "" +#| "

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgid "" +"

For more troubleshooting tips please refer to userbase.kde.org/Akonadi.

" +msgstr "" +"

更多的錯誤解決提示,請參考userbase.kde.org/Akonadi。

" + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "&New Folder..." +msgstr "新增資料夾(&N)..." + +#: widgets/standardactionmanager.cpp:76 +#, kde-format +msgid "New" +msgstr "新增" + +#: widgets/standardactionmanager.cpp:78 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgid "&Delete Folder" +msgstr "刪除 %1 個資料夾(&D)" + +#: widgets/standardactionmanager.cpp:78 +#, kde-format +msgid "Delete" +msgstr "刪除" + +#: widgets/standardactionmanager.cpp:80 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgid "&Synchronize Folder" +msgstr "同步 %1 個資料夾(&S)" + +#: widgets/standardactionmanager.cpp:81 widgets/standardactionmanager.cpp:162 +#: widgets/standardactionmanager.cpp:255 +#, kde-format +msgid "Synchronize" +msgstr "同步" + +#: widgets/standardactionmanager.cpp:88 +#, kde-format +msgid "Folder &Properties" +msgstr "資料夾屬性(&P)" + +#: widgets/standardactionmanager.cpp:89 widgets/standardactionmanager.cpp:154 +#, kde-format +msgid "Properties" +msgstr "屬性" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "&Paste" +msgstr "貼上(&P)" + +#: widgets/standardactionmanager.cpp:96 +#, kde-format +msgid "Paste" +msgstr "貼上" + +#: widgets/standardactionmanager.cpp:99 +#, kde-format +msgid "Manage Local &Subscriptions..." +msgstr "管理本地端的訂閱(&S)" + +#: widgets/standardactionmanager.cpp:100 +#, kde-format +msgid "Manage Local Subscriptions" +msgstr "管理本地端的訂閱" + +#: widgets/standardactionmanager.cpp:107 +#, kde-format +msgid "Add to Favorite Folders" +msgstr "新增到我的最愛資料夾" + +#: widgets/standardactionmanager.cpp:108 +#, kde-format +msgid "Add to Favorite" +msgstr "新增到我的最愛" + +#: widgets/standardactionmanager.cpp:115 +#, kde-format +msgid "Remove from Favorite Folders" +msgstr "從我的最愛資料夾移除" + +#: widgets/standardactionmanager.cpp:116 +#, kde-format +msgid "Remove from Favorite" +msgstr "從我的最愛中移除" + +#: widgets/standardactionmanager.cpp:123 +#, kde-format +msgid "Rename Favorite..." +msgstr "重新命名我的最愛..." + +#: widgets/standardactionmanager.cpp:124 +#, kde-format +msgid "Rename" +msgstr "重新命名" + +#: widgets/standardactionmanager.cpp:131 widgets/standardactionmanager.cpp:170 +#, kde-format +msgid "Copy Folder To..." +msgstr "複製資料夾到..." + +#: widgets/standardactionmanager.cpp:132 widgets/standardactionmanager.cpp:138 +#: widgets/standardactionmanager.cpp:171 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy To" +msgstr "複製到" + +#: widgets/standardactionmanager.cpp:138 widgets/standardactionmanager.cpp:185 +#, kde-format +msgid "Copy Item To..." +msgstr "複製項目到..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move Item To..." +msgstr "移動項目到..." + +#: widgets/standardactionmanager.cpp:139 widgets/standardactionmanager.cpp:142 +#: widgets/standardactionmanager.cpp:179 widgets/standardactionmanager.cpp:186 +#, kde-format +msgid "Move To" +msgstr "移動到" + +#: widgets/standardactionmanager.cpp:141 widgets/standardactionmanager.cpp:178 +#, kde-format +msgid "Move Folder To..." +msgstr "移動資料夾到..." + +#: widgets/standardactionmanager.cpp:148 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgid "&Cut Item" +msgstr "剪下 %1 個項目(&C)" + +#: widgets/standardactionmanager.cpp:148 widgets/standardactionmanager.cpp:149 +#, kde-format +msgid "Cut" +msgstr "剪下" + +#: widgets/standardactionmanager.cpp:149 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgid "&Cut Folder" +msgstr "剪下 %1 個資料夾(&C)" + +#: widgets/standardactionmanager.cpp:150 +#, kde-format +msgid "Create Resource" +msgstr "建立資源" + +#: widgets/standardactionmanager.cpp:151 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgid "Delete Resource" +msgstr "刪除 %1 個資源" + +#: widgets/standardactionmanager.cpp:153 +#, kde-format +msgid "&Resource Properties" +msgstr "資源屬性(&R)" + +#: widgets/standardactionmanager.cpp:161 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgid "Synchronize Resource" +msgstr "同步 %1 個資源" + +#: widgets/standardactionmanager.cpp:168 +#, kde-format +msgid "Work Offline" +msgstr "離線工作" + +#: widgets/standardactionmanager.cpp:188 +#, kde-format +msgid "&Synchronize Folder Recursively" +msgstr "遞迴地同步資料夾(&S)" + +#: widgets/standardactionmanager.cpp:189 +#, kde-format +msgid "Synchronize Recursively" +msgstr "遞迴地同步" + +#: widgets/standardactionmanager.cpp:196 +#, kde-format +msgid "&Move Folder To Trash" +msgstr "將資料夾移到垃圾桶(&M)" + +#: widgets/standardactionmanager.cpp:197 +#, kde-format +msgid "Move Folder To Trash" +msgstr "將資料夾移到垃圾桶" + +#: widgets/standardactionmanager.cpp:204 +#, kde-format +msgid "&Move Item To Trash" +msgstr "將項目移到垃圾桶(&M)" + +#: widgets/standardactionmanager.cpp:205 +#, kde-format +msgid "Move Item To Trash" +msgstr "將項目移到垃圾桶" + +#: widgets/standardactionmanager.cpp:212 widgets/standardactionmanager.cpp:228 +#, kde-format +msgid "&Restore Folder From Trash" +msgstr "從垃圾桶中回復資料夾(&R)" + +#: widgets/standardactionmanager.cpp:213 widgets/standardactionmanager.cpp:229 +#, kde-format +msgid "Restore Folder From Trash" +msgstr "從垃圾桶中回復資料夾" + +#: widgets/standardactionmanager.cpp:220 widgets/standardactionmanager.cpp:237 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "&Restore Item From Trash" +msgstr "從垃圾桶中回復項目(&R)" + +#: widgets/standardactionmanager.cpp:221 widgets/standardactionmanager.cpp:238 +#: widgets/standardactionmanager.cpp:244 +#, kde-format +msgid "Restore Item From Trash" +msgstr "從垃圾桶中回復項目" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "&Restore Collection From Trash" +msgstr "從垃圾桶中回復收藏(&R)" + +#: widgets/standardactionmanager.cpp:235 +#, kde-format +msgid "Restore Collection From Trash" +msgstr "從垃圾桶中回復收藏 " + +#: widgets/standardactionmanager.cpp:246 +#, kde-format +msgid "&Synchronize Favorite Folders" +msgstr "同步最愛資料夾(&S)" + +#: widgets/standardactionmanager.cpp:247 +#, kde-format +msgid "Synchronize Favorite Folders" +msgstr "同步最愛資料夾" + +#: widgets/standardactionmanager.cpp:254 +#, kde-format +msgid "Synchronize Folder Tree" +msgstr "同步資料夾樹狀圖" + +#: widgets/standardactionmanager.cpp:340 +#, kde-format +msgid "&Copy Folder" +msgid_plural "&Copy %1 Folders" +msgstr[0] "複製 %1 個資料夾(&C)" + +#: widgets/standardactionmanager.cpp:341 +#, kde-format +msgid "&Copy Item" +msgid_plural "&Copy %1 Items" +msgstr[0] "複製 %1 個項目(&C)" + +#: widgets/standardactionmanager.cpp:342 +#, fuzzy, kde-format +#| msgid "&Cut Item" +#| msgid_plural "&Cut %1 Items" +msgctxt "@action" +msgid "&Cut Item" +msgid_plural "&Cut %1 Items" +msgstr[0] "剪下 %1 個項目(&C)" + +#: widgets/standardactionmanager.cpp:343 +#, fuzzy, kde-format +#| msgid "&Cut Folder" +#| msgid_plural "&Cut %1 Folders" +msgctxt "@action" +msgid "&Cut Folder" +msgid_plural "&Cut %1 Folders" +msgstr[0] "剪下 %1 個資料夾(&C)" + +#: widgets/standardactionmanager.cpp:344 +#, kde-format +msgid "&Delete Item" +msgid_plural "&Delete %1 Items" +msgstr[0] "刪除 %1 個項目(&D)" + +#: widgets/standardactionmanager.cpp:345 +#, fuzzy, kde-format +#| msgid "&Delete Folder" +#| msgid_plural "&Delete %1 Folders" +msgctxt "@action" +msgid "&Delete Folder" +msgid_plural "&Delete %1 Folders" +msgstr[0] "刪除 %1 個資料夾(&D)" + +#: widgets/standardactionmanager.cpp:346 +#, fuzzy, kde-format +#| msgid "&Synchronize Folder" +#| msgid_plural "&Synchronize %1 Folders" +msgctxt "@action" +msgid "&Synchronize Folder" +msgid_plural "&Synchronize %1 Folders" +msgstr[0] "同步 %1 個資料夾(&S)" + +#: widgets/standardactionmanager.cpp:347 +#, kde-format +msgid "&Delete Resource" +msgid_plural "&Delete %1 Resources" +msgstr[0] "刪除 %1 個資源(&D)" + +#: widgets/standardactionmanager.cpp:348 +#, kde-format +msgid "&Synchronize Resource" +msgid_plural "&Synchronize %1 Resources" +msgstr[0] "同步 %1 個資源(&S)" + +#: widgets/standardactionmanager.cpp:350 +#, kde-format +msgid "Copy Folder" +msgid_plural "Copy %1 Folders" +msgstr[0] "複製 %1 個資料夾" + +#: widgets/standardactionmanager.cpp:351 +#, kde-format +msgid "Copy Item" +msgid_plural "Copy %1 Items" +msgstr[0] "複製 %1 個項目" + +#: widgets/standardactionmanager.cpp:352 +#, kde-format +msgid "Cut Item" +msgid_plural "Cut %1 Items" +msgstr[0] "剪下 %1 個項目" + +#: widgets/standardactionmanager.cpp:353 +#, kde-format +msgid "Cut Folder" +msgid_plural "Cut %1 Folders" +msgstr[0] "剪下 %1 個資料夾" + +#: widgets/standardactionmanager.cpp:354 +#, kde-format +msgid "Delete Item" +msgid_plural "Delete %1 Items" +msgstr[0] "刪除 %1 個項目" + +#: widgets/standardactionmanager.cpp:355 +#, kde-format +msgid "Delete Folder" +msgid_plural "Delete %1 Folders" +msgstr[0] "刪除 %1 個資料夾" + +#: widgets/standardactionmanager.cpp:356 +#, kde-format +msgid "Synchronize Folder" +msgid_plural "Synchronize %1 Folders" +msgstr[0] "同步 %1 個資料夾" + +#: widgets/standardactionmanager.cpp:357 +#, fuzzy, kde-format +#| msgid "Delete Resource" +#| msgid_plural "Delete %1 Resources" +msgctxt "@action" +msgid "Delete Resource" +msgid_plural "Delete %1 Resources" +msgstr[0] "刪除 %1 個資源" + +#: widgets/standardactionmanager.cpp:358 +#, fuzzy, kde-format +#| msgid "Synchronize Resource" +#| msgid_plural "Synchronize %1 Resources" +msgctxt "@action" +msgid "Synchronize Resource" +msgid_plural "Synchronize %1 Resources" +msgstr[0] "同步 %1 個資源" + +#: widgets/standardactionmanager.cpp:361 +#, kde-format +msgctxt "@label:textbox name of Akonadi folder" +msgid "Name" +msgstr "名稱" + +#: widgets/standardactionmanager.cpp:368 +#, kde-format +msgid "Do you really want to delete this folder and all its sub-folders?" +msgid_plural "" +"Do you really want to delete %1 folders and all their sub-folders?" +msgstr[0] "您確定要刪除這 %1 個資料夾以及它所有的子資料夾嗎?" + +#: widgets/standardactionmanager.cpp:371 +#, kde-format +msgctxt "@title:window" +msgid "Delete folder?" +msgid_plural "Delete folders?" +msgstr[0] "要刪除資料夾嗎?" + +#: widgets/standardactionmanager.cpp:372 +#, kde-format +msgid "Could not delete folder: %1" +msgstr "無法刪除資料夾:%1" + +#: widgets/standardactionmanager.cpp:373 +#, kde-format +msgid "Folder deletion failed" +msgstr "刪除資料夾失敗" + +#: widgets/standardactionmanager.cpp:375 +#, kde-format +msgctxt "@title:window" +msgid "Properties of Folder %1" +msgstr "資料夾 %1 的內容" + +#: widgets/standardactionmanager.cpp:379 +#, kde-format +msgid "Do you really want to delete the selected item?" +msgid_plural "Do you really want to delete %1 items?" +msgstr[0] "您真的要刪除這 %1 個項目嗎?" + +#: widgets/standardactionmanager.cpp:380 +#, kde-format +msgctxt "@title:window" +msgid "Delete item?" +msgid_plural "Delete items?" +msgstr[0] "要刪除項目嗎?" + +#: widgets/standardactionmanager.cpp:381 +#, kde-format +msgid "Could not delete item: %1" +msgstr "無法刪除項目:%1" + +#: widgets/standardactionmanager.cpp:382 +#, kde-format +msgid "Item deletion failed" +msgstr "刪除項目失敗" + +#: widgets/standardactionmanager.cpp:384 +#, kde-format +msgctxt "@title:window" +msgid "Rename Favorite" +msgstr "重新命名我的最愛" + +#: widgets/standardactionmanager.cpp:385 +#, kde-format +msgctxt "@label:textbox name of the folder" +msgid "Name:" +msgstr "名稱:" + +#: widgets/standardactionmanager.cpp:387 +#, kde-format +msgctxt "@title:window" +msgid "New Resource" +msgstr "新增資源" + +#: widgets/standardactionmanager.cpp:388 +#, kde-format +msgid "Could not create resource: %1" +msgstr "無法建立資源:%1" + +#: widgets/standardactionmanager.cpp:389 +#, kde-format +msgid "Resource creation failed" +msgstr "資源建立失敗" + +#: widgets/standardactionmanager.cpp:393 +#, kde-format +msgid "Do you really want to delete this resource?" +msgid_plural "Do you really want to delete %1 resources?" +msgstr[0] "您確定要刪除這 %1 個資源嗎?" + +#: widgets/standardactionmanager.cpp:396 +#, kde-format +msgctxt "@title:window" +msgid "Delete Resource?" +msgid_plural "Delete Resources?" +msgstr[0] "要刪除資源嗎?" + +#: widgets/standardactionmanager.cpp:398 +#, kde-format +msgid "Could not paste data: %1" +msgstr "無法貼上資料:%1" + +#: widgets/standardactionmanager.cpp:399 +#, kde-format +msgid "Paste failed" +msgstr "貼上失敗" + +#: widgets/standardactionmanager.cpp:752 +#, kde-format +msgid "We can not add \"/\" in folder name." +msgstr "不能在資料夾名稱中加入 \"/\"。" + +#: widgets/standardactionmanager.cpp:752 widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "Create new folder error" +msgstr "建立新的資料夾時發生錯誤" + +#: widgets/standardactionmanager.cpp:756 +#, kde-format +msgid "We can not add \".\" at begin or end of folder name." +msgstr "不能在資料夾名稱的開頭或結尾加入 \".\"。" + +#: widgets/standardactionmanager.cpp:995 +#, kde-format +msgid "" +"Before syncing folder \"%1\" it is necessary to have the resource online. Do " +"you want to make it online?" +msgstr "同步資料夾 \"%1\" 之前需要先讓資源上線。您要讓它上線嗎?" + +#: widgets/standardactionmanager.cpp:996 +#, kde-format +msgid "Account \"%1\" is offline" +msgstr "帳號 \"%1\" 已離線" + +#: widgets/standardactionmanager.cpp:997 +#, kde-format +msgctxt "@action:button" +msgid "Go Online" +msgstr "上線" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Move to This Folder" +msgstr "移至此資料夾" + +#: widgets/standardactionmanager.cpp:1592 +#, kde-format +msgid "Copy to This Folder" +msgstr "複製到此資料夾" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Failed to create relation." +msgid "Failed to update subscription: %1" +msgstr "建立關聯時失敗。" + +#: widgets/subscriptiondialog.cpp:93 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgctxt "@title" +msgid "Subscription Error" +msgstr "本地端的訂閱" + +#. i18n: ectx: property (windowTitle), widget (QDialog, SubscriptionDialog) +#: widgets/subscriptiondialog.ui:14 +#, fuzzy, kde-format +#| msgid "Local Subscriptions" +msgid "Local Subscriptions" +msgstr "本地端的訂閱" + +#. i18n: ectx: property (text), widget (QLabel, label) +#: widgets/subscriptiondialog.ui:22 +#, kde-format +msgid "Search:" +msgstr "搜尋:" + +#. i18n: ectx: property (text), widget (QCheckBox, subscribedOnlyCheckBox) +#: widgets/subscriptiondialog.ui:36 +#, fuzzy, kde-format +#| msgid "Subscribed only" +msgid "&Subscribed only" +msgstr "只有訂閱的群組" + +#. i18n: ectx: property (text), widget (QPushButton, subscribeButton) +#: widgets/subscriptiondialog.ui:62 +#, fuzzy, kde-format +#| msgid "Subscribe" +msgid "Su&bscribe" +msgstr "訂閱" + +#. i18n: ectx: property (text), widget (QPushButton, unsubscribeButton) +#: widgets/subscriptiondialog.ui:69 +#, fuzzy, kde-format +#| msgid "Unsubscribe" +msgid "&Unsubscribe" +msgstr "取消訂閱" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "Failed to create a new tag" +msgstr "建立新標籤時失敗" + +#: widgets/tageditwidget.cpp:116 +#, kde-format +msgid "An error occurred while creating a new tag" +msgstr "建立新標籤時發生錯誤" + +#: widgets/tageditwidget.cpp:164 +#, kde-kuit-format +msgctxt "@info" +msgid "Do you really want to remove the tag %1?" +msgstr "您確定要刪除標籤 %1 嗎?" + +#: widgets/tageditwidget.cpp:165 +#, kde-format +msgctxt "@title" +msgid "Delete tag" +msgstr "刪除標籤" + +#: widgets/tageditwidget.cpp:189 +#, kde-format +msgctxt "@info" +msgid "Delete tag" +msgstr "刪除標籤" + +#. i18n: ectx: property (text), widget (QLabel, selectLabel) +#: widgets/tageditwidget.ui:29 +#, fuzzy, kde-format +#| msgctxt "@label:textbox" +#| msgid "Configure which tags should be applied." +msgid "Select tags that should be applied." +msgstr "設定哪些標籤要被套用。" + +#. i18n: ectx: property (text), widget (QPushButton, newTagButton) +#: widgets/tageditwidget.ui:54 +#, fuzzy, kde-format +#| msgctxt "@label" +#| msgid "Create new tag" +msgid "Create new tag" +msgstr "建立新標籤" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagManagementDialog) +#: widgets/tagmanagementdialog.ui:14 +#, fuzzy, kde-format +#| msgctxt "@title:window" +#| msgid "Manage Tags" +msgid "Manage Tags" +msgstr "管理標籤" + +#: widgets/tagselectioncombobox.cpp:124 +#, fuzzy, kde-format +#| msgctxt "@title" +#| msgid "Delete tag" +msgctxt "@label Placeholder text in tag selection combobox" +msgid "Select tags..." +msgstr "刪除標籤" + +#. i18n: ectx: property (windowTitle), widget (QDialog, TagSelectionDialog) +#: widgets/tagselectiondialog.ui:14 +#, kde-format +msgid "Select Tags" +msgstr "" + +#: widgets/tagwidget.cpp:45 +#, kde-format +msgid "Clear" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (Akonadi::TagView, tagView) +#: widgets/tagwidget.ui:20 +#, kde-format +msgid "Click to add tags" +msgstr "" + +#. i18n: ectx: property (text), widget (QToolButton, editButton) +#: widgets/tagwidget.ui:27 +#, kde-format +msgid "..." +msgstr "..." + +#: xml/akonadi2xml.cpp:25 +#, kde-format +msgid "Akonadi To XML converter" +msgstr "Akonadi XML 轉換器" + +#: xml/akonadi2xml.cpp:27 +#, kde-format +msgid "Converts an Akonadi collection subtree into a XML file." +msgstr "將 Akonadi 收藏子樹轉為 XML 檔。" + +#: xml/akonadi2xml.cpp:29 +#, kde-format +msgid "(c) 2009 Volker Krause " +msgstr "(c) 2009 Volker Krause " + +#: xml/xmldocument.cpp:84 +#, kde-format +msgid "No data loaded." +msgstr "沒有載入資料。" + +#: xml/xmldocument.cpp:123 +#, kde-format +msgid "No filename specified" +msgstr "沒有指定檔案名稱" + +#: xml/xmldocument.cpp:131 +#, kde-format +msgid "Unable to open data file '%1'." +msgstr "無法開啟資料檔 '%1'。" + +#: xml/xmldocument.cpp:136 +#, kde-format +msgid "File %1 does not exist." +msgstr "檔案 %1 不存在。" + +#: xml/xmldocument.cpp:144 +#, kde-format +msgid "Unable to parse data file '%1'." +msgstr "無法剖析資料檔 '%1'。" + +#: xml/xmldocument.cpp:151 +#, kde-format +msgid "Schema definition could not be loaded and parsed." +msgstr "機制定義無法載入或剖析。" + +#: xml/xmldocument.cpp:156 +#, kde-format +msgid "Unable to create schema parser context." +msgstr "無法建立機制剖析器內文。" + +#: xml/xmldocument.cpp:161 +#, kde-format +msgid "Unable to create schema." +msgstr "無法建立機制。" + +#: xml/xmldocument.cpp:166 +#, kde-format +msgid "Unable to create schema validation context." +msgstr "無法建立機制確認內文。" + +#: xml/xmldocument.cpp:171 +#, kde-format +msgid "Invalid file format." +msgstr "無效的檔案格式。" + +#: xml/xmldocument.cpp:179 +#, kde-format +msgid "Unable to parse data file: %1" +msgstr "無法剖析資料檔:%1" + +#: xml/xmldocument.cpp:304 +#, kde-format +msgid "Unable to find collection %1" +msgstr "無法找到收藏 %1" + +#~ msgid "Id" +#~ msgstr "代碼" + +#~ msgid "Remote Id" +#~ msgstr "遠端代碼" + +#~ msgid "MimeType" +#~ msgstr "Mime 型態" + +#~ msgid "Default Name" +#~ msgstr "預設名稱" diff --git a/sanitizers.supp b/sanitizers.supp new file mode 100644 index 0000000..c56b842 --- /dev/null +++ b/sanitizers.supp @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: none +# Suppression file for ASAN/LSAN + +leak:libspeechd +leak:getdelim +leak:g_malloc +leak:libfontconfig +leak:libdbus +leak:QEasingCurve:: +leak:QtSharedPointer::ExternalRefCountData::getAndRef +leak:QArrayData::allocate +leak:QObject::QObject +leak:QObjectPrivate::addConnection +leak:QObjectPrivate::connectImpl +leak:QPropertyAnimation::QPropertyAnimation diff --git a/src/.krazy b/src/.krazy new file mode 100644 index 0000000..f22ca66 --- /dev/null +++ b/src/.krazy @@ -0,0 +1 @@ +SKIP qsqlite diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..4e79dc0 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,19 @@ +add_subdirectory(agentserver) +add_subdirectory(akonadicontrol) +add_subdirectory(akonadictl) +if(NOT WIN32) + add_subdirectory(asapcat) +endif() +add_subdirectory(private) +add_subdirectory(interfaces) +if(SQLITE_FOUND) + add_subdirectory(qsqlite) +endif() +add_subdirectory(rds) +add_subdirectory(server) +add_subdirectory(shared) +add_subdirectory(core) +add_subdirectory(agentbase) +add_subdirectory(widgets) +add_subdirectory(selftest) +add_subdirectory(xml) diff --git a/src/Messages.sh b/src/Messages.sh new file mode 100755 index 0000000..3afb406 --- /dev/null +++ b/src/Messages.sh @@ -0,0 +1,3 @@ +#! /bin/sh +$EXTRACTRC `find -name "*.ui"` >> rc.cpp +$XGETTEXT `find -name "*.cpp" -o -name "*.h" | grep -v '/tests/' | grep -v '/autotests/'` -o $podir/libakonadi5.pot diff --git a/src/agentbase/CMakeLists.txt b/src/agentbase/CMakeLists.txt new file mode 100644 index 0000000..907516b --- /dev/null +++ b/src/agentbase/CMakeLists.txt @@ -0,0 +1,119 @@ +add_library(KF5AkonadiAgentBase) +add_library(KF5::AkonadiAgentBase ALIAS KF5AkonadiAgentBase) + +ecm_qt_declare_logging_category(KF5AkonadiAgentBase HEADER akonadiagentbase_debug.h IDENTIFIER AKONADIAGENTBASE_LOG CATEGORY_NAME org.kde.pim.akonadiagentbase + DESCRIPTION "akonadi (Akonadi AgentBase Library)" + OLD_CATEGORY_NAMES akonadiagentbase_log + EXPORT AKONADI + ) + +ecm_generate_headers(AkonadiAgentBase_HEADERS + HEADER_NAMES + AccountsIntegration + AgentBase + AgentSearchInterface + PreprocessorBase + ResourceBase + ResourceSettings + TransportResourceBase + REQUIRED_HEADERS AkonadiAgentBase_HEADERS +) + + +KCONFIG_ADD_KCFG_FILES(KF5AkonadiAgentBase resourcebasesettings.kcfgc) + +qt_add_dbus_interfaces(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml ) + +qt_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml + resourcebase.h Akonadi::ResourceBase resourceadaptor Akonadi__ResourceAdaptor) +qt_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml + preprocessorbase_p.h Akonadi::PreprocessorBasePrivate preprocessoradaptor Akonadi__PreprocessorAdaptor) +qt_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml + agentbase.h Akonadi::AgentBase statusadaptor Akonadi__StatusAdaptor) +qt_add_dbus_adaptor(akonadiagentbase_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml + agentbase.h Akonadi::AgentBase controladaptor Akonadi__ControlAdaptor) +qt_add_dbus_adaptor(akonadiagentbase_SRCS ../interfaces/org.freedesktop.Akonadi.Resource.Transport.xml + transportresourcebase_p.h Akonadi::TransportResourceBasePrivate transportadaptor Akonadi__TransportAdaptor) +qt_add_dbus_adaptor(akonadiagentbase_SRCS ../interfaces/org.freedesktop.Akonadi.Agent.Search.xml + agentsearchinterface_p.h Akonadi::AgentSearchInterfacePrivate searchadaptor Akonadi__SearchAdaptor ) +qt_add_dbus_adaptor(akonadiagentbase_SRCS ../interfaces/org.kde.Akonadi.Accounts.xml + accountsintegration.h Akonadi::AccountsIntegration accountsadaptor Akonadi__AccountsAdaptor) + +target_sources(KF5AkonadiAgentBase PRIVATE + accountsintegration.cpp + agentbase.cpp + agentsearchinterface.cpp + preprocessorbase.cpp + preprocessorbase_p.cpp + recursivemover.cpp + resourcebase.cpp + resourcescheduler.cpp + resourcesettings.cpp + transportresourcebase.cpp + ${akonadiagentbase_SRCS} +) + + + +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(KF5AkonadiAgentBase PROPERTIES UNITY_BUILD ON) +endif() + +generate_export_header(KF5AkonadiAgentBase BASE_NAME akonadiagentbase) + + +target_include_directories(KF5AkonadiAgentBase INTERFACE "$") + +target_link_libraries(KF5AkonadiAgentBase +PUBLIC + Qt::DBus + Qt::Widgets # for QApplication + KF5::AkonadiCore + KF5::ConfigCore + KF5::ConfigGui # for KConfigSkeleton +PRIVATE + KF5::AkonadiPrivate + KF5::AkonadiWidgets + KF5::I18n + Qt::Network + akonadi_shared +) + +if (WITH_ACCOUNTS) + target_link_libraries(KF5AkonadiAgentBase PRIVATE KAccounts ${ACCOUNTSQT_LIBRARIES}) + target_include_directories(KF5AkonadiAgentBase PRIVATE ${ACCOUNTSQT_INCLUDE_DIRS}) +endif() + +set_target_properties(KF5AkonadiAgentBase PROPERTIES + VERSION ${AKONADI_VERSION} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiAgentBase +) + +ecm_generate_pri_file(BASE_NAME AkonadiAgentBase + LIB_NAME KF5AkonadiAgentBase + DEPS "AkonadiCore AkonadiPrivate KConfigCore KConfigGui" FILENAME_VAR PRI_FILENAME +) + +install(TARGETS + KF5AkonadiAgentBase + EXPORT KF5AkonadiTargets + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadiagentbase_export.h + ${CMAKE_CURRENT_BINARY_DIR}/resourcebasesettings.h + ${AkonadiAgentBase_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiAgentBase COMPONENT Devel +) + +install(FILES + resourcebase.kcfg + DESTINATION ${KDE_INSTALL_KCFGDIR} +) + +install(FILES + ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR} +) diff --git a/src/agentbase/accountsintegration.cpp b/src/agentbase/accountsintegration.cpp new file mode 100644 index 0000000..2ec7920 --- /dev/null +++ b/src/agentbase/accountsintegration.cpp @@ -0,0 +1,106 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "accountsintegration.h" +#include "accountsadaptor.h" +#include "config-akonadi.h" + +#include + +#include +#include + +#ifdef WITH_ACCOUNTS +#include +#include +#include +#include +#endif + +using namespace Akonadi; +using namespace std::chrono_literals; + +AccountsIntegration::AccountsIntegration() +{ +#ifdef WITH_ACCOUNTS + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Accounts"), this); + new Akonadi__AccountsAdaptor(this); +#endif +} + +bool AccountsIntegration::isEnabled() const +{ +#ifdef WITH_ACCOUNTS + return true; +#else + return false; +#endif +} + +std::optional AccountsIntegration::accountId() const +{ + return mAccountId; +} + +quint32 AccountsIntegration::getAccountId() const +{ + return mAccountId.has_value() ? *mAccountId : 0; +} + +void AccountsIntegration::setAccountId(quint32 accountId) +{ + if (accountId <= 0) { + mAccountId = std::nullopt; + } else { + mAccountId = accountId; + } + Q_EMIT accountChanged(); +} + +std::optional AccountsIntegration::accountName() const +{ +#ifdef WITH_ACCOUNTS + if (!mAccountId.has_value()) { + return std::nullopt; + } + + auto const account = KAccounts::accountsManager()->account(mAccountId.value()); + if (!account) { + return std::nullopt; + } + + return account->displayName(); +#else + return {}; +#endif +} + +void AccountsIntegration::requestAuthData(const QString &serviceType, AuthDataCallback &&callback, ErrorCallback &&errCallback) +{ +#ifdef WITH_ACCOUNTS + if (!mAccountId.has_value()) { + QTimer::singleShot(0s, this, [error = std::move(errCallback)]() { + error(i18n("There is currently no account configured.")); + }); + return; + } + + auto job = new GetCredentialsJob(mAccountId.value(), this); + job->setServiceType(serviceType); + connect(job, &GetCredentialsJob::result, this, [job, callback = std::move(callback), error = std::move(errCallback)]() { + if (job->error()) { + error(job->errorString()); + } else { + callback(job->credentialsData()); + } + }); + job->start(); +#else + QTimer::singleShot(0s, this, [error = std::move(errCallback)]() { + error(i18n("Accounts integration is not supported")); + }); +#endif +} diff --git a/src/agentbase/accountsintegration.h b/src/agentbase/accountsintegration.h new file mode 100644 index 0000000..db0a0bc --- /dev/null +++ b/src/agentbase/accountsintegration.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiagentbase_export.h" +#include + +#include +#include + +class Akonadi__AccountsAdaptor; +namespace Akonadi +{ +class AKONADIAGENTBASE_EXPORT AccountsIntegration : public QObject +{ + Q_OBJECT + + friend class ::Akonadi__AccountsAdaptor; + +public: + explicit AccountsIntegration(); + ~AccountsIntegration() override = default; + + /** + * Returns whether Accounts integration is enabled. + */ + bool isEnabled() const; + + using AuthDataCallback = std::function; + using ErrorCallback = std::function; + void requestAuthData(const QString &serviceType, AuthDataCallback &&cb, ErrorCallback &&err); + + std::optional accountName() const; +public Q_SLOTS: + std::optional accountId() const; + void setAccountId(quint32 accountId); + +Q_SIGNALS: + void accountChanged(); + +private: + // For DBus adaptor which doesn't understand std::optional + quint32 getAccountId() const; + + std::optional mAccountId; +}; + +} // namespace Akonadi + diff --git a/src/agentbase/agentbase.cpp b/src/agentbase/agentbase.cpp new file mode 100644 index 0000000..203f57e --- /dev/null +++ b/src/agentbase/agentbase.cpp @@ -0,0 +1,1280 @@ +/* + SPDX-FileCopyrightText: 2006 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2007 Bruno Virlet + SPDX-FileCopyrightText: 2008 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentbase.h" +#include "agentbase_p.h" + +#include "agentconfigurationdialog.h" +#include "agentmanager.h" +#include "akonadifull-version.h" +#include "changerecorder.h" +#include "controladaptor.h" +#include "itemfetchjob.h" +#include "monitor_p.h" +#include "private/standarddirs_p.h" +#include "servermanager_p.h" +#include "session.h" +#include "session_p.h" +#include "statusadaptor.h" + +#include "akonadiagentbase_debug.h" + +#include + +#include +#include +#if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#if defined __GLIBC__ +#include // for dumping memory information +#endif + +#ifdef Q_OS_WIN +#include +#endif + +#include +#include + +using namespace Akonadi; + +static AgentBase *sAgentBase = nullptr; + +AgentBase::Observer::Observer() +{ +} + +AgentBase::Observer::~Observer() +{ +} + +void AgentBase::Observer::itemAdded(const Item &item, const Collection &collection) +{ + Q_UNUSED(item) + Q_UNUSED(collection) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::itemChanged(const Item &item, const QSet &partIdentifiers) +{ + Q_UNUSED(item) + Q_UNUSED(partIdentifiers) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::itemRemoved(const Item &item) +{ + Q_UNUSED(item) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + Q_UNUSED(collection) + Q_UNUSED(parent) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::collectionChanged(const Collection &collection) +{ + Q_UNUSED(collection) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::Observer::collectionRemoved(const Collection &collection) +{ + Q_UNUSED(collection) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + Q_UNUSED(item) + Q_UNUSED(source) + Q_UNUSED(dest) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_UNUSED(item) + Q_UNUSED(collection) + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemLinked, sAgentBase->d_ptr, &AgentBasePrivate::itemLinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_UNUSED(item) + Q_UNUSED(collection) + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemUnlinked, sAgentBase->d_ptr, &AgentBasePrivate::itemUnlinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + Q_UNUSED(collection) + Q_UNUSED(source) + Q_UNUSED(dest) + if (sAgentBase) { + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV2::collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes) +{ + Q_UNUSED(changedAttributes) + collectionChanged(collection); +} + +void AgentBase::ObserverV3::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) +{ + Q_UNUSED(items) + Q_UNUSED(addedFlags) + Q_UNUSED(removedFlags) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsFlagsChanged, sAgentBase->d_ptr, &AgentBasePrivate::itemsFlagsChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsMoved(const Akonadi::Item::List &items, const Collection &sourceCollection, const Collection &destinationCollection) +{ + Q_UNUSED(items) + Q_UNUSED(sourceCollection) + Q_UNUSED(destinationCollection) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsMoved, sAgentBase->d_ptr, &AgentBasePrivate::itemsMoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsRemoved(const Akonadi::Item::List &items) +{ + Q_UNUSED(items) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsRemoved, sAgentBase->d_ptr, &AgentBasePrivate::itemsRemoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsLinked(const Akonadi::Item::List &items, const Collection &collection) +{ + Q_UNUSED(items) + Q_UNUSED(collection) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsLinked, sAgentBase->d_ptr, &AgentBasePrivate::itemsLinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV3::itemsUnlinked(const Akonadi::Item::List &items, const Collection &collection) +{ + Q_UNUSED(items) + Q_UNUSED(collection) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimizations in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsUnlinked, sAgentBase->d_ptr, &AgentBasePrivate::itemsUnlinked); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::tagAdded(const Tag &tag) +{ + Q_UNUSED(tag) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::tagAdded, sAgentBase->d_ptr, &AgentBasePrivate::tagAdded); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::tagChanged(const Tag &tag) +{ + Q_UNUSED(tag) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::tagChanged, sAgentBase->d_ptr, &AgentBasePrivate::tagChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::tagRemoved(const Tag &tag) +{ + Q_UNUSED(tag) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::tagRemoved, sAgentBase->d_ptr, &AgentBasePrivate::tagRemoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::itemsTagsChanged(const Item::List &items, const QSet &addedTags, const QSet &removedTags) +{ + Q_UNUSED(items) + Q_UNUSED(addedTags) + Q_UNUSED(removedTags) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::itemsTagsChanged, sAgentBase->d_ptr, &AgentBasePrivate::itemsTagsChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::relationAdded(const Akonadi::Relation &relation) +{ + Q_UNUSED(relation) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::relationAdded, sAgentBase->d_ptr, &AgentBasePrivate::relationAdded); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::relationRemoved(const Akonadi::Relation &relation) +{ + Q_UNUSED(relation) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + QObject::disconnect(sAgentBase->changeRecorder(), &Monitor::relationRemoved, sAgentBase->d_ptr, &AgentBasePrivate::relationRemoved); + sAgentBase->d_ptr->changeProcessed(); + } +} + +void AgentBase::ObserverV4::itemsRelationsChanged(const Akonadi::Item::List &items, + const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations) +{ + Q_UNUSED(items) + Q_UNUSED(addedRelations) + Q_UNUSED(removedRelations) + + if (sAgentBase) { + // not implementation, let's disconnect the signal to enable optimization in Monitor + disconnect(sAgentBase->changeRecorder(), &Monitor::itemsRelationsChanged, sAgentBase->d_ptr, &AgentBasePrivate::itemsRelationsChanged); + sAgentBase->d_ptr->changeProcessed(); + } +} + +/// @cond PRIVATE + +AgentBasePrivate::AgentBasePrivate(AgentBase *parent) + : q_ptr(parent) + , mStatusCode(AgentBase::Idle) + , mProgress(0) + , mNeedsNetwork(false) + , mOnline(false) + , mDesiredOnlineState(false) + , mPendingQuit(false) + , mSettings(nullptr) + , mChangeRecorder(nullptr) + , mTracer(nullptr) + , mObserver(nullptr) + , mPowerInterface(nullptr) + , mTemporaryOfflineTimer(nullptr) + , mEventLoopLocker(nullptr) + , mNetworkManager(nullptr) +{ + Internal::setClientType(Internal::Agent); +} + +AgentBasePrivate::~AgentBasePrivate() +{ + mChangeRecorder->setConfig(nullptr); + delete mSettings; +} + +void AgentBasePrivate::init() +{ + Q_Q(AgentBase); +#if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0) + Kdelibs4ConfigMigrator migrate(mId); + migrate.setConfigFiles(QStringList() << QStringLiteral("%1rc").arg(mId)); + migrate.migrate(); +#endif + /** + * Create a default session for this process. + */ + SessionPrivate::createDefaultSession(mId.toLatin1()); + + mTracer = + new org::freedesktop::Akonadi::Tracer(ServerManager::serviceName(ServerManager::Server), QStringLiteral("/tracing"), QDBusConnection::sessionBus(), q); + + new Akonadi__ControlAdaptor(q); + new Akonadi__StatusAdaptor(q); + if (!QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), q, QDBusConnection::ExportAdaptors)) { + Q_EMIT q->error(i18n("Unable to register object at dbus: %1", QDBusConnection::sessionBus().lastError().message())); + } + + mSettings = new QSettings(ServerManager::agentConfigFilePath(mId), QSettings::IniFormat); + + mChangeRecorder = new ChangeRecorder(q); + mChangeRecorder->setObjectName(QStringLiteral("AgentBaseChangeRecorder")); + mChangeRecorder->ignoreSession(Session::defaultSession()); + mChangeRecorder->itemFetchScope().setCacheOnly(true); + mChangeRecorder->setConfig(mSettings); + + mDesiredOnlineState = mSettings->value(QStringLiteral("Agent/DesiredOnlineState"), true).toBool(); + mOnline = mDesiredOnlineState; + + // reinitialize the status message now that online state is available + mStatusMessage = defaultReadyMessage(); + + mName = mSettings->value(QStringLiteral("Agent/Name")).toString(); + if (mName.isEmpty()) { + mName = mSettings->value(QStringLiteral("Resource/Name")).toString(); + if (!mName.isEmpty()) { + mSettings->remove(QStringLiteral("Resource/Name")); + mSettings->setValue(QStringLiteral("Agent/Name"), mName); + } + } + + connect(mChangeRecorder, &Monitor::itemAdded, this, &AgentBasePrivate::itemAdded); + connect(mChangeRecorder, &Monitor::itemChanged, this, &AgentBasePrivate::itemChanged); + connect(mChangeRecorder, &Monitor::collectionAdded, this, &AgentBasePrivate::collectionAdded); + connect(mChangeRecorder, + qOverload(&ChangeRecorder::collectionChanged), + this, + qOverload(&AgentBasePrivate::collectionChanged)); + connect(mChangeRecorder, + qOverload &>(&ChangeRecorder::collectionChanged), + this, + qOverload &>(&AgentBasePrivate::collectionChanged)); + connect(mChangeRecorder, &Monitor::collectionMoved, this, &AgentBasePrivate::collectionMoved); + connect(mChangeRecorder, &Monitor::collectionRemoved, this, &AgentBasePrivate::collectionRemoved); + connect(mChangeRecorder, &Monitor::collectionSubscribed, this, &AgentBasePrivate::collectionSubscribed); + connect(mChangeRecorder, &Monitor::collectionUnsubscribed, this, &AgentBasePrivate::collectionUnsubscribed); + + connect(q, qOverload(&AgentBase::status), this, &AgentBasePrivate::slotStatus); + connect(q, &AgentBase::percent, this, &AgentBasePrivate::slotPercent); + connect(q, &AgentBase::warning, this, &AgentBasePrivate::slotWarning); + connect(q, &AgentBase::error, this, &AgentBasePrivate::slotError); + + mPowerInterface = new QDBusInterface(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement/Actions/SuspendSession"), + QStringLiteral("org.kde.Solid.PowerManagement.Actions.SuspendSession"), + QDBusConnection::sessionBus(), + this); + if (mPowerInterface->isValid()) { + connect(mPowerInterface, SIGNAL(resumingFromSuspend()), this, SLOT(slotResumedFromSuspend())); // clazy:exclude=old-style-connect + } else { + delete mPowerInterface; + mPowerInterface = nullptr; + } + + // Use reference counting to allow agents to finish internal jobs when the + // agent is stopped. + mEventLoopLocker = new QEventLoopLocker(); + + mResourceTypeName = AgentManager::self()->instance(mId).type().name(); + setProgramName(); + + QTimer::singleShot(0, q, [this] { + delayedInit(); + }); +} + +void AgentBasePrivate::delayedInit() +{ + Q_Q(AgentBase); + + const QString serviceId = ServerManager::agentServiceName(ServerManager::Agent, mId); + if (!QDBusConnection::sessionBus().registerService(serviceId)) { + qCCritical(AKONADIAGENTBASE_LOG) << "Unable to register service" << serviceId << "at dbus:" << QDBusConnection::sessionBus().lastError().message(); + } + q->setOnlineInternal(mDesiredOnlineState); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Debug"), this, QDBusConnection::ExportScriptableSlots); +} + +void AgentBasePrivate::setProgramName() +{ + // ugly, really ugly, if you find another solution, change it and blame me for this code (Andras) + QString programName = mResourceTypeName; + if (!mName.isEmpty()) { + programName = i18nc("Name and type of Akonadi resource", "%1 of type %2", mName, mResourceTypeName); + } + + QGuiApplication::setApplicationDisplayName(programName); +} + +void AgentBasePrivate::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + if (mObserver) { + mObserver->itemAdded(item, collection); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers) +{ + if (mObserver) { + mObserver->itemChanged(item, partIdentifiers); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + auto observer2 = dynamic_cast(mObserver); + if (mObserver) { + // inter-resource moves, requires we know which resources the source and destination are in though + if (!source.resource().isEmpty() && !dest.resource().isEmpty()) { + if (source.resource() != dest.resource()) { + if (source.resource() == q_ptr->identifier()) { // moved away from us + Akonadi::Item i(item); + i.setParentCollection(source); + mObserver->itemRemoved(i); + } else if (dest.resource() == q_ptr->identifier()) { // moved to us + mObserver->itemAdded(item, dest); + } else if (observer2) { + observer2->itemMoved(item, source, dest); + } else { + // not for us, not sure if we should get here at all + changeProcessed(); + } + return; + } + } + // intra-resource move + if (observer2) { + observer2->itemMoved(item, source, dest); + } else { + // ### we cannot just call itemRemoved here as this will already trigger changeProcessed() + // so, just itemAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using ObserverV2 + mObserver->itemAdded(item, dest); + // mObserver->itemRemoved( item ); + } + } +} + +void AgentBasePrivate::itemRemoved(const Akonadi::Item &item) +{ + if (mObserver) { + mObserver->itemRemoved(item); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + auto observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->itemLinked(item, collection); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + auto observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->itemUnlinked(item, collection); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) +{ + auto observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsFlagsChanged(items, addedFlags, removedFlags); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source, const Akonadi::Collection &destination) +{ + auto observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsMoved(items, source, destination); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsRemoved(const Akonadi::Item::List &items) +{ + auto observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsRemoved(items); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) +{ + if (!mObserver) { + changeProcessed(); + return; + } + + auto observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsLinked(items, collection); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection) +{ + if (!mObserver) { + changeProcessed(); + return; + } + + auto observer3 = dynamic_cast(mObserver); + if (observer3) { + observer3->itemsUnlinked(items, collection); + } else { + Q_ASSERT_X(false, Q_FUNC_INFO, "Batch slots must never be called when ObserverV3 is not available"); + } +} + +void AgentBasePrivate::tagAdded(const Akonadi::Tag &tag) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->tagAdded(tag); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::tagChanged(const Akonadi::Tag &tag) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->tagChanged(tag); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::tagRemoved(const Akonadi::Tag &tag) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->tagRemoved(tag); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->itemsTagsChanged(items, addedTags, removedTags); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::relationAdded(const Akonadi::Relation &relation) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->relationAdded(relation); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::relationRemoved(const Akonadi::Relation &relation) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->relationRemoved(relation); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::itemsRelationsChanged(const Akonadi::Item::List &items, + const Akonadi::Relation::List &addedRelations, + const Akonadi::Relation::List &removedRelations) +{ + auto observer4 = dynamic_cast(mObserver); + if (observer4) { + observer4->itemsRelationsChanged(items, addedRelations, removedRelations); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + if (mObserver) { + mObserver->collectionAdded(collection, parent); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionChanged(const Akonadi::Collection &collection) +{ + auto observer2 = dynamic_cast(mObserver); + if (mObserver && observer2 == nullptr) { // For ObserverV2 we use the variant with the part identifiers + mObserver->collectionChanged(collection); + } else if (!mObserver) { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes) +{ + auto observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->collectionChanged(collection, changedAttributes); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest) +{ + auto observer2 = dynamic_cast(mObserver); + if (observer2) { + observer2->collectionMoved(collection, source, dest); + } else if (mObserver) { + // ### we cannot just call collectionRemoved here as this will already trigger changeProcessed() + // so, just collectionAdded() is good enough as no resource can have implemented intra-resource moves anyway + // without using ObserverV2 + mObserver->collectionAdded(collection, dest); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionRemoved(const Akonadi::Collection &collection) +{ + if (mObserver) { + mObserver->collectionRemoved(collection); + } else { + changeProcessed(); + } +} + +void AgentBasePrivate::collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + Q_UNUSED(collection) + Q_UNUSED(parent) + changeProcessed(); +} + +void AgentBasePrivate::collectionUnsubscribed(const Akonadi::Collection &collection) +{ + Q_UNUSED(collection) + changeProcessed(); +} + +void AgentBasePrivate::changeProcessed() +{ + mChangeRecorder->changeProcessed(); + QTimer::singleShot(0, mChangeRecorder, &ChangeRecorder::replayNext); +} + +void AgentBasePrivate::slotStatus(int status, const QString &message) +{ + mStatusMessage = message; + mStatusCode = 0; + + switch (status) { + case AgentBase::Idle: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultReadyMessage(); + } + + mStatusCode = 0; + break; + case AgentBase::Running: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultSyncingMessage(); + } + + mStatusCode = 1; + break; + case AgentBase::Broken: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultErrorMessage(); + } + + mStatusCode = 2; + break; + + case AgentBase::NotConfigured: + if (mStatusMessage.isEmpty()) { + mStatusMessage = defaultUnconfiguredMessage(); + } + + mStatusCode = 3; + break; + + default: + Q_ASSERT(!"Unknown status passed"); + break; + } +} + +void AgentBasePrivate::slotPercent(int progress) +{ + mProgress = progress; +} + +void AgentBasePrivate::slotWarning(const QString &message) +{ + mTracer->warning(QStringLiteral("AgentBase(%1)").arg(mId), message); +} + +void AgentBasePrivate::slotError(const QString &message) +{ + mTracer->error(QStringLiteral("AgentBase(%1)").arg(mId), message); +} + +void AgentBasePrivate::slotNetworkStatusChange(bool isOnline) +{ + Q_UNUSED(isOnline) + Q_Q(AgentBase); + q->setOnlineInternal(mDesiredOnlineState); +} + +void AgentBasePrivate::slotResumedFromSuspend() +{ + if (mNeedsNetwork) { + slotNetworkStatusChange(mNetworkManager->isOnline()); + } +} + +void AgentBasePrivate::slotTemporaryOfflineTimeout() +{ + Q_Q(AgentBase); + q->setOnlineInternal(true); +} + +QString AgentBasePrivate::dumpNotificationListToString() const +{ + return mChangeRecorder->dumpNotificationListToString(); +} + +void AgentBasePrivate::dumpMemoryInfo() const +{ + // Send it to stdout, so we can debug user problems. + // since you have to explicitly call this + // it won't flood users with release builds. + QTextStream stream(stdout); + stream << dumpMemoryInfoToString(); +} + +QString AgentBasePrivate::dumpMemoryInfoToString() const +{ + // man mallinfo for more info + QString str; +#if defined __GLIBC__ + struct mallinfo mi; + mi = mallinfo(); + QTextStream stream(&str); + stream << "Total non-mmapped bytes (arena): " << mi.arena << '\n' + << "# of free chunks (ordblks): " << mi.ordblks << '\n' + << "# of free fastbin blocks (smblks>: " << mi.smblks << '\n' + << "# of mapped regions (hblks): " << mi.hblks << '\n' + << "Bytes in mapped regions (hblkhd): " << mi.hblkhd << '\n' + << "Max. total allocated space (usmblks): " << mi.usmblks << '\n' + << "Free bytes held in fastbins (fsmblks):" << mi.fsmblks << '\n' + << "Total allocated space (uordblks): " << mi.uordblks << '\n' + << "Total free space (fordblks): " << mi.fordblks << '\n' + << "Topmost releasable block (keepcost): " << mi.keepcost << '\n'; +#else + str = QLatin1String("mallinfo() not supported"); +#endif + return str; +} + +AgentBase::AgentBase(const QString &id) + : d_ptr(new AgentBasePrivate(this)) +{ + sAgentBase = this; + d_ptr->mId = id; + d_ptr->init(); +} + +AgentBase::AgentBase(AgentBasePrivate *d, const QString &id) + : d_ptr(d) +{ + sAgentBase = this; + d_ptr->mId = id; + d_ptr->init(); +} + +AgentBase::~AgentBase() +{ + delete d_ptr; +} + +void AgentBase::debugAgent(int argc, char **argv) +{ + Q_UNUSED(argc) +#ifdef Q_OS_WIN + if (qEnvironmentVariableIsSet("AKONADI_DEBUG_WAIT")) { + if (QByteArray(argv[0]).endsWith(qgetenv("AKONADI_DEBUG_WAIT") + ".exe")) { + while (!IsDebuggerPresent()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + DebugBreak(); + } + } +#else + Q_UNUSED(argv) +#endif +} + +QString AgentBase::parseArguments(int argc, char **argv) +{ + Q_UNUSED(argc) + + QCommandLineOption identifierOption(QStringLiteral("identifier"), i18n("Agent identifier"), QStringLiteral("argument")); + QCommandLineParser parser; + parser.addOption(identifierOption); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(*qApp); + parser.setApplicationDescription(i18n("Akonadi Agent")); + + if (!parser.isSet(identifierOption)) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier argument missing"; + exit(1); + } + + const QString identifier = parser.value(identifierOption); + if (identifier.isEmpty()) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier argument is empty"; + exit(1); + } + + QCoreApplication::setApplicationName(ServerManager::addNamespace(identifier)); + QCoreApplication::setApplicationVersion(QStringLiteral(AKONADI_FULL_VERSION)); + + const QFileInfo fi(QString::fromLocal8Bit(argv[0])); + // strip off full path and possible .exe suffix + const QString catalog = fi.baseName(); + + auto translator = new QTranslator(qApp); + translator->load(catalog); + QCoreApplication::installTranslator(translator); + + return identifier; +} + +/// @endcond + +int AgentBase::init(AgentBase &r) +{ + KLocalizedString::setApplicationDomain("libakonadi5"); + KAboutData::setApplicationData(r.aboutData()); + return qApp->exec(); +} + +int AgentBase::status() const +{ + Q_D(const AgentBase); + + return d->mStatusCode; +} + +QString AgentBase::statusMessage() const +{ + Q_D(const AgentBase); + + return d->mStatusMessage; +} + +int AgentBase::progress() const +{ + Q_D(const AgentBase); + + return d->mProgress; +} + +QString AgentBase::progressMessage() const +{ + Q_D(const AgentBase); + + return d->mProgressMessage; +} + +bool AgentBase::isOnline() const +{ + Q_D(const AgentBase); + + return d->mOnline; +} + +void AgentBase::setNeedsNetwork(bool needsNetwork) +{ + Q_D(AgentBase); + if (d->mNeedsNetwork == needsNetwork) { + return; + } + + d->mNeedsNetwork = needsNetwork; + + if (d->mNeedsNetwork) { + d->mNetworkManager = new QNetworkConfigurationManager(this); + connect(d->mNetworkManager, &QNetworkConfigurationManager::onlineStateChanged, d, &AgentBasePrivate::slotNetworkStatusChange, Qt::UniqueConnection); + + } else { + delete d->mNetworkManager; + d->mNetworkManager = nullptr; + setOnlineInternal(d->mDesiredOnlineState); + } +} + +void AgentBase::setOnline(bool state) +{ + Q_D(AgentBase); + + if (d->mPendingQuit) { + return; + } + + d->mDesiredOnlineState = state; + d->mSettings->setValue(QStringLiteral("Agent/DesiredOnlineState"), state); + setOnlineInternal(state); +} + +void AgentBase::setTemporaryOffline(int makeOnlineInSeconds) +{ + Q_D(AgentBase); + + // if not currently online, avoid bringing it online after the timeout + if (!d->mOnline) { + return; + } + + setOnlineInternal(false); + + if (!d->mTemporaryOfflineTimer) { + d->mTemporaryOfflineTimer = new QTimer(d); + d->mTemporaryOfflineTimer->setSingleShot(true); + connect(d->mTemporaryOfflineTimer, &QTimer::timeout, d, &AgentBasePrivate::slotTemporaryOfflineTimeout); + } + d->mTemporaryOfflineTimer->setInterval(std::chrono::seconds{makeOnlineInSeconds}); + d->mTemporaryOfflineTimer->start(); +} + +void AgentBase::setOnlineInternal(bool state) +{ + Q_D(AgentBase); + if (state && d->mNeedsNetwork) { + if (!d->mNetworkManager->isOnline()) { + // Don't go online if the resource needs network but there is none + state = false; + } + } + d->mOnline = state; + + if (d->mTemporaryOfflineTimer) { + d->mTemporaryOfflineTimer->stop(); + } + + const QString newMessage = d->defaultReadyMessage(); + if (d->mStatusMessage != newMessage && d->mStatusCode != AgentBase::Broken) { + Q_EMIT status(d->mStatusCode, newMessage); + } + + doSetOnline(state); + Q_EMIT onlineChanged(state); +} + +void AgentBase::doSetOnline(bool online) +{ + Q_UNUSED(online) +} + +KAboutData AgentBase::aboutData() const +{ + return KAboutData(qApp->applicationName(), agentName(), qApp->applicationVersion()); +} + +void AgentBase::configure(WId windowId) +{ + Q_UNUSED(windowId) + + // Fallback if the agent implements the new plugin-based configuration, + // but someone calls the deprecated configure() method + auto instance = Akonadi::AgentManager::self()->instance(identifier()); + QPointer dialog = new AgentConfigurationDialog(instance, nullptr); + if (dialog->exec()) { + Q_EMIT configurationDialogAccepted(); + } else { + Q_EMIT configurationDialogRejected(); + } + delete dialog; +} + +#ifdef Q_OS_WIN // krazy:exclude=cpp +void AgentBase::configure(qlonglong windowId) +{ + configure(static_cast(windowId)); +} +#endif + +WId AgentBase::winIdForDialogs() const +{ + const bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.akonaditray")); + if (!registered) { + return 0; + } + + QDBusInterface dbus(QStringLiteral("org.freedesktop.akonaditray"), QStringLiteral("/Actions"), QStringLiteral("org.freedesktop.Akonadi.Tray")); + const QDBusMessage reply = dbus.call(QStringLiteral("getWinId")); + + if (reply.type() == QDBusMessage::ErrorMessage) { + return 0; + } + + const WId winid = static_cast(reply.arguments().at(0).toLongLong()); + + return winid; +} + +void AgentBase::quit() +{ + Q_D(AgentBase); + aboutToQuit(); + + if (d->mSettings) { + d->mChangeRecorder->setConfig(nullptr); + d->mSettings->sync(); + delete d->mSettings; + d->mSettings = nullptr; + } + + delete d->mEventLoopLocker; + d->mEventLoopLocker = nullptr; +} + +void AgentBase::aboutToQuit() +{ + Q_D(AgentBase); + d->mPendingQuit = true; +} + +void AgentBase::cleanup() +{ + Q_D(AgentBase); + // prevent the monitor from picking up deletion signals for our own data if we are a resource + // and thus avoid that we kill our own data as last act before our own death + d->mChangeRecorder->blockSignals(true); + + aboutToQuit(); + + const QString fileName = d->mSettings->fileName(); + + /* + * First destroy the settings object... + */ + d->mChangeRecorder->setConfig(nullptr); + delete d->mSettings; + d->mSettings = nullptr; + + /* + * ... then remove the file from hd. + */ + if (!QFile::remove(fileName)) { + qCWarning(AKONADIAGENTBASE_LOG) << "Impossible to remove " << fileName; + } + + /* + * ... and remove the changes file from hd. + */ + const QString changeDataFileName = fileName + QStringLiteral("_changes.dat"); + if (!QFile::remove(changeDataFileName)) { + qCWarning(AKONADIAGENTBASE_LOG) << "Impossible to remove " << changeDataFileName; + } + + /* + * ... and also remove the agent configuration file if there is one. + */ + const QString configFile = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + config()->name(); + if (!QFile::remove(configFile)) { + qCWarning(AKONADIAGENTBASE_LOG) << "Impossible to remove config file " << configFile; + } + + delete d->mEventLoopLocker; + d->mEventLoopLocker = nullptr; +} + +void AgentBase::registerObserver(Observer *observer) +{ + // TODO in theory we should re-connect change recorder signals here that we disconnected previously + d_ptr->mObserver = observer; + + const bool hasObserverV3 = (dynamic_cast(d_ptr->mObserver) != nullptr); + const bool hasObserverV4 = (dynamic_cast(d_ptr->mObserver) != nullptr); + + disconnect(d_ptr->mChangeRecorder, &Monitor::tagAdded, d_ptr, &AgentBasePrivate::tagAdded); + disconnect(d_ptr->mChangeRecorder, &Monitor::tagChanged, d_ptr, &AgentBasePrivate::tagChanged); + disconnect(d_ptr->mChangeRecorder, &Monitor::tagRemoved, d_ptr, &AgentBasePrivate::tagRemoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsTagsChanged, d_ptr, &AgentBasePrivate::itemsTagsChanged); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsFlagsChanged, d_ptr, &AgentBasePrivate::itemsFlagsChanged); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsMoved, d_ptr, &AgentBasePrivate::itemsMoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsRemoved, d_ptr, &AgentBasePrivate::itemsRemoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsLinked, d_ptr, &AgentBasePrivate::itemsLinked); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemsUnlinked, d_ptr, &AgentBasePrivate::itemsUnlinked); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemMoved, d_ptr, &AgentBasePrivate::itemMoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemRemoved, d_ptr, &AgentBasePrivate::itemRemoved); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemLinked, d_ptr, &AgentBasePrivate::itemLinked); + disconnect(d_ptr->mChangeRecorder, &Monitor::itemUnlinked, d_ptr, &AgentBasePrivate::itemUnlinked); + + if (hasObserverV4) { + connect(d_ptr->mChangeRecorder, &Monitor::tagAdded, d_ptr, &AgentBasePrivate::tagAdded); + connect(d_ptr->mChangeRecorder, &Monitor::tagChanged, d_ptr, &AgentBasePrivate::tagChanged); + connect(d_ptr->mChangeRecorder, &Monitor::tagRemoved, d_ptr, &AgentBasePrivate::tagRemoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemsTagsChanged, d_ptr, &AgentBasePrivate::itemsTagsChanged); + } + + if (hasObserverV3) { + connect(d_ptr->mChangeRecorder, &Monitor::itemsFlagsChanged, d_ptr, &AgentBasePrivate::itemsFlagsChanged); + connect(d_ptr->mChangeRecorder, &Monitor::itemsMoved, d_ptr, &AgentBasePrivate::itemsMoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemsRemoved, d_ptr, &AgentBasePrivate::itemsRemoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemsLinked, d_ptr, &AgentBasePrivate::itemsLinked); + connect(d_ptr->mChangeRecorder, &Monitor::itemsUnlinked, d_ptr, &AgentBasePrivate::itemsUnlinked); + } else { + // V2 - don't connect these if we have V3 + connect(d_ptr->mChangeRecorder, &Monitor::itemMoved, d_ptr, &AgentBasePrivate::itemMoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemRemoved, d_ptr, &AgentBasePrivate::itemRemoved); + connect(d_ptr->mChangeRecorder, &Monitor::itemLinked, d_ptr, &AgentBasePrivate::itemLinked); + connect(d_ptr->mChangeRecorder, &Monitor::itemUnlinked, d_ptr, &AgentBasePrivate::itemUnlinked); + } +} + +QString AgentBase::identifier() const +{ + return d_ptr->mId; +} + +void AgentBase::setAgentName(const QString &name) +{ + Q_D(AgentBase); + if (name == d->mName) { + return; + } + + // TODO: rename collection + d->mName = name; + + if (d->mName.isEmpty() || d->mName == d->mId) { + d->mSettings->remove(QStringLiteral("Resource/Name")); + d->mSettings->remove(QStringLiteral("Agent/Name")); + } else { + d->mSettings->setValue(QStringLiteral("Agent/Name"), d->mName); + } + + d->mSettings->sync(); + + d->setProgramName(); + + Q_EMIT agentNameChanged(d->mName); +} + +QString AgentBase::agentName() const +{ + Q_D(const AgentBase); + if (d->mName.isEmpty()) { + return d->mId; + } else { + return d->mName; + } +} + +void AgentBase::changeProcessed() +{ + Q_D(AgentBase); + d->changeProcessed(); +} + +ChangeRecorder *AgentBase::changeRecorder() const +{ + return d_ptr->mChangeRecorder; +} + +KSharedConfigPtr AgentBase::config() +{ + return KSharedConfig::openConfig(); +} + +void AgentBase::abort() +{ + Q_EMIT abortRequested(); +} + +void AgentBase::reconfigure() +{ + Q_EMIT reloadConfiguration(); +} + +#include "moc_agentbase.cpp" +#include "moc_agentbase_p.cpp" diff --git a/src/agentbase/agentbase.h b/src/agentbase/agentbase.h new file mode 100644 index 0000000..e4a1096 --- /dev/null +++ b/src/agentbase/agentbase.h @@ -0,0 +1,798 @@ +/* + This file is part of akonadiresources. + + SPDX-FileCopyrightText: 2006 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2008 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiagentbase_export.h" +#include "item.h" + +#include + +#include + +#include +#include + +class Akonadi__ControlAdaptor; +class Akonadi__StatusAdaptor; + +class KAboutData; + +namespace Akonadi +{ +class AgentBasePrivate; +class ChangeRecorder; +class Collection; +class Item; + +/** + * @short The base class for all Akonadi agents and resources. + * + * This class is a base class for all Akonadi agents, which covers the real + * agent processes and all resources. + * + * It provides: + * - lifetime management + * - change monitoring and recording + * - configuration interface + * - problem reporting + * + * Akonadi Server supports several ways to launch agents and resources: + * - As a separate application (@see AKONADI_AGENT_MAIN) + * - As a thread in the AgentServer + * - As a separate process, using the akonadi_agent_launcher + * + * The idea is this, the agent or resource is written as a plugin instead of an + * executable (@see AgentFactory). In the AgentServer case, the AgentServer + * looks up the plugin and launches the agent in a separate thread. In the + * launcher case, a new akonadi_agent_launcher process is started for each + * agent or resource instance. + * + * When making an Agent or Resource suitable for running in the AgentServer some + * extra caution is needed. Because multiple instances of several kinds of agents + * run in the same process, one cannot blindly use global objects like KGlobal. + * For this reasons several methods where added to avoid problems in this context, + * most notably AgentBase::config(). Additionally, + * one cannot use QDBusConnection::sessionBus() with dbus < 1.4, because of a + * multithreading bug in libdbus. Instead one should use + * QDBusConnection::sessionBus() which works around this problem. + * + * @author Till Adam , Volker Krause + */ +class AKONADIAGENTBASE_EXPORT AgentBase : public QObject, protected QDBusContext +{ + Q_OBJECT + +public: + /** + * @short The interface for reacting on monitored or replayed changes. + * + * The Observer provides an interface to react on monitored or replayed changes. + * + * Since the this base class does only tell the change recorder that the change + * has been processed, an AgentBase subclass which wants to actually process + * the change needs to subclass Observer and reimplement the methods it is + * interested in. + * + * Such an agent specific Observer implementation can either be done + * stand-alone, i.e. as a separate object, or by inheriting both AgentBase + * and AgentBase::Observer. + * + * The observer implementation then has registered with the agent, so it + * can forward the incoming changes to the observer. + * + * @note In the multiple inheritance approach the init() method automatically + * registers itself as the observer. + * + * @note Do not call the base implementation of reimplemented virtual methods! + * The default implementation disconnected themselves from the Akonadi::ChangeRecorder + * to enable internal optimizations for unused notifications. + * + * Example for stand-alone observer: + * @code + * class ExampleAgent : public AgentBase + * { + * public: + * ExampleAgent( const QString &id ); + * + * ~ExampleAgent(); + * + * private: + * AgentBase::Observer *mObserver; + * }; + * + * class ExampleObserver : public AgentBase::Observer + * { + * protected: + * void itemChanged( const Item &item ); + * }; + * + * ExampleAgent::ExampleAgent( const QString &id ) + : AgentBase( id ) + , mObserver( 0 ) + * { + * mObserver = new ExampleObserver(); + * registerObserver( mObserver ); + * } + * + * ExampleAgent::~ExampleAgent() + * { + * delete mObserver; + * } + * + * void ExampleObserver::itemChanged( const Item &item ) + * { + * // do something with item + * qCDebug(AKONADIAGENTBASE_LOG) << "Item id=" << item.id(); + * + * // let base implementation tell the change recorder that we + * // have processed the change + * AgentBase::Observer::itemChanged( item ); + * } + * @endcode + * + * Example for observer through multiple inheritance: + * @code + * class ExampleAgent : public AgentBase, public AgentBase::Observer + * { + * public: + * ExampleAgent( const QString &id ); + * + * protected: + * void itemChanged( const Item &item ); + * }; + * + * ExampleAgent::ExampleAgent( const QString &id ) + : AgentBase( id ) + * { + * // no need to create or register observer since + * // we are the observer and registration happens automatically + * // in init() + * } + * + * void ExampleAgent::itemChanged( const Item &item ) + * { + * // do something with item + * qCDebug(AKONADIAGENTBASE_LOG) << "Item id=" << item.id(); + * + * // let base implementation tell the change recorder that we + * // have processed the change + * AgentBase::Observer::itemChanged( item ); + * } + * @endcode + * + * @author Kevin Krammer + * + * @deprecated Use ObserverV2 instead + */ + class AKONADIAGENTBASE_DEPRECATED AKONADIAGENTBASE_EXPORT Observer // krazy:exclude=dpointer + { + public: + /** + * Creates an observer instance. + */ + Observer(); + + /** + * Destroys the observer instance. + */ + virtual ~Observer(); + + /** + * Reimplement to handle adding of new items. + * @param item The newly added item. + * @param collection The collection @p item got added to. + */ + virtual void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * Reimplement to handle changes to existing items. + * @param item The changed item. + * @param partIdentifiers The identifiers of the item parts that has been changed. + */ + virtual void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); + + /** + * Reimplement to handle deletion of items. + * @param item The deleted item. + */ + virtual void itemRemoved(const Akonadi::Item &item); + + /** + * Reimplement to handle adding of new collections. + * @param collection The newly added collection. + * @param parent The parent collection. + */ + virtual void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + /** + * Reimplement to handle changes to existing collections. + * @param collection The changed collection. + */ + virtual void collectionChanged(const Akonadi::Collection &collection); + + /** + * Reimplement to handle deletion of collections. + * @param collection The deleted collection. + */ + virtual void collectionRemoved(const Akonadi::Collection &collection); + }; + + /** + * BC extension of Observer with support for monitoring item and collection moves. + * Use this one instead of Observer. + * + * @since 4.4 + */ + class AKONADIAGENTBASE_EXPORT ObserverV2 : public Observer // krazy:exclude=dpointer + { + public: + using Observer::collectionChanged; + + /** + * Reimplement to handle item moves. + * When using this class in combination with Akonadi::ResourceBase, inter-resource + * moves are handled internally already and the corresponding add or delete method + * is called instead. + * + * @param item The moved item. + * @param collectionSource The collection the item has been moved from. + * @param collectionDestination The collection the item has been moved to. + */ + virtual void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination); + + /** + * Reimplement to handle item linking. + * This is only relevant for virtual resources. + * @param item The linked item. + * @param collection The collection the item is linked to. + */ + virtual void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * Reimplement to handle item unlinking. + * This is only relevant for virtual resources. + * @param item The unlinked item. + * @param collection The collection the item is unlinked from. + */ + virtual void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * Reimplement to handle collection moves. + * When using this class in combination with Akonadi::ResourceBase, inter-resource + * moves are handled internally already and the corresponding add or delete method + * is called instead. + * + * @param collection The moved collection. + * @param collectionSource The previous parent collection. + * @param collectionDestination The new parent collection. + */ + virtual void + collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination); + + /** + * Reimplement to handle changes to existing collections. + * @param collection The changed collection. + * @param changedAttributes The identifiers of the collection parts/attributes that has been changed. + */ + virtual void collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes); + }; + + /** + * BC extension of ObserverV2 with support for batch operations + * + * @warning When using ObserverV3, you will never get single-item notifications + * from AgentBase::Observer, even when you don't reimplement corresponding batch + * method from ObserverV3. For instance, when you don't reimplement itemsRemoved() + * here, you will not get any notifications about item removal whatsoever! + * + * @since 4.11 + */ + class AKONADIAGENTBASE_EXPORT ObserverV3 : public ObserverV2 // krazy:exclude=dpointer + { + public: + /** + * Reimplement to handle changes in flags of existing items + * + * @warning When using ObserverV3, you will never get notifications about + * flag changes via Observer::itemChanged(), even when you don't reimplement + * itemsFlagsChanged()! + * + * @param items The changed items + * @param addedFlags Flags that have been added to the item + * @param removedFlags Flags that have been removed from the item + */ + virtual void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags); + + /** + * Reimplement to handle batch notification about items deletion. + * + * @param items List of deleted items + */ + virtual void itemsRemoved(const Akonadi::Item::List &items); + + /** + * Reimplement to handle batch notification about items move + * + * @param items List of moved items + * @param sourceCollection Collection from where the items were moved + * @param destinationCollection Collection to which the items were moved + */ + virtual void + itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &sourceCollection, const Akonadi::Collection &destinationCollection); + + /** + * Reimplement to handle batch notifications about items linking. + * + * @param items Linked items + * @param collection Collection to which the items have been linked + */ + virtual void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + /** + * Reimplement to handle batch notifications about items unlinking. + * + * @param items Unlinked items + * @param collection Collection from which the items have been unlinked + */ + virtual void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + }; + + /** + * Observer that adds support for item tagging + * + * @warning ObserverV4 subclasses ObserverV3 which changes behavior of some of the + * virtual methods from Observer and ObserverV2. Please make sure you read + * documentation of ObserverV3 and adapt your agent accordingly. + * + * @since 4.13 + */ + class AKONADIAGENTBASE_EXPORT ObserverV4 : public ObserverV3 // krazy:exclude=dpointer + { + public: + /** + * Reimplement to handle tags additions + * + * @param tag Newly added tag + */ + virtual void tagAdded(const Akonadi::Tag &tag); + + /** + * Reimplement to handle tags changes + * + * @param tag Tag that has been changed + */ + virtual void tagChanged(const Akonadi::Tag &tag); + + /** + * Reimplement to handle tags removal. + * + * @note All items that were tagged by @p tag will get a separate notification + * about untagging via itemsTagsChanged(). It is guaranteed that the itemsTagsChanged() + * notification will be delivered before this one. + * + * @param tag Tag that has been removed. + */ + virtual void tagRemoved(const Akonadi::Tag &tag); + + /** + * Reimplement to handle items tagging + * + * @param items Items that were tagged or untagged + * @param addedTags Set of tags that were added to all @p items + * @param removedTags Set of tags that were removed from all @p items + */ + virtual void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags); + + /** + * Reimplement to handle relations being added + */ + virtual void relationAdded(const Akonadi::Relation &relation); + + /** + * Reimplement to handle relations being removed + */ + virtual void relationRemoved(const Akonadi::Relation &relation); + + /** + * Reimplement to handled relations changing on items + * @param items Items that had relations added/removed from them + * @param addedRelations the list of relations that were added to all @p items + * @param removedRelations the list of relations that were removed from all @p items + */ + virtual void + itemsRelationsChanged(const Akonadi::Item::List &items, const Akonadi::Relation::List &addedRelations, const Akonadi::Relation::List &removedRelations); + }; + + /** + * This enum describes the different states the + * agent can be in. + */ + enum Status { + Idle = 0, ///< The agent does currently nothing. + Running, ///< The agent is working on something. + Broken, ///< The agent encountered an error state. + NotConfigured ///< The agent is lacking required configuration + }; + + /** + * Use this method in the main function of your agent + * application to initialize your agent subclass. + * This method also takes care of creating a KApplication + * object and parsing command line arguments. + * + * @note In case the given class is also derived from AgentBase::Observer + * it gets registered as its own observer (see AgentBase::Observer), e.g. + * agentInstance->registerObserver( agentInstance ); + * + * @code + * + * class MyAgent : public AgentBase + * { + * ... + * }; + * + * AKONADI_AGENT_MAIN( MyAgent ) + * + * @endcode + * + * @param argc number of arguments + * @param argv arguments for the function + */ + template static int init(int argc, char **argv) + { + // Disable session management + qunsetenv("SESSION_MANAGER"); + + QApplication app(argc, argv); + debugAgent(argc, argv); + const QString id = parseArguments(argc, argv); + T r(id); + + // check if T also inherits AgentBase::Observer and + // if it does, automatically register it on itself + auto observer = dynamic_cast(&r); + if (observer != nullptr) { + r.registerObserver(observer); + } + return init(r); + } + + /** + * This method returns the current status code of the agent. + * + * The following return values are possible: + * + * - 0 - Idle + * - 1 - Running + * - 2 - Broken + * - 3 - NotConfigured + */ + Q_REQUIRED_RESULT virtual int status() const; + + /** + * This method returns an i18n'ed description of the current status code. + */ + Q_REQUIRED_RESULT virtual QString statusMessage() const; + + /** + * This method returns the current progress of the agent in percentage. + */ + Q_REQUIRED_RESULT virtual int progress() const; + + /** + * This method returns an i18n'ed description of the current progress. + */ + Q_REQUIRED_RESULT virtual QString progressMessage() const; + +public Q_SLOTS: + /** + * This method is called whenever the agent shall show its configuration dialog + * to the user. It will be automatically called when the agent is started for + * the first time. + * + * @param windowId The parent window id. + * + * @note If the method is reimplemented it has to emit the configurationDialogAccepted() + * or configurationDialogRejected() signals depending on the users choice. + */ + virtual void configure(WId windowId); + +public: + /** + * This method returns the windows id, which should be used for dialogs. + */ + Q_REQUIRED_RESULT WId winIdForDialogs() const; + +#ifdef Q_OS_WIN + /** + * Overload of @ref configure needed because WId cannot be automatically casted + * to qlonglong on Windows. + */ + void configure(qlonglong windowId); +#endif + + /** + * Returns the instance identifier of this agent. + */ + Q_REQUIRED_RESULT QString identifier() const; + + /** + * This method is called when the agent is removed from + * the system, so it can do some cleanup stuff. + * + * @note If you reimplement this in a subclass make sure + * to call this base implementation at the end. + */ + virtual void cleanup(); + + /** + * Registers the given observer for reacting on monitored or recorded changes. + * + * @param observer The change handler to register. No ownership transfer, i.e. + * the caller stays owner of the pointer and can reset + * the registration by calling this method with @c 0 + */ + void registerObserver(Observer *observer); + + /** + * This method is used to set the name of the agent. + * + * @since 4.3 + * @param name name of the agent + */ + // FIXME_API: make sure location is renamed to this by agentbase + void setAgentName(const QString &name); + + /** + * Returns the name of the agent. + * + * @since 4.3 + */ + Q_REQUIRED_RESULT QString agentName() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever the name of the agent has changed. + * + * @param name The new name of the agent. + * + * @since 4.3 + */ + void agentNameChanged(const QString &name); + + /** + * This signal should be emitted whenever the status of the agent has been changed. + * @param status The new Status code. + * @param message A i18n'ed description of the new status. + */ + void status(int status, const QString &message = QString()); + + /** + * This signal should be emitted whenever the progress of an action in the agent + * (e.g. data transfer, connection establishment to remote server etc.) has changed. + * + * @param progress The progress of the action in percent. + */ + void percent(int progress); + + /** + * This signal shall be used to report warnings. + * + * @param message The i18n'ed warning message. + */ + void warning(const QString &message); + + /** + * This signal shall be used to report errors. + * + * @param message The i18n'ed error message. + */ + void error(const QString &message); + + /** + * This signal should be emitted whenever the status of the agent has been changed. + * @param status The object that describes the status change. + * + * @since 4.6 + */ + void advancedStatus(const QVariantMap &status); + + /** + * Emitted when another application has remotely asked the agent to abort + * its current operation. + * Connect to this signal if your agent supports abortion. After aborting + * and cleaning up, agents should return to Idle status. + * + * @since 4.4 + */ + void abortRequested(); + + /** + * Emitted if another application has changed the agent's configuration remotely + * and called AgentInstance::reconfigure(). + * + * @since 4.2 + */ + void reloadConfiguration(); + + /** + * Emitted when the online state changed. + * @param online The online state. + * @since 4.2 + */ + void onlineChanged(bool online); + + /** + * This signal is emitted whenever the user has accepted the configuration dialog. + * + * @note Implementors of agents/resources are responsible to emit this signal if + * the agent/resource reimplements configure(). + * + * @since 4.4 + */ + void configurationDialogAccepted(); + + /** + * This signal is emitted whenever the user has rejected the configuration dialog. + * + * @note Implementors of agents/resources are responsible to emit this signal if + * the agent/resource reimplements configure(). + * + * @since 4.4 + */ + void configurationDialogRejected(); + +protected: + /** + * Creates an agent base. + * + * @param id The instance id of the agent. + */ + AgentBase(const QString &id); + + /** + * Destroys the agent base. + */ + ~AgentBase(); + + /** + * This method is called whenever the agent application is about to + * quit. + * + * Reimplement this method to do session cleanup (e.g. disconnecting + * from groupware server). + */ + virtual void aboutToQuit(); + + /** + * Returns the Akonadi::ChangeRecorder object used for monitoring. + * Use this to configure which parts you want to monitor. + */ + ChangeRecorder *changeRecorder() const; + + /** + * Returns the config object for this Agent. + */ + KSharedConfigPtr config(); + + /** + * Marks the current change as processes and replays the next change if change + * recording is enabled (noop otherwise). This method is called + * from the default implementation of the change notification slots. While not + * required when not using change recording, it is nevertheless recommended + * to call this method when done with processing a change notification. + */ + void changeProcessed(); + + /** + * Returns whether the agent is currently online. + */ + bool isOnline() const; + + /** + * Sets whether the agent needs network or not. + * + * @since 4.2 + * @todo use this in combination with QNetworkConfiguration to change + * the onLine status of the agent. + * @param needsNetwork @c true if the agents needs network. Defaults to @c false + */ + void setNeedsNetwork(bool needsNetwork); + + /** + * Sets whether the agent shall be online or not. + */ + void setOnline(bool state); + +protected: + /** + * Sets the agent offline but will make it online again after a given time + * + * Use this method when the agent detects some problem with its backend but it wants + * to retry all pending operations after some time - e.g. a server can not be reached currently + * + * Example usage: + * @code + * void ExampleResource::onItemRemovedFinished(KJob *job) + * { + * if (job->error()) { + * Q_EMIT status(Broken, job->errorString()); + * deferTask(); + * setTemporaryOffline(300); + * return; + * } + * ... + * } + * @endcode + * + * @since 4.13 + * @param makeOnlineInSeconds timeout in seconds after which the agent changes to online + */ + void setTemporaryOffline(int makeOnlineInSeconds = 300); + + /// @cond PRIVATE + static void debugAgent(int argc, char **argv); + + AgentBasePrivate *d_ptr; + explicit AgentBase(AgentBasePrivate *d, const QString &id); + friend class ObserverV2; + /// @endcond + + /** + * This method is called whenever the @p online status has changed. + * Reimplement this method to react on online status changes. + * @param online online status + */ + virtual void doSetOnline(bool online); + + virtual KAboutData aboutData() const; + +private: + /// @cond PRIVATE + static QString parseArguments(int argc, char **argv); + static int init(AgentBase &r); + void setOnlineInternal(bool state); + + // D-Bus interface stuff + void abort(); + void reconfigure(); + void quit(); + + // dbus agent interface + friend class ::Akonadi__StatusAdaptor; + friend class ::Akonadi__ControlAdaptor; + + Q_DECLARE_PRIVATE(AgentBase) + Q_PRIVATE_SLOT(d_func(), void delayedInit()) + Q_PRIVATE_SLOT(d_func(), void slotStatus(int, const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotPercent(int)) + Q_PRIVATE_SLOT(d_func(), void slotWarning(const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotError(const QString &)) + Q_PRIVATE_SLOT(d_func(), void slotNetworkStatusChange(bool)) + Q_PRIVATE_SLOT(d_func(), void slotResumedFromSuspend()) + Q_PRIVATE_SLOT(d_func(), void slotTemporaryOfflineTimeout()) + + /// @endcond +}; + +} + +#ifndef AKONADI_AGENT_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi agents. + */ +#define AKONADI_AGENT_MAIN(agentClass) \ + int main(int argc, char **argv) \ + { \ + return Akonadi::AgentBase::init(argc, argv); \ + } +#endif + diff --git a/src/agentbase/agentbase_p.h b/src/agentbase/agentbase_p.h new file mode 100644 index 0000000..342fb08 --- /dev/null +++ b/src/agentbase/agentbase_p.h @@ -0,0 +1,144 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2008 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentbase.h" +#include "tracerinterface.h" + +#include + +class QSettings; +class QTimer; +class QNetworkConfigurationManager; + +namespace Akonadi +{ +/** + * @internal + */ +class AgentBasePrivate : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.dfaure") + +public: + explicit AgentBasePrivate(AgentBase *parent); + ~AgentBasePrivate() override; + void init(); + virtual void delayedInit(); + +public Q_SLOTS: + void slotStatus(int status, const QString &message); + void slotPercent(int progress); + void slotWarning(const QString &message); + void slotError(const QString &message); + void slotNetworkStatusChange(bool isOnline); + void slotResumedFromSuspend(); + void slotTemporaryOfflineTimeout(); + + virtual void changeProcessed(); + + QString defaultReadyMessage() const + { + if (mOnline) { + return i18nc("@info:status Application ready for work", "Ready"); + } + return i18nc("@info:status", "Offline"); + } + + QString defaultSyncingMessage() const + { + return i18nc("@info:status", "Syncing..."); + } + + QString defaultErrorMessage() const + { + return i18nc("@info:status", "Error."); + } + + QString defaultUnconfiguredMessage() const + { + return i18nc("@info:status", "Not configured"); + } + + void setProgramName(); + +public: + AgentBase *q_ptr; + Q_DECLARE_PUBLIC(AgentBase) + + QString mId; + QString mName; + QString mResourceTypeName; + + int mStatusCode; + QString mStatusMessage; + + int mProgress; + QString mProgressMessage; + + bool mNeedsNetwork; + bool mOnline; + bool mDesiredOnlineState; + + bool mPendingQuit; + + QSettings *mSettings = nullptr; + + ChangeRecorder *mChangeRecorder = nullptr; + + org::freedesktop::Akonadi::Tracer *mTracer = nullptr; + + AgentBase::Observer *mObserver = nullptr; + QDBusInterface *mPowerInterface = nullptr; + + QTimer *mTemporaryOfflineTimer = nullptr; + + QEventLoopLocker *mEventLoopLocker = nullptr; + QNetworkConfigurationManager *mNetworkManager = nullptr; + +public Q_SLOTS: + // Dump the contents of the current ChangeReplay + Q_SCRIPTABLE QString dumpNotificationListToString() const; + Q_SCRIPTABLE void dumpMemoryInfo() const; + Q_SCRIPTABLE QString dumpMemoryInfoToString() const; + + virtual void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + virtual void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); + virtual void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &destination); + virtual void itemRemoved(const Akonadi::Item &item); + void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + virtual void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags); + virtual void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source, const Akonadi::Collection &destination); + virtual void itemsRemoved(const Akonadi::Item::List &items); + virtual void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + virtual void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + virtual void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + virtual void collectionChanged(const Akonadi::Collection &collection); + virtual void collectionChanged(const Akonadi::Collection &collection, const QSet &changedAttributes); + virtual void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination); + virtual void collectionRemoved(const Akonadi::Collection &collection); + void collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + void collectionUnsubscribed(const Akonadi::Collection &collection); + + virtual void tagAdded(const Akonadi::Tag &tag); + virtual void tagChanged(const Akonadi::Tag &tag); + virtual void tagRemoved(const Akonadi::Tag &tag); + virtual void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags); + + virtual void relationAdded(const Akonadi::Relation &relation); + virtual void relationRemoved(const Akonadi::Relation &relation); + virtual void + itemsRelationsChanged(const Akonadi::Item::List &items, const Akonadi::Relation::List &addedRelations, const Akonadi::Relation::List &removedRelations); +}; + +} + diff --git a/src/agentbase/agentfactory.cpp b/src/agentbase/agentfactory.cpp new file mode 100644 index 0000000..deea17d --- /dev/null +++ b/src/agentbase/agentfactory.cpp @@ -0,0 +1,59 @@ +/* + This file is part of akonadiresources. + + SPDX-FileCopyrightText: 2006 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentfactory.h" +#include "servermanager.h" +#include "servermanager_p.h" + +#include +#include + +#include +#include + +QThreadStorage s_agentComponentDatas; + +using namespace Akonadi; + +class Akonadi::AgentFactoryBasePrivate +{ +public: + QString catalogName; +}; + +AgentFactoryBase::AgentFactoryBase(const char *catalogName, QObject *parent) + : QObject(parent) + , d(new AgentFactoryBasePrivate) +{ + d->catalogName = QString::fromLatin1(catalogName); + if (!KGlobal::hasMainComponent()) { + new KComponentData("AkonadiAgentServer", "libakonadi", KComponentData::RegisterAsMainComponent); + } + KLocalizedString::setApplicationDomain(catalogName); + + Internal::setClientType(Internal::Agent); + ServerManager::self(); // make sure it's created in the main thread +} + +AgentFactoryBase::~AgentFactoryBase() +{ + delete d; +} + +void AgentFactoryBase::createComponentData(const QString &identifier) const +{ + Q_ASSERT(!s_agentComponentDatas.hasLocalData()); + + if (QThread::currentThread() != QCoreApplication::instance()->thread()) { + s_agentComponentDatas.setLocalData( + new KComponentData(ServerManager::addNamespace(identifier).toLatin1(), d->catalogName.toLatin1(), KComponentData::SkipMainComponentRegistration)); + } else { + s_agentComponentDatas.setLocalData(new KComponentData(ServerManager::addNamespace(identifier).toLatin1(), d->catalogName.toLatin1())); + } +} diff --git a/src/agentbase/agentfactory.h b/src/agentbase/agentfactory.h new file mode 100644 index 0000000..9c0196d --- /dev/null +++ b/src/agentbase/agentfactory.h @@ -0,0 +1,90 @@ +/* + This file is part of akonadiresources. + + SPDX-FileCopyrightText: 2006 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentbase.h" +#include "akonadiagentbase_export.h" + +#include + +namespace Akonadi +{ +class AgentFactoryBasePrivate; + +/** + * @short A factory base class for in-process agents. + * + * @see AKONADI_AGENT_FACTORY() + * @internal + * @since 4.6 + */ +class AKONADIAGENTBASE_EXPORT AgentFactoryBase : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new agent factory. + * Executed in the main thread, performs KDE infrastructure setup. + * + * @param catalogName The translation catalog of this resource. + * @param parent The parent object. + */ + explicit AgentFactoryBase(const char *catalogName, QObject *parent = nullptr); + + ~AgentFactoryBase() override; + +public Q_SLOTS: + /** + * Creates a new agent instance with the given identifier. + */ + virtual QObject *createInstance(const QString &identifier) const = 0; + +protected: + void createComponentData(const QString &identifier) const; + +private: + AgentFactoryBasePrivate *const d; +}; + +/** + * @short A factory for in-process agents. + * + * @see AKONADI_AGENT_FACTORY() + * @internal + * @since 4.6 + */ +template class AgentFactory : public AgentFactoryBase +{ +public: + /** reimplemented */ + explicit AgentFactory(const char *catalogName, QObject *parent = nullptr) + : AgentFactoryBase(catalogName, parent) + { + } + + QObject *createInstance(const QString &identifier) const override + { + createComponentData(identifier); + T *instance = new T(identifier); + + // check if T also inherits AgentBase::Observer and + // if it does, automatically register it on itself + auto observer = dynamic_cast(instance); + if (observer != nullptr) { + instance->registerObserver(observer); + } + + return instance; + } +}; + +} + diff --git a/src/agentbase/agentsearchinterface.cpp b/src/agentbase/agentsearchinterface.cpp new file mode 100644 index 0000000..47b3903 --- /dev/null +++ b/src/agentbase/agentsearchinterface.cpp @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentsearchinterface.h" +#include "agentbase.h" +#include "agentsearchinterface_p.h" +#include "akonadiagentbase_debug.h" +#include "collection.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "private/imapset_p.h" +#include "searchadaptor.h" +#include "searchresultjob_p.h" +#include "servermanager.h" +#include + +using namespace Akonadi; + +AgentSearchInterfacePrivate::AgentSearchInterfacePrivate(AgentSearchInterface *qq) + : q(qq) +{ + new Akonadi__SearchAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Search"), this, QDBusConnection::ExportAdaptors); + + QTimer::singleShot(0, this, &AgentSearchInterfacePrivate::delayedInit); +} + +void AgentSearchInterfacePrivate::delayedInit() +{ + QDBusInterface iface(ServerManager::serviceName(ServerManager::Server), + QStringLiteral("/SearchManager"), + QStringLiteral("org.freedesktop.Akonadi.SearchManager"), + QDBusConnection::sessionBus(), + this); + QDBusMessage msg = iface.call(QStringLiteral("registerInstance"), dynamic_cast(q)->identifier()); + // TODO ? +} + +void AgentSearchInterfacePrivate::addSearch(const QString &query, const QString &queryLanguage, quint64 resultCollectionId) +{ + q->addSearch(query, queryLanguage, Collection(resultCollectionId)); +} + +void AgentSearchInterfacePrivate::removeSearch(quint64 resultCollectionId) +{ + q->removeSearch(Collection(resultCollectionId)); +} + +void AgentSearchInterfacePrivate::search(const QByteArray &searchId, const QString &query, quint64 collectionId) +{ + mSearchId = searchId; + mCollectionId = collectionId; + + auto fetchJob = new CollectionFetchJob(Collection(mCollectionId), CollectionFetchJob::Base, this); + fetchJob->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + fetchJob->setProperty("query", query); + connect(fetchJob, &KJob::finished, this, &AgentSearchInterfacePrivate::collectionReceived); +} + +void AgentSearchInterfacePrivate::collectionReceived(KJob *job) +{ + auto fetchJob = qobject_cast(job); + if (fetchJob->error()) { + qCCritical(AKONADIAGENTBASE_LOG) << fetchJob->errorString(); + new SearchResultJob(fetchJob->property("searchId").toByteArray(), Collection(mCollectionId), this); + return; + } + + if (fetchJob->collections().count() != 1) { + qCDebug(AKONADIAGENTBASE_LOG) << "Server requested search in invalid collection, or collection was removed in the meanwhile"; + // Tell server we are done + new SearchResultJob(fetchJob->property("searchId").toByteArray(), Collection(mCollectionId), this); + return; + } + + const Collection collection = fetchJob->collections().at(0); + q->search(fetchJob->property("query").toString(), collection); +} + +AgentSearchInterface::AgentSearchInterface() + : d(new AgentSearchInterfacePrivate(this)) +{ +} + +AgentSearchInterface::~AgentSearchInterface() +{ + delete d; +} + +void AgentSearchInterface::searchFinished(const QVector &result, ResultScope scope) +{ + if (scope == Akonadi::AgentSearchInterface::Rid) { + QVector rids; + rids.reserve(result.size()); + for (qint64 rid : result) { + rids << QByteArray::number(rid); + } + + searchFinished(rids); + return; + } + + auto resultJob = new SearchResultJob(d->mSearchId, Collection(d->mCollectionId), d); + resultJob->setResult(result); +} + +void AgentSearchInterface::searchFinished(const ImapSet &result, ResultScope scope) +{ + if (scope == Akonadi::AgentSearchInterface::Rid) { + QVector rids; + const ImapInterval::List lstInterval = result.intervals(); + for (const ImapInterval &interval : lstInterval) { + const int endInterval(interval.end()); + for (int i = interval.begin(); i <= endInterval; ++i) { + rids << QByteArray::number(i); + } + } + + searchFinished(rids); + return; + } + + auto resultJob = new SearchResultJob(d->mSearchId, Collection(d->mCollectionId), d); + resultJob->setResult(result); +} + +void AgentSearchInterface::searchFinished(const QVector &result) +{ + auto resultJob = new SearchResultJob(d->mSearchId, Collection(d->mCollectionId), d); + resultJob->setResult(result); +} + +#include "moc_agentsearchinterface_p.cpp" diff --git a/src/agentbase/agentsearchinterface.h b/src/agentbase/agentsearchinterface.h new file mode 100644 index 0000000..5a57fe1 --- /dev/null +++ b/src/agentbase/agentsearchinterface.h @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiagentbase_export.h" +#include + +namespace Akonadi +{ +class Collection; +class AgentSearchInterfacePrivate; +class ImapSet; + +/** + * @short An interface for agents (or resources) that support searching in their backend. + * + * Inherit from this additionally to Akonadi::AgentBase (or Akonadi::ResourceBase) + * and implement its two pure virtual methods. + * + * Make sure to add the @c Search capability to the agent desktop file. + * + * @since 4.5 + */ +class AKONADIAGENTBASE_EXPORT AgentSearchInterface +{ +public: + enum ResultScope { + Uid, + Rid, + }; + + /** + * Creates a new agent search interface. + */ + AgentSearchInterface(); + + /** + * Destroys the agent search interface. + */ + virtual ~AgentSearchInterface(); + + /** + * Adds a new search. + * + * @param query The query string, using the language specified in @p queryLanguage + * @param queryLanguage The query language used for @p query + * @param resultCollection The destination collection for the search results. It's a virtual + * collection, results can be added/removed using Akonadi::LinkJob and Akonadi::UnlinkJob respectively. + */ + virtual void addSearch(const QString &query, const QString &queryLanguage, const Akonadi::Collection &resultCollection) = 0; + + /** + * Removes a previously added search. + * @param resultCollection The result collection given in an previous addSearch() call. + * You do not need to take care of deleting results in there, the collection is just provided as a way to + * identify the search. + */ + virtual void removeSearch(const Akonadi::Collection &resultCollection) = 0; + + /** + * Perform a search on remote storage and return results using SearchResultJob. + * + * @since 4.13 + */ + virtual void search(const QString &query, const Collection &collection) = 0; + + void searchFinished(const QVector &result, ResultScope scope); + void searchFinished(const ImapSet &result, ResultScope scope); + void searchFinished(const QVector &result); + +private: + /// @cond PRIVATE + AgentSearchInterfacePrivate *const d; + /// @endcond +}; + +} + diff --git a/src/agentbase/agentsearchinterface_p.h b/src/agentbase/agentsearchinterface_p.h new file mode 100644 index 0000000..cd5afd8 --- /dev/null +++ b/src/agentbase/agentsearchinterface_p.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentsearchinterface.h" +#include "collection.h" + +#include + +class KJob; + +namespace Akonadi +{ +class AgentSearchInterfacePrivate : public QObject +{ + Q_OBJECT +public: + explicit AgentSearchInterfacePrivate(AgentSearchInterface *qq); + + void search(const QByteArray &searchId, const QString &query, quint64 collectionId); + void addSearch(const QString &query, const QString &queryLanguage, quint64 resultCollectionId); + void removeSearch(quint64 resultCollectionId); + + QByteArray mSearchId; + qint64 mCollectionId; + +private Q_SLOTS: + void delayedInit(); + void collectionReceived(KJob *job); + +private: + AgentSearchInterface *const q; +}; + +} + diff --git a/src/agentbase/preprocessorbase.cpp b/src/agentbase/preprocessorbase.cpp new file mode 100644 index 0000000..765308a --- /dev/null +++ b/src/agentbase/preprocessorbase.cpp @@ -0,0 +1,50 @@ +/****************************************************************************** + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#include "preprocessorbase.h" + +#include "preprocessorbase_p.h" + +#include "akonadiagentbase_debug.h" + +using namespace Akonadi; + +PreprocessorBase::PreprocessorBase(const QString &id) + : AgentBase(new PreprocessorBasePrivate(this), id) +{ +} + +PreprocessorBase::~PreprocessorBase() +{ +} + +void PreprocessorBase::finishProcessing(ProcessingResult result) +{ + Q_D(PreprocessorBase); + + Q_ASSERT_X(result != ProcessingDelayed, "PreprocessorBase::terminateProcessing", "You should never pass ProcessingDelayed to this function"); + Q_ASSERT_X(d->mInDelayedProcessing, "PreprocessorBase::terminateProcessing", "terminateProcessing() called while not in delayed processing mode"); + Q_UNUSED(result) + + d->mInDelayedProcessing = false; + Q_EMIT d->itemProcessed(d->mDelayedProcessingItemId); +} + +void PreprocessorBase::setFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(PreprocessorBase); + + d->mFetchScope = fetchScope; +} + +ItemFetchScope &PreprocessorBase::fetchScope() +{ + Q_D(PreprocessorBase); + + return d->mFetchScope; +} diff --git a/src/agentbase/preprocessorbase.h b/src/agentbase/preprocessorbase.h new file mode 100644 index 0000000..bbd05fa --- /dev/null +++ b/src/agentbase/preprocessorbase.h @@ -0,0 +1,173 @@ +/****************************************************************************** + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#pragma once + +#include "agentbase.h" +#include "akonadiagentbase_export.h" +#include "collection.h" +#include "item.h" + +namespace Akonadi +{ +class ItemFetchScope; + +class PreprocessorBasePrivate; + +/** + * @short The base class for all Akonadi preprocessor agents. + * + * This class should be used as a base class by all preprocessor agents + * since it encapsulates large parts of the protocol between + * preprocessor agent, agent manager and the Akonadi storage. + * + * Preprocessor agents are special agents that are informed about newly + * added items before any other agents. This allows them to do filtering + * on the items or any other task that shall be done before the new item + * is visible in the Akonadi storage system. + * + * The method all the preprocessors must implement is processItem(). + * + * @author Szymon Stefanek + * @since 4.4 + */ +class AKONADIAGENTBASE_EXPORT PreprocessorBase : public AgentBase +{ + Q_OBJECT + +public: + /** + * Describes the possible return values of the processItem() method. + */ + enum ProcessingResult { + /** + * Processing completed successfully for this item. + * The Akonadi server will push in a new item when it's available. + */ + ProcessingCompleted, + + /** + * Processing was delayed to a later stage. + * This must be returned when implementing asynchronous preprocessing. + * + * If this value is returned, finishProcessing() has to be called + * when processing is done. + */ + ProcessingDelayed, + + /** + * Processing for this item failed (and the failure is unrecoverable). + * The Akonadi server will push in a new item when it's available, + * after possibly logging the failure. + */ + ProcessingFailed, + + /** + * Processing for this item was refused. This is very + * similar to ProcessingFailed above but additionally remarks + * that the item that the Akonadi server pushed in wasn't + * meant for this Preprocessor. + * The Akonadi server will push in a new item when it's available, + * after possibly logging the failure and maybe taking some additional action. + */ + ProcessingRefused + }; + + /** + * This method must be implemented by every preprocessor subclass. + * @param item the item to process + * It must realize the preprocessing of the given @p item. + * + * The Akonadi server will push in for preprocessing any newly created item: + * it's your responsibility to decide if you want to process the item or not. + * + * The method should return ProcessingCompleted on success, ProcessingDelayed + * if processing is implemented asynchronously and + * ProcessingRefused or ProcessingFailed if the processing + * didn't complete. + * + * If your operation is asynchronous then you should also + * connect to the abortRequested() signal and handle it + * appropriately (as the server MAY abort your async job + * if it decides that it's taking too long). + */ + virtual ProcessingResult processItem(const Item &item) = 0; + + /** + * This method must be called if processing is implemented asynchronously. + * @param result the processing result + * You should call it when you have completed the processing + * or if an abortRequest() signal arrives (and in this case you + * will probably use ProcessingFailed as result). + * + * Valid values for @p result are ProcessingCompleted, + * PocessingRefused and ProcessingFailed. Passing any + * other value will lead to a runtime assertion. + */ + void finishProcessing(ProcessingResult result); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + +protected: + /** + * Creates a new preprocessor base agent. + * + * @param id The instance id of the preprocessor base agent. + */ + PreprocessorBase(const QString &id); + + /** + * Destroys the preprocessor base agent. + */ + ~PreprocessorBase() override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(PreprocessorBase) + /// @endcond + +}; // class PreprocessorBase + +} // namespace Akonadi + +#ifndef AKONADI_PREPROCESSOR_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi preprocessors. + */ +#define AKONADI_PREPROCESSOR_MAIN(preProcessorClass) \ + int main(int argc, char **argv) \ + { \ + return Akonadi::PreprocessorBase::init(argc, argv); \ + } +#endif //! AKONADI_RESOURCE_MAIN + diff --git a/src/agentbase/preprocessorbase_p.cpp b/src/agentbase/preprocessorbase_p.cpp new file mode 100644 index 0000000..23ab358 --- /dev/null +++ b/src/agentbase/preprocessorbase_p.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "preprocessorbase_p.h" + +#include "preprocessoradaptor.h" +#include "servermanager.h" +#include + +#include "akonadiagentbase_debug.h" +#include "itemfetchjob.h" + +using namespace Akonadi; + +PreprocessorBasePrivate::PreprocessorBasePrivate(PreprocessorBase *parent) + : AgentBasePrivate(parent) +{ + Q_Q(PreprocessorBase); + + new Akonadi__PreprocessorAdaptor(this); + + if (!QDBusConnection::sessionBus().registerObject(QStringLiteral("/Preprocessor"), this, QDBusConnection::ExportAdaptors)) { + Q_EMIT q->error(i18n("Unable to register object at dbus: %1", QDBusConnection::sessionBus().lastError().message())); + } +} + +void PreprocessorBasePrivate::delayedInit() +{ + if (!QDBusConnection::sessionBus().registerService(ServerManager::agentServiceName(ServerManager::Preprocessor, mId))) { + qCCritical(AKONADIAGENTBASE_LOG) << "Unable to register service at D-Bus: " << QDBusConnection::sessionBus().lastError().message(); + } + AgentBasePrivate::delayedInit(); +} + +void PreprocessorBasePrivate::beginProcessItem(qlonglong itemId, qlonglong collectionId, const QString &mimeType) +{ + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: about to process item " << itemId << " in collection " << collectionId << " with mimeType " << mimeType; + + auto fetchJob = new ItemFetchJob(Item(itemId), this); + fetchJob->setFetchScope(mFetchScope); + connect(fetchJob, &ItemFetchJob::result, this, &PreprocessorBasePrivate::itemFetched); +} + +void PreprocessorBasePrivate::itemFetched(KJob *job) +{ + Q_Q(PreprocessorBase); + + if (job->error()) { + Q_EMIT itemProcessed(PreprocessorBase::ProcessingFailed); + return; + } + + auto fetchJob = qobject_cast(job); + + if (fetchJob->items().isEmpty()) { + Q_EMIT itemProcessed(PreprocessorBase::ProcessingFailed); + return; + } + + const Item item = fetchJob->items().at(0); + + switch (q->processItem(item)) { + case PreprocessorBase::ProcessingFailed: + case PreprocessorBase::ProcessingRefused: + case PreprocessorBase::ProcessingCompleted: + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: item processed, emitting signal (" << item.id() << ")"; + + // TODO: Handle the different status codes appropriately + + Q_EMIT itemProcessed(item.id()); + + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: item processed, signal emitted (" << item.id() << ")"; + break; + case PreprocessorBase::ProcessingDelayed: + qCDebug(AKONADIAGENTBASE_LOG) << "PreprocessorBase: item processing delayed (" << item.id() << ")"; + + mInDelayedProcessing = true; + mDelayedProcessingItemId = item.id(); + break; + } +} diff --git a/src/agentbase/preprocessorbase_p.h b/src/agentbase/preprocessorbase_p.h new file mode 100644 index 0000000..afe4465 --- /dev/null +++ b/src/agentbase/preprocessorbase_p.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentbase_p.h" + +#include "itemfetchscope.h" +#include "preprocessorbase.h" + +class KJob; + +namespace Akonadi +{ +class PreprocessorBasePrivate : public AgentBasePrivate +{ + Q_OBJECT + +public: + explicit PreprocessorBasePrivate(PreprocessorBase *parent); + + void delayedInit() override; + + void beginProcessItem(qlonglong itemId, qlonglong collectionId, const QString &mimeType); + +Q_SIGNALS: + void itemProcessed(qlonglong id); + +private Q_SLOTS: + void itemFetched(KJob *job); + +public: + bool mInDelayedProcessing = false; + qlonglong mDelayedProcessingItemId = 0; + ItemFetchScope mFetchScope; + + Q_DECLARE_PUBLIC(PreprocessorBase) +}; + +} + diff --git a/src/agentbase/recursivemover.cpp b/src/agentbase/recursivemover.cpp new file mode 100644 index 0000000..94113d0 --- /dev/null +++ b/src/agentbase/recursivemover.cpp @@ -0,0 +1,214 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "recursivemover_p.h" + +using namespace Akonadi; + +RecursiveMover::RecursiveMover(AgentBasePrivate *parent) + : KCompositeJob(parent) + , m_agentBase(parent) + , m_currentAction(None) +{ +} + +void RecursiveMover::start() +{ + Q_ASSERT(receivers(SIGNAL(result(KJob *)))); + + auto job = new CollectionFetchJob(m_movedCollection, CollectionFetchJob::Recursive, this); + connect(job, &CollectionFetchJob::finished, this, &RecursiveMover::collectionListResult); + addSubjob(job); + ++m_runningJobs; +} + +void RecursiveMover::setCollection(const Collection &collection, const Collection &parentCollection) +{ + m_movedCollection = collection; + m_collections.insert(collection.id(), m_movedCollection); + m_collections.insert(parentCollection.id(), parentCollection); +} + +void RecursiveMover::collectionListResult(KJob *job) +{ + Q_ASSERT(m_pendingCollections.isEmpty()); + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + // build a parent -> children map for the following topological sorting + // while we are iterating anyway, also fill m_collections here + auto fetchJob = qobject_cast(job); + QHash colTree; + const Akonadi::Collection::List lstCol = fetchJob->collections(); + for (const Collection &col : lstCol) { + colTree[col.parentCollection().id()] << col; + m_collections.insert(col.id(), col); + } + + // topological sort; BFS traversal of the tree + m_pendingCollections.push_back(m_movedCollection); + QQueue toBeProcessed; + toBeProcessed.enqueue(m_movedCollection); + while (!toBeProcessed.isEmpty()) { + const Collection col = toBeProcessed.dequeue(); + const Collection::List children = colTree.value(col.id()); + if (children.isEmpty()) { + continue; + } + m_pendingCollections += children; + for (const Collection &child : children) { + toBeProcessed.enqueue(child); + } + } + + replayNextCollection(); +} + +void RecursiveMover::collectionFetchResult(KJob *job) +{ + Q_ASSERT(m_currentCollection.isValid()); + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + auto fetchJob = qobject_cast(job); + if (fetchJob->collections().size() == 1) { + m_currentCollection = fetchJob->collections().at(0); + m_currentCollection.setParentCollection(m_collections.value(m_currentCollection.parentCollection().id())); + m_collections.insert(m_currentCollection.id(), m_currentCollection); + } else { + // already deleted, move on + } + + if (!m_runningJobs && m_pendingReplay) { + replayNext(); + } +} + +void RecursiveMover::itemListResult(KJob *job) +{ + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + const Akonadi::Item::List lstItems = qobject_cast(job)->items(); + for (const Item &item : lstItems) { + if (item.remoteId().isEmpty()) { + m_pendingItems.push_back(item); + } + } + + if (!m_runningJobs && m_pendingReplay) { + replayNext(); + } +} + +void RecursiveMover::itemFetchResult(KJob *job) +{ + Q_ASSERT(m_currentAction == None); + --m_runningJobs; + + if (job->error()) { + return; // error handling is in the base class + } + + auto fetchJob = qobject_cast(job); + if (fetchJob->items().size() == 1) { + m_currentAction = AddItem; + m_agentBase->itemAdded(fetchJob->items().at(0), m_currentCollection); + } else { + // deleted since we started, skip + m_currentItem = Item(); + replayNextItem(); + } +} + +void RecursiveMover::replayNextCollection() +{ + if (!m_pendingCollections.isEmpty()) { + m_currentCollection = m_pendingCollections.takeFirst(); + auto job = new ItemFetchJob(m_currentCollection, this); + connect(job, &ItemFetchJob::result, this, &RecursiveMover::itemListResult); + addSubjob(job); + ++m_runningJobs; + + if (m_currentCollection.remoteId().isEmpty()) { + Q_ASSERT(m_currentAction == None); + m_currentAction = AddCollection; + m_agentBase->collectionAdded(m_currentCollection, m_collections.value(m_currentCollection.parentCollection().id())); + return; + } else { + // replayNextItem(); - but waiting for the fetch job to finish first + m_pendingReplay = true; + return; + } + } else { + // nothing left to do + emitResult(); + } +} + +void RecursiveMover::replayNextItem() +{ + Q_ASSERT(m_currentCollection.isValid()); + if (m_pendingItems.isEmpty()) { + replayNextCollection(); // all items processed here + return; + } else { + Q_ASSERT(m_currentAction == None); + m_currentItem = m_pendingItems.takeFirst(); + auto job = new ItemFetchJob(m_currentItem, this); + job->fetchScope().fetchFullPayload(); + connect(job, &ItemFetchJob::result, this, &RecursiveMover::itemFetchResult); + addSubjob(job); + ++m_runningJobs; + } +} + +void RecursiveMover::changeProcessed() +{ + Q_ASSERT(m_currentAction != None); + + if (m_currentAction == AddCollection) { + Q_ASSERT(m_currentCollection.isValid()); + auto job = new CollectionFetchJob(m_currentCollection, CollectionFetchJob::Base, this); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + connect(job, &CollectionFetchJob::result, this, &RecursiveMover::collectionFetchResult); + addSubjob(job); + ++m_runningJobs; + } + + m_currentAction = None; +} + +void RecursiveMover::replayNext() +{ + // wait for runnings jobs to finish before actually doing the replay + if (m_runningJobs) { + m_pendingReplay = true; + return; + } + + m_pendingReplay = false; + + if (m_currentCollection.isValid()) { + replayNextItem(); + } else { + replayNextCollection(); + } +} + +#include "moc_recursivemover_p.cpp" diff --git a/src/agentbase/recursivemover_p.h b/src/agentbase/recursivemover_p.h new file mode 100644 index 0000000..6c2d3ef --- /dev/null +++ b/src/agentbase/recursivemover_p.h @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentbase_p.h" +#include "collection.h" +#include "item.h" + +#include + +namespace Akonadi +{ +/** + * Helper class for expanding inter-resource collection moves inside ResourceBase. + * + * @note This is intentionally not an Akonadi::Job since we don't need autostarting + * here. + */ +class RecursiveMover : public KCompositeJob +{ + Q_OBJECT +public: + explicit RecursiveMover(AgentBasePrivate *parent); + + /// Set the collection that is actually moved. + void setCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parentCollection); + + void start() override; + + /// Call once the last replayed change has been processed. + void changeProcessed(); + +public Q_SLOTS: + /// Trigger the next change replay, will call emitResult() once everything has been replayed + void replayNext(); + +private: + void replayNextCollection(); + void replayNextItem(); + +private Q_SLOTS: + void collectionListResult(KJob *job); + void collectionFetchResult(KJob *job); + void itemListResult(KJob *job); + void itemFetchResult(KJob *job); + +private: + AgentBasePrivate *const m_agentBase; + Collection m_movedCollection; + /// sorted queue of collections still to be processed + Collection::List m_pendingCollections; + /// holds up-to-date full collection objects, used for e.g. having proper parent collections for collectionAdded + QHash m_collections; + Item::List m_pendingItems; + + Collection m_currentCollection; + Item m_currentItem; + + enum CurrentAction { None, AddCollection, AddItem } m_currentAction; + + int m_runningJobs = 0; + bool m_pendingReplay = false; +}; + +} + +Q_DECLARE_METATYPE(Akonadi::RecursiveMover *) + diff --git a/src/agentbase/resourcebase.cpp b/src/agentbase/resourcebase.cpp new file mode 100644 index 0000000..3cb4feb --- /dev/null +++ b/src/agentbase/resourcebase.cpp @@ -0,0 +1,1600 @@ +/* + SPDX-FileCopyrightText: 2006 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resourcebase.h" +#include "agentbase_p.h" + +#include "akonadifull-version.h" +#include "collectiondeletejob.h" +#include "collectionsync_p.h" +#include "relationsync.h" +#include "resourceadaptor.h" +#include "resourcescheduler_p.h" +#include "tagsync.h" +#include "tracerinterface.h" +#include + +#include "changerecorder.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "favoritecollectionattribute.h" +#include "invalidatecachejob_p.h" +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "itemmodifyjob_p.h" +#include "monitor_p.h" +#include "recursivemover_p.h" +#include "resourceselectjob_p.h" +#include "servermanager_p.h" +#include "session.h" +#include "specialcollectionattribute.h" +#include "tagmodifyjob.h" + +#include "akonadiagentbase_debug.h" + +#include +#include +#include + +#include +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace AkRanges; + +class Akonadi::ResourceBasePrivate : public AgentBasePrivate +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.dfaure") + +public: + explicit ResourceBasePrivate(ResourceBase *parent) + : AgentBasePrivate(parent) + , scheduler(nullptr) + , mItemSyncer(nullptr) + , mItemSyncFetchScope(nullptr) + , mItemTransactionMode(ItemSync::SingleTransaction) + , mItemMergeMode(ItemSync::RIDMerge) + , mCollectionSyncer(nullptr) + , mTagSyncer(nullptr) + , mRelationSyncer(nullptr) + , mHierarchicalRid(false) + , mUnemittedProgress(0) + , mAutomaticProgressReporting(true) + , mDisableAutomaticItemDeliveryDone(false) + , mItemSyncBatchSize(10) + , mCurrentCollectionFetchJob(nullptr) + , mScheduleAttributeSyncBeforeCollectionSync(false) + { + Internal::setClientType(Internal::Resource); + mStatusMessage = defaultReadyMessage(); + mProgressEmissionCompressor.setInterval(1000); + mProgressEmissionCompressor.setSingleShot(true); + // HACK: skip local changes of the EntityDisplayAttribute by default. Remove this for KDE5 and adjust resource implementations accordingly. + mKeepLocalCollectionChanges << "ENTITYDISPLAY"; + } + + ~ResourceBasePrivate() override + { + delete mItemSyncFetchScope; + } + + Q_DECLARE_PUBLIC(ResourceBase) + + void delayedInit() override + { + const QString serviceId = ServerManager::agentServiceName(ServerManager::Resource, mId); + if (!QDBusConnection::sessionBus().registerService(serviceId)) { + QString reason = QDBusConnection::sessionBus().lastError().message(); + if (reason.isEmpty()) { + reason = QStringLiteral("this service is probably running already."); + } + qCCritical(AKONADIAGENTBASE_LOG) << "Unable to register service" << serviceId << "at D-Bus:" << reason; + + if (QThread::currentThread() == QCoreApplication::instance()->thread()) { + QCoreApplication::instance()->exit(1); + } + } else { + AgentBasePrivate::delayedInit(); + } + } + + void changeProcessed() override + { + if (m_recursiveMover) { + m_recursiveMover->changeProcessed(); + QTimer::singleShot(0, m_recursiveMover.data(), &RecursiveMover::replayNext); + return; + } + + mChangeRecorder->changeProcessed(); + if (!mChangeRecorder->isEmpty()) { + scheduler->scheduleChangeReplay(); + } + scheduler->taskDone(); + } + + void slotAbortRequested(); + + void slotDeliveryDone(KJob *job); + void slotCollectionSyncDone(KJob *job); + void slotLocalListDone(KJob *job); + void slotSynchronizeCollection(const Collection &col); + void slotItemRetrievalCollectionFetchDone(KJob *job); + void slotCollectionListDone(KJob *job); + void slotSynchronizeCollectionAttributes(const Collection &col); + void slotCollectionListForAttributesDone(KJob *job); + void slotCollectionAttributesSyncDone(KJob *job); + void slotSynchronizeTags(); + void slotSynchronizeRelations(); + void slotAttributeRetrievalCollectionFetchDone(KJob *job); + + void slotItemSyncDone(KJob *job); + + void slotPercent(KJob *job, quint64 percent); + void slotDelayedEmitProgress(); + void slotDeleteResourceCollection(); + void slotDeleteResourceCollectionDone(KJob *job); + void slotCollectionDeletionDone(KJob *job); + + void slotInvalidateCache(const Akonadi::Collection &collection); + + void slotPrepareItemRetrieval(const Akonadi::Item &item); + void slotPrepareItemRetrievalResult(KJob *job); + + void slotPrepareItemsRetrieval(const QVector &item); + void slotPrepareItemsRetrievalResult(KJob *job); + + void changeCommittedResult(KJob *job); + + void slotRecursiveMoveReplay(RecursiveMover *mover); + void slotRecursiveMoveReplayResult(KJob *job); + + void slotTagSyncDone(KJob *job); + void slotRelationSyncDone(KJob *job); + + void slotSessionReconnected() + { + Q_Q(ResourceBase); + + new ResourceSelectJob(q->identifier()); + } + + void createItemSyncInstanceIfMissing() + { + Q_Q(ResourceBase); + Q_ASSERT_X(scheduler->currentTask().type == ResourceScheduler::SyncCollection, + "createItemSyncInstance", + "Calling items retrieval methods although no item retrieval is in progress"); + if (!mItemSyncer) { + mItemSyncer = new ItemSync(q->currentCollection()); + mItemSyncer->setTransactionMode(mItemTransactionMode); + mItemSyncer->setBatchSize(mItemSyncBatchSize); + mItemSyncer->setMergeMode(mItemMergeMode); + if (mItemSyncFetchScope) { + mItemSyncer->setFetchScope(*mItemSyncFetchScope); + } + mItemSyncer->setDisableAutomaticDeliveryDone(mDisableAutomaticItemDeliveryDone); + mItemSyncer->setProperty("collection", QVariant::fromValue(q->currentCollection())); + connect(mItemSyncer, &KJob::percentChanged, this, + &ResourceBasePrivate::slotPercent); // NOLINT(google-runtime-int): ulong comes from KJob + + connect(mItemSyncer, &KJob::result, this, &ResourceBasePrivate::slotItemSyncDone); + connect(mItemSyncer, &ItemSync::readyForNextBatch, q, &ResourceBase::retrieveNextItemSyncBatch); + } + Q_ASSERT(mItemSyncer); + } + +public Q_SLOTS: + // Dump the state of the scheduler + Q_SCRIPTABLE QString dumpToString() const + { + Q_Q(const ResourceBase); + return scheduler->dumpToString() + QLatin1Char('\n') + q->dumpResourceToString(); + } + + Q_SCRIPTABLE void dump() + { + scheduler->dump(); + } + + Q_SCRIPTABLE void clear() + { + scheduler->clear(); + } + +protected Q_SLOTS: + // reimplementations from AgentbBasePrivate, containing sanity checks that only apply to resources + // such as making sure that RIDs are present as well as translations of cross-resource moves + // TODO: we could possibly add recovery code for no-RID notifications by re-enquing those to the change recorder + // as the corresponding Add notifications, although that contains a risk of endless fail/retry loops + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override + { + if (collection.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::itemAdded(item, collection); + } + + void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers) override + { + if (item.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::itemChanged(item, partIdentifiers); + } + + void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) override + { + if (addedFlags.isEmpty() && removedFlags.isEmpty()) { + changeProcessed(); + return; + } + + const Item::List validItems = filterValidItems(items); + if (validItems.isEmpty()) { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsFlagsChanged(validItems, addedFlags, removedFlags); + } + + void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags) override + { + if (addedTags.isEmpty() && removedTags.isEmpty()) { + changeProcessed(); + return; + } + + const Item::List validItems = filterValidItems(items); + if (validItems.isEmpty()) { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsTagsChanged(validItems, addedTags, removedTags); + } + + // TODO move the move translation code from AgentBasePrivate here, it's wrong for agents + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &destination) override + { + if (item.remoteId().isEmpty() || destination.remoteId().isEmpty() || destination == source) { + changeProcessed(); + return; + } + AgentBasePrivate::itemMoved(item, source, destination); + } + + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source, const Akonadi::Collection &destination) override + { + if (destination.remoteId().isEmpty() || destination == source) { + changeProcessed(); + return; + } + + const Item::List validItems = filterValidItems(items); + if (validItems.isEmpty()) { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsMoved(validItems, source, destination); + } + + void itemRemoved(const Akonadi::Item &item) override + { + if (item.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::itemRemoved(item); + } + + void itemsRemoved(const Akonadi::Item::List &items) override + { + const Item::List validItems = filterValidItems(items); + if (validItems.isEmpty()) { + changeProcessed(); + return; + } + + AgentBasePrivate::itemsRemoved(validItems); + } + + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override + { + if (parent.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::collectionAdded(collection, parent); + } + + void collectionChanged(const Akonadi::Collection &collection) override + { + if (collection.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::collectionChanged(collection); + } + + void collectionChanged(const Akonadi::Collection &collection, const QSet &partIdentifiers) override + { + if (collection.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::collectionChanged(collection, partIdentifiers); + } + + void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination) override + { + // unknown destination or source == destination means we can't do/don't have to do anything + if (destination.remoteId().isEmpty() || source == destination) { + changeProcessed(); + return; + } + + // inter-resource moves, requires we know which resources the source and destination are in though + if (!source.resource().isEmpty() && !destination.resource().isEmpty() && source.resource() != destination.resource()) { + if (source.resource() == q_ptr->identifier()) { // moved away from us + AgentBasePrivate::collectionRemoved(collection); + } else if (destination.resource() == q_ptr->identifier()) { // moved to us + scheduler->taskDone(); // stop change replay for now + auto mover = new RecursiveMover(this); + mover->setCollection(collection, destination); + scheduler->scheduleMoveReplay(collection, mover); + } + return; + } + + // intra-resource move, requires the moved collection to have a valid id though + if (collection.remoteId().isEmpty()) { + changeProcessed(); + return; + } + + // intra-resource move, ie. something we can handle internally + AgentBasePrivate::collectionMoved(collection, source, destination); + } + + void collectionRemoved(const Akonadi::Collection &collection) override + { + if (collection.remoteId().isEmpty()) { + changeProcessed(); + return; + } + AgentBasePrivate::collectionRemoved(collection); + } + + void tagAdded(const Akonadi::Tag &tag) override + { + if (!tag.isValid()) { + changeProcessed(); + return; + } + + AgentBasePrivate::tagAdded(tag); + } + + void tagChanged(const Akonadi::Tag &tag) override + { + if (tag.remoteId().isEmpty()) { + changeProcessed(); + return; + } + + AgentBasePrivate::tagChanged(tag); + } + + void tagRemoved(const Akonadi::Tag &tag) override + { + if (tag.remoteId().isEmpty()) { + changeProcessed(); + return; + } + + AgentBasePrivate::tagRemoved(tag); + } + +private: + static Item::List filterValidItems(Item::List items) + { + items.erase(std::remove_if(items.begin(), + items.end(), + [](const auto &item) { + return item.remoteId().isEmpty(); + }), + items.end()); + return items; + } + +public: + // synchronize states + Collection currentCollection; + + ResourceScheduler *scheduler = nullptr; + ItemSync *mItemSyncer = nullptr; + ItemFetchScope *mItemSyncFetchScope = nullptr; + ItemSync::TransactionMode mItemTransactionMode; + ItemSync::MergeMode mItemMergeMode; + CollectionSync *mCollectionSyncer = nullptr; + TagSync *mTagSyncer = nullptr; + RelationSync *mRelationSyncer = nullptr; + bool mHierarchicalRid; + QTimer mProgressEmissionCompressor; + int mUnemittedProgress; + QMap mUnemittedAdvancedStatus; + bool mAutomaticProgressReporting; + bool mDisableAutomaticItemDeliveryDone; + QPointer m_recursiveMover; + int mItemSyncBatchSize; + QSet mKeepLocalCollectionChanges; + KJob *mCurrentCollectionFetchJob = nullptr; + bool mScheduleAttributeSyncBeforeCollectionSync; +}; + +ResourceBase::ResourceBase(const QString &id) + : AgentBase(new ResourceBasePrivate(this), id) +{ + Q_D(ResourceBase); + + qDBusRegisterMetaType(); + + new Akonadi__ResourceAdaptor(this); + + d->scheduler = new ResourceScheduler(this); + + d->mChangeRecorder->setChangeRecordingEnabled(true); + d->mChangeRecorder->setCollectionMoveTranslationEnabled(false); // we deal with this ourselves + connect(d->mChangeRecorder, &ChangeRecorder::changesAdded, d->scheduler, &ResourceScheduler::scheduleChangeReplay); + + d->mChangeRecorder->setResourceMonitored(d->mId.toLatin1()); + d->mChangeRecorder->fetchCollection(true); + + connect(d->scheduler, &ResourceScheduler::executeFullSync, this, &ResourceBase::retrieveCollections); + connect(d->scheduler, &ResourceScheduler::executeCollectionTreeSync, this, &ResourceBase::retrieveCollections); + connect(d->scheduler, &ResourceScheduler::executeCollectionSync, d, &ResourceBasePrivate::slotSynchronizeCollection); + connect(d->scheduler, &ResourceScheduler::executeCollectionAttributesSync, d, &ResourceBasePrivate::slotSynchronizeCollectionAttributes); + connect(d->scheduler, &ResourceScheduler::executeTagSync, d, &ResourceBasePrivate::slotSynchronizeTags); + connect(d->scheduler, &ResourceScheduler::executeRelationSync, d, &ResourceBasePrivate::slotSynchronizeRelations); + connect(d->scheduler, &ResourceScheduler::executeItemFetch, d, &ResourceBasePrivate::slotPrepareItemRetrieval); + connect(d->scheduler, &ResourceScheduler::executeItemsFetch, d, &ResourceBasePrivate::slotPrepareItemsRetrieval); + connect(d->scheduler, &ResourceScheduler::executeResourceCollectionDeletion, d, &ResourceBasePrivate::slotDeleteResourceCollection); + connect(d->scheduler, &ResourceScheduler::executeCacheInvalidation, d, &ResourceBasePrivate::slotInvalidateCache); + connect(d->scheduler, &ResourceScheduler::status, this, qOverload(&ResourceBase::status)); + connect(d->scheduler, &ResourceScheduler::executeChangeReplay, d->mChangeRecorder, &ChangeRecorder::replayNext); + connect(d->scheduler, &ResourceScheduler::executeRecursiveMoveReplay, d, &ResourceBasePrivate::slotRecursiveMoveReplay); + connect(d->scheduler, &ResourceScheduler::fullSyncComplete, this, &ResourceBase::synchronized); + connect(d->scheduler, &ResourceScheduler::collectionTreeSyncComplete, this, &ResourceBase::collectionTreeSynchronized); + connect(d->mChangeRecorder, &ChangeRecorder::nothingToReplay, d->scheduler, &ResourceScheduler::taskDone); + connect(d->mChangeRecorder, &Monitor::collectionRemoved, d->scheduler, &ResourceScheduler::collectionRemoved); + connect(this, &ResourceBase::abortRequested, d, &ResourceBasePrivate::slotAbortRequested); + connect(this, &ResourceBase::synchronized, d->scheduler, &ResourceScheduler::taskDone); + connect(this, &ResourceBase::collectionTreeSynchronized, d->scheduler, &ResourceScheduler::taskDone); + connect(this, &AgentBase::agentNameChanged, this, &ResourceBase::nameChanged); + connect(&d->mProgressEmissionCompressor, &QTimer::timeout, d, &ResourceBasePrivate::slotDelayedEmitProgress); + + d->scheduler->setOnline(d->mOnline); + if (!d->mChangeRecorder->isEmpty()) { + d->scheduler->scheduleChangeReplay(); + } + + new ResourceSelectJob(identifier()); + + connect(d->mChangeRecorder->session(), &Session::reconnected, d, &ResourceBasePrivate::slotSessionReconnected); +} + +ResourceBase::~ResourceBase() = default; + +void ResourceBase::synchronize() +{ + d_func()->scheduler->scheduleFullSync(); +} + +void ResourceBase::setName(const QString &name) +{ + AgentBase::setAgentName(name); +} + +QString ResourceBase::name() const +{ + return AgentBase::agentName(); +} + +QString ResourceBase::parseArguments(int argc, char **argv) +{ + Q_UNUSED(argc) + + QCommandLineOption identifierOption(QStringLiteral("identifier"), i18nc("@label command line option", "Resource identifier"), QStringLiteral("argument")); + QCommandLineParser parser; + parser.addOption(identifierOption); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(*qApp); + parser.setApplicationDescription(i18n("Akonadi Resource")); + + if (!parser.isSet(identifierOption)) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier argument missing"; + exit(1); + } + + const QString identifier = parser.value(identifierOption); + + if (identifier.isEmpty()) { + qCDebug(AKONADIAGENTBASE_LOG) << "Identifier is empty"; + exit(1); + } + + QCoreApplication::setApplicationName(ServerManager::addNamespace(identifier)); + QCoreApplication::setApplicationVersion(QStringLiteral(AKONADI_FULL_VERSION)); + + const QFileInfo fi(QString::fromLocal8Bit(argv[0])); + // strip off full path and possible .exe suffix + const QString catalog = fi.baseName(); + + auto translator = new QTranslator(qApp); + translator->load(catalog); + QCoreApplication::installTranslator(translator); + + return identifier; +} + +int ResourceBase::init(ResourceBase &r) +{ + KLocalizedString::setApplicationDomain("libakonadi5"); + KAboutData::setApplicationData(r.aboutData()); + return qApp->exec(); +} + +void ResourceBasePrivate::slotAbortRequested() +{ + Q_Q(ResourceBase); + + scheduler->cancelQueues(); + q->abortActivity(); +} + +void ResourceBase::itemRetrieved(const Item &item) +{ + Q_D(ResourceBase); + Q_ASSERT(d->scheduler->currentTask().type == ResourceScheduler::FetchItem); + if (!item.isValid()) { + d->scheduler->itemFetchDone(i18nc("@info", "Invalid item retrieved")); + return; + } + + const QSet requestedParts = d->scheduler->currentTask().itemParts; + for (const QByteArray &part : requestedParts) { + if (!item.loadedPayloadParts().contains(part)) { + qCWarning(AKONADIAGENTBASE_LOG) << "Item does not provide part" << part; + } + } + + auto job = new ItemModifyJob(item); + job->d_func()->setSilent(true); + // FIXME: remove once the item with which we call retrieveItem() has a revision number + job->disableRevisionCheck(); + connect(job, &KJob::result, d, &ResourceBasePrivate::slotDeliveryDone); +} + +void ResourceBasePrivate::slotDeliveryDone(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT(scheduler->currentTask().type == ResourceScheduler::FetchItem); + if (job->error()) { + Q_EMIT q->error(i18nc("@info", "Error while creating item: %1", job->errorString())); + } + scheduler->itemFetchDone(QString()); +} + +void ResourceBase::collectionAttributesRetrieved(const Collection &collection) +{ + Q_D(ResourceBase); + Q_ASSERT(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionAttributes); + if (!collection.isValid()) { + Q_EMIT attributesSynchronized(d->scheduler->currentTask().collection.id()); + d->scheduler->taskDone(); + return; + } + + auto job = new CollectionModifyJob(collection); + connect(job, &KJob::result, d, &ResourceBasePrivate::slotCollectionAttributesSyncDone); +} + +void ResourceBasePrivate::slotCollectionAttributesSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT(scheduler->currentTask().type == ResourceScheduler::SyncCollectionAttributes); + if (job->error()) { + Q_EMIT q->error(i18nc("@info", "Error while updating collection: %1", job->errorString())); + } + Q_EMIT q->attributesSynchronized(scheduler->currentTask().collection.id()); + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotDeleteResourceCollection() +{ + Q_Q(ResourceBase); + + auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel); + job->fetchScope().setResource(q->identifier()); + connect(job, &KJob::result, this, &ResourceBasePrivate::slotDeleteResourceCollectionDone); +} + +void ResourceBasePrivate::slotDeleteResourceCollectionDone(KJob *job) +{ + Q_Q(ResourceBase); + if (job->error()) { + Q_EMIT q->error(job->errorString()); + scheduler->taskDone(); + } else { + const auto fetchJob = static_cast(job); + + if (!fetchJob->collections().isEmpty()) { + auto job = new CollectionDeleteJob(fetchJob->collections().at(0)); + connect(job, &KJob::result, this, &ResourceBasePrivate::slotCollectionDeletionDone); + } else { + // there is no resource collection, so just ignore the request + scheduler->taskDone(); + } + } +} + +void ResourceBasePrivate::slotCollectionDeletionDone(KJob *job) +{ + Q_Q(ResourceBase); + if (job->error()) { + Q_EMIT q->error(job->errorString()); + } + + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotInvalidateCache(const Akonadi::Collection &collection) +{ + Q_Q(ResourceBase); + auto job = new InvalidateCacheJob(collection, q); + connect(job, &KJob::result, scheduler, &ResourceScheduler::taskDone); +} + +void ResourceBase::changeCommitted(const Item &item) +{ + changesCommitted(Item::List() << item); +} + +void ResourceBase::changesCommitted(const Item::List &items) +{ + Q_D(ResourceBase); + auto transaction = new TransactionSequence(this); + connect(transaction, &KJob::finished, d, &ResourceBasePrivate::changeCommittedResult); + + // Modify the items one-by-one, because STORE does not support mass RID change + for (const Item &item : items) { + auto job = new ItemModifyJob(item, transaction); + job->d_func()->setClean(); + job->disableRevisionCheck(); // TODO: remove, but where/how do we handle the error? + job->setIgnorePayload(true); // we only want to reset the dirty flag and update the remote id + } +} + +void ResourceBase::changeCommitted(const Collection &collection) +{ + Q_D(ResourceBase); + auto job = new CollectionModifyJob(collection); + connect(job, &KJob::result, d, &ResourceBasePrivate::changeCommittedResult); +} + +void ResourceBasePrivate::changeCommittedResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADIAGENTBASE_LOG) << job->errorText(); + } + + Q_Q(ResourceBase); + if (qobject_cast(job)) { + if (job->error()) { + Q_EMIT q->error(i18nc("@info", "Updating local collection failed: %1.", job->errorText())); + } + mChangeRecorder->d_ptr->invalidateCache(static_cast(job)->collection()); + } else { + if (job->error()) { + Q_EMIT q->error(i18nc("@info", "Updating local items failed: %1.", job->errorText())); + } + // Item and tag cache is invalidated by modify job + } + + changeProcessed(); +} + +void ResourceBase::changeCommitted(const Tag &tag) +{ + Q_D(ResourceBase); + auto job = new TagModifyJob(tag); + connect(job, &KJob::result, d, &ResourceBasePrivate::changeCommittedResult); +} + +void ResourceBase::requestItemDelivery(const QVector &uids, const QByteArrayList &parts) +{ + Q_D(ResourceBase); + if (!isOnline()) { + const QString errorMsg = i18nc("@info", "Cannot fetch item in offline mode."); + sendErrorReply(QDBusError::Failed, errorMsg); + Q_EMIT error(errorMsg); + return; + } + + setDelayedReply(true); + + const auto items = uids | Views::transform([](const auto uid) { + return Item{uid}; + }) + | Actions::toQVector; + d->scheduler->scheduleItemsFetch(items, QSet::fromList(parts), message()); +} + +void ResourceBase::collectionsRetrieved(const Collection::List &collections) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::collectionsRetrieved()", + "Calling collectionsRetrieved() although no collection retrieval is in progress"); + if (!d->mCollectionSyncer) { + d->mCollectionSyncer = new CollectionSync(identifier()); + d->mCollectionSyncer->setHierarchicalRemoteIds(d->mHierarchicalRid); + d->mCollectionSyncer->setKeepLocalChanges(d->mKeepLocalCollectionChanges); + connect(d->mCollectionSyncer, &KJob::percentChanged, d, + &ResourceBasePrivate::slotPercent); // NOLINT(google-runtime-int): ulong comes from KJob + connect(d->mCollectionSyncer, &KJob::result, d, &ResourceBasePrivate::slotCollectionSyncDone); + } + d->mCollectionSyncer->setRemoteCollections(collections); +} + +void ResourceBase::collectionsRetrievedIncremental(const Collection::List &changedCollections, const Collection::List &removedCollections) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::collectionsRetrievedIncremental()", + "Calling collectionsRetrievedIncremental() although no collection retrieval is in progress"); + if (!d->mCollectionSyncer) { + d->mCollectionSyncer = new CollectionSync(identifier()); + d->mCollectionSyncer->setHierarchicalRemoteIds(d->mHierarchicalRid); + d->mCollectionSyncer->setKeepLocalChanges(d->mKeepLocalCollectionChanges); + connect(d->mCollectionSyncer, &KJob::percentChanged, d, + &ResourceBasePrivate::slotPercent); // NOLINT(google-runtime-int): ulong comes from KJob + connect(d->mCollectionSyncer, &KJob::result, d, &ResourceBasePrivate::slotCollectionSyncDone); + } + d->mCollectionSyncer->setRemoteCollections(changedCollections, removedCollections); +} + +void ResourceBase::setCollectionStreamingEnabled(bool enable) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::setCollectionStreamingEnabled()", + "Calling setCollectionStreamingEnabled() although no collection retrieval is in progress"); + if (!d->mCollectionSyncer) { + d->mCollectionSyncer = new CollectionSync(identifier()); + d->mCollectionSyncer->setHierarchicalRemoteIds(d->mHierarchicalRid); + connect(d->mCollectionSyncer, &KJob::percentChanged, d, + &ResourceBasePrivate::slotPercent); // NOLINT(google-runtime-int): ulong comes from KJob + connect(d->mCollectionSyncer, &KJob::result, d, &ResourceBasePrivate::slotCollectionSyncDone); + } + d->mCollectionSyncer->setStreamingEnabled(enable); +} + +void ResourceBase::collectionsRetrievalDone() +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree || d->scheduler->currentTask().type == ResourceScheduler::SyncAll, + "ResourceBase::collectionsRetrievalDone()", + "Calling collectionsRetrievalDone() although no collection retrieval is in progress"); + // streaming enabled, so finalize the sync + if (d->mCollectionSyncer) { + d->mCollectionSyncer->retrievalDone(); + } else { + // user did the sync himself, we are done now + // FIXME: we need the same special case for SyncAll as in slotCollectionSyncDone here! + d->scheduler->taskDone(); + } +} + +void ResourceBase::setKeepLocalCollectionChanges(const QSet &parts) +{ + Q_D(ResourceBase); + d->mKeepLocalCollectionChanges = parts; +} + +void ResourceBasePrivate::slotCollectionSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + mCollectionSyncer = nullptr; + if (job->error()) { + if (job->error() != Job::UserCanceled) { + Q_EMIT q->error(job->errorString()); + } + } else { + if (scheduler->currentTask().type == ResourceScheduler::SyncAll) { + auto list = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); + list->setFetchScope(q->changeRecorder()->collectionFetchScope()); + list->fetchScope().fetchAttribute(); + list->fetchScope().fetchAttribute(); + list->fetchScope().setResource(mId); + list->fetchScope().setListFilter(CollectionFetchScope::Sync); + connect(list, &KJob::result, this, &ResourceBasePrivate::slotLocalListDone); + return; + } else if (scheduler->currentTask().type == ResourceScheduler::SyncCollectionTree) { + scheduler->scheduleCollectionTreeSyncCompletion(); + } + } + scheduler->taskDone(); +} + +namespace +{ +bool sortCollectionsForSync(const Collection &l, const Collection &r) +{ + const auto lType = l.hasAttribute() ? l.attribute()->collectionType() : QByteArray(); + const bool lInbox = (lType == "inbox") || (l.remoteId().midRef(1).compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0); + const bool lFav = l.hasAttribute(); + + const auto rType = r.hasAttribute() ? r.attribute()->collectionType() : QByteArray(); + const bool rInbox = (rType == "inbox") || (r.remoteId().midRef(1).compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0); + const bool rFav = r.hasAttribute(); + + // inbox is always first + if (lInbox) { + return true; + } else if (rInbox) { + return false; + } + + // favorites right after inbox + if (lFav) { + return !rInbox; + } else if (rFav) { + return lInbox; + } + + // trash is always last (unless it's favorite) + if (lType == "trash") { + return false; + } else if (rType == "trash") { + return true; + } + + // Fallback to sorting by id + return l.id() < r.id(); +} + +} // namespace + +void ResourceBasePrivate::slotLocalListDone(KJob *job) +{ + Q_Q(ResourceBase); + if (job->error()) { + Q_EMIT q->error(job->errorString()); + } else { + Collection::List cols = static_cast(job)->collections(); + std::sort(cols.begin(), cols.end(), sortCollectionsForSync); + for (const Collection &col : std::as_const(cols)) { + scheduler->scheduleSync(col); + } + scheduler->scheduleFullSyncCompletion(); + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotSynchronizeCollection(const Collection &col) +{ + Q_Q(ResourceBase); + currentCollection = col; + // This can happen due to FetchHelper::triggerOnDemandFetch() in the akonadi server (not an error). + if (!col.remoteId().isEmpty()) { + // check if this collection actually can contain anything + QStringList contentTypes = currentCollection.contentMimeTypes(); + contentTypes.removeAll(Collection::mimeType()); + contentTypes.removeAll(Collection::virtualMimeType()); + if (!contentTypes.isEmpty() || col.isVirtual()) { + if (mAutomaticProgressReporting) { + Q_EMIT q->status(AgentBase::Running, i18nc("@info:status", "Syncing folder '%1'", currentCollection.displayName())); + } + + qCDebug(AKONADIAGENTBASE_LOG) << "Preparing collection sync of collection" << currentCollection.id() << currentCollection.displayName(); + auto fetchJob = new Akonadi::CollectionFetchJob(col, CollectionFetchJob::Base, this); + fetchJob->setFetchScope(q->changeRecorder()->collectionFetchScope()); + connect(fetchJob, &KJob::result, this, &ResourceBasePrivate::slotItemRetrievalCollectionFetchDone); + mCurrentCollectionFetchJob = fetchJob; + return; + } + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotItemRetrievalCollectionFetchDone(KJob *job) +{ + Q_Q(ResourceBase); + mCurrentCollectionFetchJob = nullptr; + if (job->error()) { + qCWarning(AKONADIAGENTBASE_LOG) << "Failed to retrieve collection for sync: " << job->errorString(); + q->cancelTask(i18n("Failed to retrieve collection for sync.")); + return; + } + auto fetchJob = static_cast(job); + const Collection::List collections = fetchJob->collections(); + if (collections.isEmpty()) { + qCWarning(AKONADIAGENTBASE_LOG) << "The fetch job returned empty collection set. This is unexpected."; + q->cancelTask(i18n("Failed to retrieve collection for sync.")); + return; + } + q->retrieveItems(collections.at(0)); +} + +int ResourceBase::itemSyncBatchSize() const +{ + Q_D(const ResourceBase); + return d->mItemSyncBatchSize; +} + +void ResourceBase::setItemSyncBatchSize(int batchSize) +{ + Q_D(ResourceBase); + d->mItemSyncBatchSize = batchSize; +} + +void ResourceBase::setScheduleAttributeSyncBeforeItemSync(bool enable) +{ + Q_D(ResourceBase); + d->mScheduleAttributeSyncBeforeCollectionSync = enable; +} + +void ResourceBasePrivate::slotSynchronizeCollectionAttributes(const Collection &col) +{ + Q_Q(ResourceBase); + auto fetchJob = new Akonadi::CollectionFetchJob(col, CollectionFetchJob::Base, this); + fetchJob->setFetchScope(q->changeRecorder()->collectionFetchScope()); + connect(fetchJob, &KJob::result, this, &ResourceBasePrivate::slotAttributeRetrievalCollectionFetchDone); + Q_ASSERT(!mCurrentCollectionFetchJob); + mCurrentCollectionFetchJob = fetchJob; +} + +void ResourceBasePrivate::slotAttributeRetrievalCollectionFetchDone(KJob *job) +{ + mCurrentCollectionFetchJob = nullptr; + Q_Q(ResourceBase); + if (job->error()) { + qCWarning(AKONADIAGENTBASE_LOG) << "Failed to retrieve collection for attribute sync: " << job->errorString(); + q->cancelTask(i18n("Failed to retrieve collection for attribute sync.")); + return; + } + auto fetchJob = static_cast(job); + // FIXME: Why not call q-> directly? + QMetaObject::invokeMethod(q, "retrieveCollectionAttributes", Q_ARG(Akonadi::Collection, fetchJob->collections().at(0))); +} + +void ResourceBasePrivate::slotSynchronizeTags() +{ + Q_Q(ResourceBase); + QMetaObject::invokeMethod(this, [q] { + q->retrieveTags(); + }); +} + +void ResourceBasePrivate::slotSynchronizeRelations() +{ + Q_Q(ResourceBase); + QMetaObject::invokeMethod(this, [q] { + q->retrieveRelations(); + }); +} + +void ResourceBasePrivate::slotPrepareItemRetrieval(const Item &item) +{ + Q_Q(ResourceBase); + auto fetch = new ItemFetchJob(item, this); + // we always need at least parent so we can use ItemCreateJob to merge + fetch->fetchScope().setAncestorRetrieval(qMax(ItemFetchScope::Parent, q->changeRecorder()->itemFetchScope().ancestorRetrieval())); + fetch->fetchScope().setCacheOnly(true); + fetch->fetchScope().setFetchRemoteIdentification(true); + + // copy list of attributes to fetch + const QSet attributes = q->changeRecorder()->itemFetchScope().attributes(); + for (const auto &attribute : attributes) { + fetch->fetchScope().fetchAttribute(attribute); + } + + connect(fetch, &KJob::result, this, &ResourceBasePrivate::slotPrepareItemRetrievalResult); +} + +void ResourceBasePrivate::slotPrepareItemRetrievalResult(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT_X(scheduler->currentTask().type == ResourceScheduler::FetchItem, + "ResourceBasePrivate::slotPrepareItemRetrievalResult()", + "Preparing item retrieval although no item retrieval is in progress"); + if (job->error()) { + q->cancelTask(job->errorText()); + return; + } + auto fetch = qobject_cast(job); + if (fetch->items().count() != 1) { + q->cancelTask(i18n("The requested item no longer exists")); + return; + } + const QSet parts = scheduler->currentTask().itemParts; + if (!q->retrieveItem(fetch->items().at(0), parts)) { + q->cancelTask(); + } +} + +void ResourceBasePrivate::slotPrepareItemsRetrieval(const QVector &items) +{ + Q_Q(ResourceBase); + auto fetch = new ItemFetchJob(items, this); + // we always need at least parent so we can use ItemCreateJob to merge + fetch->fetchScope().setAncestorRetrieval(qMax(ItemFetchScope::Parent, q->changeRecorder()->itemFetchScope().ancestorRetrieval())); + fetch->fetchScope().setCacheOnly(true); + fetch->fetchScope().setFetchRemoteIdentification(true); + // It's possible that one or more items were removed before this task was + // executed, so ignore it and just handle the rest. + fetch->fetchScope().setIgnoreRetrievalErrors(true); + + // copy list of attributes to fetch + const QSet attributes = q->changeRecorder()->itemFetchScope().attributes(); + for (const auto &attribute : attributes) { + fetch->fetchScope().fetchAttribute(attribute); + } + + connect(fetch, &KJob::result, this, &ResourceBasePrivate::slotPrepareItemsRetrievalResult); +} + +void ResourceBasePrivate::slotPrepareItemsRetrievalResult(KJob *job) +{ + Q_Q(ResourceBase); + Q_ASSERT_X(scheduler->currentTask().type == ResourceScheduler::FetchItems, + "ResourceBasePrivate::slotPrepareItemsRetrievalResult()", + "Preparing items retrieval although no items retrieval is in progress"); + if (job->error()) { + q->cancelTask(job->errorText()); + return; + } + auto fetch = qobject_cast(job); + const auto items = fetch->items(); + if (items.isEmpty()) { + q->cancelTask(); + return; + } + + const QSet parts = scheduler->currentTask().itemParts; + Q_ASSERT(items.first().parentCollection().isValid()); + if (!q->retrieveItems(items, parts)) { + q->cancelTask(); + } +} + +void ResourceBasePrivate::slotRecursiveMoveReplay(RecursiveMover *mover) +{ + Q_ASSERT(mover); + Q_ASSERT(!m_recursiveMover); + m_recursiveMover = mover; + connect(mover, &KJob::result, this, &ResourceBasePrivate::slotRecursiveMoveReplayResult); + mover->start(); +} + +void ResourceBasePrivate::slotRecursiveMoveReplayResult(KJob *job) +{ + Q_Q(ResourceBase); + m_recursiveMover = nullptr; + + if (job->error()) { + q->deferTask(); + return; + } + + changeProcessed(); +} + +void ResourceBase::itemsRetrievalDone() +{ + Q_D(ResourceBase); + // streaming enabled, so finalize the sync + if (d->mItemSyncer) { + d->mItemSyncer->deliveryDone(); + } else { + if (d->scheduler->currentTask().type == ResourceScheduler::FetchItems) { + d->scheduler->currentTask().sendDBusReplies(QString()); + } + // user did the sync himself, we are done now + d->scheduler->taskDone(); + } +} + +void ResourceBase::clearCache() +{ + Q_D(ResourceBase); + d->scheduler->scheduleResourceCollectionDeletion(); +} + +void ResourceBase::invalidateCache(const Collection &collection) +{ + Q_D(ResourceBase); + d->scheduler->scheduleCacheInvalidation(collection); +} + +Collection ResourceBase::currentCollection() const +{ + Q_D(const ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncCollection, + "ResourceBase::currentCollection()", + "Trying to access current collection although no item retrieval is in progress"); + return d->currentCollection; +} + +Item ResourceBase::currentItem() const +{ + Q_D(const ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::FetchItem, + "ResourceBase::currentItem()", + "Trying to access current item although no item retrieval is in progress"); + return d->scheduler->currentTask().items[0]; +} + +Item::List ResourceBase::currentItems() const +{ + Q_D(const ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::FetchItems, + "ResourceBase::currentItems()", + "Trying to access current items although no items retrieval is in progress"); + return d->scheduler->currentTask().items; +} + +void ResourceBase::synchronizeCollectionTree() +{ + d_func()->scheduler->scheduleCollectionTreeSync(); +} + +void ResourceBase::synchronizeTags() +{ + d_func()->scheduler->scheduleTagSync(); +} + +void ResourceBase::synchronizeRelations() +{ + d_func()->scheduler->scheduleRelationSync(); +} + +void ResourceBase::cancelTask() +{ + Q_D(ResourceBase); + if (d->mCurrentCollectionFetchJob) { + d->mCurrentCollectionFetchJob->kill(); + d->mCurrentCollectionFetchJob = nullptr; + } + switch (d->scheduler->currentTask().type) { + case ResourceScheduler::FetchItem: + itemRetrieved(Item()); // sends the error reply and + break; + case ResourceScheduler::FetchItems: + itemsRetrieved(Item::List()); + break; + case ResourceScheduler::ChangeReplay: + d->changeProcessed(); + break; + case ResourceScheduler::SyncCollectionTree: + case ResourceScheduler::SyncAll: + if (d->mCollectionSyncer) { + d->mCollectionSyncer->rollback(); + } else { + d->scheduler->taskDone(); + } + break; + case ResourceScheduler::SyncCollection: + if (d->mItemSyncer) { + d->mItemSyncer->rollback(); + } else { + d->scheduler->taskDone(); + } + break; + default: + d->scheduler->taskDone(); + } +} + +void ResourceBase::cancelTask(const QString &msg) +{ + cancelTask(); + + Q_EMIT error(msg); +} + +void ResourceBase::deferTask() +{ + Q_D(ResourceBase); + qCDebug(AKONADIAGENTBASE_LOG) << "Deferring task" << d->scheduler->currentTask(); + // Deferring a CollectionSync is just not implemented. + // We'd need to d->mItemSyncer->rollback() but also to NOT call taskDone in slotItemSyncDone() here... + Q_ASSERT(!d->mItemSyncer); + d->scheduler->deferTask(); +} + +void ResourceBase::doSetOnline(bool state) +{ + d_func()->scheduler->setOnline(state); +} + +void ResourceBase::synchronizeCollection(qint64 collectionId) +{ + synchronizeCollection(collectionId, false); +} + +void ResourceBase::synchronizeCollection(qint64 collectionId, bool recursive) +{ + Q_D(ResourceBase); + auto job = new CollectionFetchJob(Collection(collectionId), recursive ? CollectionFetchJob::Recursive : CollectionFetchJob::Base); + job->setFetchScope(changeRecorder()->collectionFetchScope()); + job->fetchScope().setResource(identifier()); + job->fetchScope().setListFilter(CollectionFetchScope::Sync); + connect(job, &KJob::result, d, &ResourceBasePrivate::slotCollectionListDone); +} + +void ResourceBasePrivate::slotCollectionListDone(KJob *job) +{ + if (!job->error()) { + const Collection::List list = static_cast(job)->collections(); + for (const Collection &collection : list) { + // We also get collections that should not be synced but are part of the tree. + if (collection.shouldList(Collection::ListSync)) { + if (mScheduleAttributeSyncBeforeCollectionSync) { + scheduler->scheduleAttributesSync(collection); + } + scheduler->scheduleSync(collection); + } + } + } else { + qCWarning(AKONADIAGENTBASE_LOG) << "Failed to fetch collection for collection sync: " << job->errorString(); + } +} + +void ResourceBase::synchronizeCollectionAttributes(const Akonadi::Collection &col) +{ + Q_D(ResourceBase); + d->scheduler->scheduleAttributesSync(col); +} + +void ResourceBase::synchronizeCollectionAttributes(qint64 collectionId) +{ + Q_D(ResourceBase); + auto job = new CollectionFetchJob(Collection(collectionId), CollectionFetchJob::Base); + job->setFetchScope(changeRecorder()->collectionFetchScope()); + job->fetchScope().setResource(identifier()); + connect(job, &KJob::result, d, &ResourceBasePrivate::slotCollectionListForAttributesDone); +} + +void ResourceBasePrivate::slotCollectionListForAttributesDone(KJob *job) +{ + if (!job->error()) { + const Collection::List list = static_cast(job)->collections(); + if (!list.isEmpty()) { + const Collection &col = list.first(); + scheduler->scheduleAttributesSync(col); + } + } + // TODO: error handling +} + +void ResourceBase::setTotalItems(int amount) +{ + qCDebug(AKONADIAGENTBASE_LOG) << amount; + Q_D(ResourceBase); + setItemStreamingEnabled(true); + if (d->mItemSyncer) { + d->mItemSyncer->setTotalItems(amount); + } +} + +void ResourceBase::setDisableAutomaticItemDeliveryDone(bool disable) +{ + Q_D(ResourceBase); + if (d->mItemSyncer) { + d->mItemSyncer->setDisableAutomaticDeliveryDone(disable); + } + d->mDisableAutomaticItemDeliveryDone = disable; +} + +void ResourceBase::setItemStreamingEnabled(bool enable) +{ + Q_D(ResourceBase); + d->createItemSyncInstanceIfMissing(); + if (d->mItemSyncer) { + d->mItemSyncer->setStreamingEnabled(enable); + } +} + +void ResourceBase::itemsRetrieved(const Item::List &items) +{ + Q_D(ResourceBase); + if (d->scheduler->currentTask().type == ResourceScheduler::FetchItems) { + auto trx = new TransactionSequence(this); + connect(trx, &KJob::result, d, &ResourceBasePrivate::slotItemSyncDone); + for (const Item &item : items) { + Q_ASSERT(item.parentCollection().isValid()); + if (item.isValid()) { // NOLINT(bugprone-branch-clone) + new ItemModifyJob(item, trx); + } else if (!item.remoteId().isEmpty()) { + auto job = new ItemCreateJob(item, item.parentCollection(), trx); + job->setMerge(ItemCreateJob::RID); + } else { + // This should not happen, but just to be sure... + new ItemModifyJob(item, trx); + } + } + trx->commit(); + } else { + d->createItemSyncInstanceIfMissing(); + if (d->mItemSyncer) { + d->mItemSyncer->setFullSyncItems(items); + } + } +} + +void ResourceBase::itemsRetrievedIncremental(const Item::List &changedItems, const Item::List &removedItems) +{ + Q_D(ResourceBase); + d->createItemSyncInstanceIfMissing(); + if (d->mItemSyncer) { + d->mItemSyncer->setIncrementalSyncItems(changedItems, removedItems); + } +} + +void ResourceBasePrivate::slotItemSyncDone(KJob *job) +{ + mItemSyncer = nullptr; + Q_Q(ResourceBase); + if (job->error() && job->error() != Job::UserCanceled) { + Q_EMIT q->error(job->errorString()); + } + if (scheduler->currentTask().type == ResourceScheduler::FetchItems) { + scheduler->currentTask().sendDBusReplies((job->error() && job->error() != Job::UserCanceled) ? job->errorString() : QString()); + } + scheduler->taskDone(); +} + +void ResourceBasePrivate::slotDelayedEmitProgress() +{ + Q_Q(ResourceBase); + if (mAutomaticProgressReporting) { + Q_EMIT q->percent(mUnemittedProgress); + + for (const QVariantMap &statusMap : std::as_const(mUnemittedAdvancedStatus)) { + Q_EMIT q->advancedStatus(statusMap); + } + } + mUnemittedProgress = 0; + mUnemittedAdvancedStatus.clear(); +} + +void ResourceBasePrivate::slotPercent(KJob *job, quint64 percent) +{ + mUnemittedProgress = static_cast(percent); + + const auto collection = job->property("collection").value(); + if (collection.isValid()) { + QVariantMap statusMap; + statusMap.insert(QStringLiteral("key"), QStringLiteral("collectionSyncProgress")); + statusMap.insert(QStringLiteral("collectionId"), collection.id()); + statusMap.insert(QStringLiteral("percent"), static_cast(percent)); + + mUnemittedAdvancedStatus[collection.id()] = statusMap; + } + // deliver completion right away, intermediate progress at 1s intervals + if (percent == 100U) { + mProgressEmissionCompressor.stop(); + slotDelayedEmitProgress(); + } else if (!mProgressEmissionCompressor.isActive()) { + mProgressEmissionCompressor.start(); + } +} + +void ResourceBase::setHierarchicalRemoteIdentifiersEnabled(bool enable) +{ + Q_D(ResourceBase); + d->mHierarchicalRid = enable; +} + +void ResourceBase::scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument, SchedulePriority priority) +{ + Q_D(ResourceBase); + d->scheduler->scheduleCustomTask(receiver, method, argument, priority); +} + +void ResourceBase::taskDone() +{ + Q_D(ResourceBase); + d->scheduler->taskDone(); +} + +void ResourceBase::retrieveCollectionAttributes(const Collection &collection) +{ + collectionAttributesRetrieved(collection); +} + +void ResourceBase::retrieveTags() +{ + Q_D(ResourceBase); + d->scheduler->taskDone(); +} + +void ResourceBase::retrieveRelations() +{ + Q_D(ResourceBase); + d->scheduler->taskDone(); +} + +bool ResourceBase::retrieveItem(const Akonadi::Item &item, const QSet &parts) +{ + Q_UNUSED(item) + Q_UNUSED(parts) + // retrieveItem() can no longer be pure virtual, because then we could not mark + // it as deprecated (i.e. implementations would still be forced to implement it), + // so instead we assert here. + // NOTE: Don't change to Q_ASSERT_X here: while the macro can be disabled at + // compile time, we want to hit this assert *ALWAYS*. + qt_assert_x("Akonadi::ResourceBase::retrieveItem()", + "The base implementation of retrieveItem() must never be reached. " + "You must implement either retrieveItem() or retrieveItems(Akonadi::Item::List, QSet) overload " + "to handle item retrieval requests.", + __FILE__, + __LINE__); + return false; +} + +bool ResourceBase::retrieveItems(const Akonadi::Item::List &items, const QSet &parts) +{ + Q_D(ResourceBase); + + // If we reach this implementation of retrieveItems() then it means that the + // resource is still using the deprecated retrieveItem() method, so we explode + // this to a myriad of tasks in scheduler and let them be processed one by one + + const qint64 id = d->scheduler->currentTask().serial; + for (const auto &item : items) { + d->scheduler->scheduleItemFetch(item, parts, d->scheduler->currentTask().dbusMsgs, id); + } + taskDone(); + return true; +} + +void Akonadi::ResourceBase::abortActivity() +{ +} + +void ResourceBase::setItemTransactionMode(ItemSync::TransactionMode mode) +{ + Q_D(ResourceBase); + d->mItemTransactionMode = mode; +} + +void ResourceBase::setItemSynchronizationFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(ResourceBase); + if (!d->mItemSyncFetchScope) { + d->mItemSyncFetchScope = new ItemFetchScope; + } + *(d->mItemSyncFetchScope) = fetchScope; +} + +void ResourceBase::setItemMergingMode(ItemSync::MergeMode mode) +{ + Q_D(ResourceBase); + d->mItemMergeMode = mode; +} + +void ResourceBase::setAutomaticProgressReporting(bool enabled) +{ + Q_D(ResourceBase); + d->mAutomaticProgressReporting = enabled; +} + +QString ResourceBase::dumpNotificationListToString() const +{ + Q_D(const ResourceBase); + return d->dumpNotificationListToString(); +} + +QString ResourceBase::dumpSchedulerToString() const +{ + Q_D(const ResourceBase); + return d->dumpToString(); +} + +void ResourceBase::dumpMemoryInfo() const +{ + Q_D(const ResourceBase); + d->dumpMemoryInfo(); +} + +QString ResourceBase::dumpMemoryInfoToString() const +{ + Q_D(const ResourceBase); + return d->dumpMemoryInfoToString(); +} + +void ResourceBase::tagsRetrieved(const Tag::List &tags, const QHash &tagMembers) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncTags || d->scheduler->currentTask().type == ResourceScheduler::SyncAll + || d->scheduler->currentTask().type == ResourceScheduler::Custom, + "ResourceBase::tagsRetrieved()", + "Calling tagsRetrieved() although no tag retrieval is in progress"); + if (!d->mTagSyncer) { + d->mTagSyncer = new TagSync(this); + connect(d->mTagSyncer, &KJob::percentChanged, d, + &ResourceBasePrivate::slotPercent); // NOLINT(google-runtime-int): ulong comes from KJob + connect(d->mTagSyncer, &KJob::result, d, &ResourceBasePrivate::slotTagSyncDone); + } + d->mTagSyncer->setFullTagList(tags); + d->mTagSyncer->setTagMembers(tagMembers); +} + +void ResourceBasePrivate::slotTagSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + mTagSyncer = nullptr; + if (job->error()) { + if (job->error() != Job::UserCanceled) { + qCWarning(AKONADIAGENTBASE_LOG) << "TagSync failed: " << job->errorString(); + Q_EMIT q->error(job->errorString()); + } + } + + scheduler->taskDone(); +} + +void ResourceBase::relationsRetrieved(const Relation::List &relations) +{ + Q_D(ResourceBase); + Q_ASSERT_X(d->scheduler->currentTask().type == ResourceScheduler::SyncRelations || d->scheduler->currentTask().type == ResourceScheduler::SyncAll + || d->scheduler->currentTask().type == ResourceScheduler::Custom, + "ResourceBase::relationsRetrieved()", + "Calling relationsRetrieved() although no relation retrieval is in progress"); + if (!d->mRelationSyncer) { + d->mRelationSyncer = new RelationSync(this); + connect(d->mRelationSyncer, &KJob::percentChanged, d, + &ResourceBasePrivate::slotPercent); // NOLINT(google-runtime-int): ulong comes from KJob + connect(d->mRelationSyncer, &KJob::result, d, &ResourceBasePrivate::slotRelationSyncDone); + } + d->mRelationSyncer->setRemoteRelations(relations); +} + +void ResourceBasePrivate::slotRelationSyncDone(KJob *job) +{ + Q_Q(ResourceBase); + mRelationSyncer = nullptr; + if (job->error()) { + if (job->error() != Job::UserCanceled) { + qCWarning(AKONADIAGENTBASE_LOG) << "RelationSync failed: " << job->errorString(); + Q_EMIT q->error(job->errorString()); + } + } + + scheduler->taskDone(); +} + +#include "moc_resourcebase.cpp" +#include "resourcebase.moc" diff --git a/src/agentbase/resourcebase.h b/src/agentbase/resourcebase.h new file mode 100644 index 0000000..1743bd0 --- /dev/null +++ b/src/agentbase/resourcebase.h @@ -0,0 +1,880 @@ +/* + This file is part of akonadiresources. + + SPDX-FileCopyrightText: 2006 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentbase.h" +#include "akonadiagentbase_export.h" +#include "collection.h" +#include "item.h" +#include "itemsync.h" + +class KJob; +class Akonadi__ResourceAdaptor; +class ResourceState; + +namespace Akonadi +{ +class ResourceBasePrivate; + +/** + * @short The base class for all Akonadi resources. + * + * This class should be used as a base class by all resource agents, + * because it encapsulates large parts of the protocol between + * resource agent, agent manager and the Akonadi storage. + * + * It provides many convenience methods to make implementing a + * new Akonadi resource agent as simple as possible. + * + *

How to write a resource

+ * + * The following provides an overview of what you need to do to implement + * your own Akonadi resource. In the following, the term 'backend' refers + * to the entity the resource connects with Akonadi, be it a single file + * or a remote server. + * + * @todo Complete this (online/offline state management) + * + *
Basic %Resource Framework
+ * + * The following is needed to create a new resource: + * - A new class deriving from Akonadi::ResourceBase, implementing at least all + * pure-virtual methods, see below for further details. + * - call init() in your main() function. + * - a .desktop file similar to the following example + * \code + * [Desktop Entry] + * Name=My Akonadi Resource + * Type=AkonadiResource + * Exec=akonadi_my_resource + * Icon=my-icon + * + * X-Akonadi-MimeTypes= + * X-Akonadi-Capabilities=Resource + * X-Akonadi-Identifier=akonadi_my_resource + * \endcode + * + *
Handling PIM Items
+ * + * To follow item changes in the backend, the following steps are necessary: + * - Implement retrieveItems() to synchronize all items in the given + * collection. If the backend supports incremental retrieval, + * implementing support for that is recommended to improve performance. + * - Convert the items provided by the backend to Akonadi items. + * This typically happens either in retrieveItems() if you retrieved + * the collection synchronously (not recommended for network backends) or + * in the result slot of the asynchronous retrieval job. + * Converting means to create Akonadi::Item objects for every retrieved + * item. It's very important that every object has its remote identifier set. + * - Call itemsRetrieved() or itemsRetrievedIncremental() respectively + * with the item objects created above. The Akonadi storage will then be + * updated automatically. Note that it is usually not necessary to manipulate + * any item in the Akonadi storage manually. + * + * To fetch item data on demand, the method retrieveItem() needs to be + * reimplemented. Fetch the requested data there and call itemRetrieved() + * with the result item. + * + * To write local changes back to the backend, you need to re-implement + * the following three methods: + * - itemAdded() + * - itemChanged() + * - itemRemoved() + * + * Note that these three functions don't get the full payload of the items by default, + * you need to change the item fetch scope of the change recorder to fetch the full + * payload. This can be expensive with big payloads, though.
+ * Once you have handled changes in itemAdded() and itemChanged(), call changeCommitted(). + * Once you have handled changes in itemRemoved(), call changeProcessed(); + * These methods are called whenever a local item related to this resource is + * added, modified or deleted. They are only called if the resource is online, otherwise + * all changes are recorded and replayed as soon the resource is online again. + * + *
Handling Collections
+ * + * To follow collection changes in the backend, the following steps are necessary: + * - Implement retrieveCollections() to retrieve collections from the backend. + * If the backend supports incremental collections updates, implementing + * support for that is recommended to improve performance. + * - Convert the collections of the backend to Akonadi collections. + * This typically happens either in retrieveCollections() if you retrieved + * the collection synchronously (not recommended for network backends) or + * in the result slot of the asynchronous retrieval job. + * Converting means to create Akonadi::Collection objects for every retrieved + * collection. It's very important that every object has its remote identifier + * and its parent remote identifier set. + * - Call collectionsRetrieved() or collectionsRetrievedIncremental() respectively + * with the collection objects created above. The Akonadi storage will then be + * updated automatically. Note that it is usually not necessary to manipulate + * any collection in the Akonadi storage manually. + * + * + * To write local collection changes back to the backend, you need to re-implement + * the following three methods: + * - collectionAdded() + * - collectionChanged() + * - collectionRemoved() + * Once you have handled changes in collectionAdded() and collectionChanged(), call changeCommitted(). + * Once you have handled changes in collectionRemoved(), call changeProcessed(); + * These methods are called whenever a local collection related to this resource is + * added, modified or deleted. They are only called if the resource is online, otherwise + * all changes are recorded and replayed as soon the resource is online again. + * + * @todo Convenience base class for collection-less resources + */ +// FIXME_API: API dox need to be updated for Observer approach (kevin) +class AKONADIAGENTBASE_EXPORT ResourceBase : public AgentBase +{ + Q_OBJECT + +public: + /** + * Use this method in the main function of your resource + * application to initialize your resource subclass. + * This method also takes care of creating a KApplication + * object and parsing command line arguments. + * + * @note In case the given class is also derived from AgentBase::Observer + * it gets registered as its own observer (see AgentBase::Observer), e.g. + * resourceInstance->registerObserver( resourceInstance ); + * + * @code + * + * class MyResource : public ResourceBase + * { + * ... + * }; + * + * int main( int argc, char **argv ) + * { + * return ResourceBase::init( argc, argv ); + * } + * + * @endcode + * + * @param argc number of arguments + * @param argv string arguments + */ + template static int init(int argc, char **argv) + { + // Disable session management + qunsetenv("SESSION_MANAGER"); + + QApplication app(argc, argv); + debugAgent(argc, argv); + const QString id = parseArguments(argc, argv); + T r(id); + + // check if T also inherits AgentBase::Observer and + // if it does, automatically register it on itself + auto observer = dynamic_cast(&r); + if (observer != nullptr) { + r.registerObserver(observer); + } + + return init(r); + } + + /** + * This method is used to set the name of the resource. + */ + void setName(const QString &name); + + /** + * Returns the name of the resource. + */ + Q_REQUIRED_RESULT QString name() const; + + /** + * Enable or disable automatic progress reporting. By default, it is enabled. + * When enabled, the resource will automatically emit the signals percent() and status() + * while syncing items or collections. + * + * The automatic progress reporting is done on a per item / per collection basis, so if a + * finer granularity is desired, automatic reporting should be disabled and the subclass should + * emit the percent() and status() signals itself. + * + * @param enabled Whether or not automatic emission of the signals is enabled. + * @since 4.7 + */ + void setAutomaticProgressReporting(bool enabled); + +Q_SIGNALS: + /** + * This signal is emitted whenever the name of the resource has changed. + * + * @param name The new name of the resource. + */ + void nameChanged(const QString &name); + + /** + * Emitted when a full synchronization has been completed. + */ + void synchronized(); + + /** + * Emitted when a collection attributes synchronization has been completed. + * + * @param collectionId The identifier of the collection whose attributes got synchronized. + * @since 4.6 + */ + void attributesSynchronized(qlonglong collectionId); + + /** + * Emitted when a collection tree synchronization has been completed. + * + * @since 4.8 + */ + void collectionTreeSynchronized(); + + /** + * Emitted when the item synchronization processed the current batch and is ready for a new one. + * Use this to throttle the delivery to not overload Akonadi. + * + * Throttling can be used during item retrieval (retrieveItems(Akonadi::Collection)) in streaming mode. + * To throttle only deliver itemSyncBatchSize() items, and wait for this signal, then again deliver + * @param remainingBatchSize items. + * + * By always only providing the number of items required to process the batch, the items don't pile + * up in memory and items are only provided as fast as Akonadi can process them. + * + * @see batchSize() + * + * @since 4.14 + */ + void retrieveNextItemSyncBatch(int remainingBatchSize); + +protected Q_SLOTS: + /** + * Retrieve the collection tree from the remote server and supply it via + * collectionsRetrieved() or collectionsRetrievedIncremental(). + * @see collectionsRetrieved(), collectionsRetrievedIncremental() + */ + virtual void retrieveCollections() = 0; + + /** + * Retrieve all tags from the backend + * @see tagsRetrieved() + */ + virtual void retrieveTags(); + + /** + * Retrieve all relations from the backend + * @see relationsRetrieved() + */ + virtual void retrieveRelations(); + + /** + * Retrieve the attributes of a single collection from the backend. The + * collection to retrieve attributes for is provided as @p collection. + * Add the attributes parts and call collectionAttributesRetrieved() + * when done. + * + * @param collection The collection whose attributes should be retrieved. + * @see collectionAttributesRetrieved() + * @since 4.6 + */ + virtual void retrieveCollectionAttributes(const Akonadi::Collection &collection); + + /** + * Retrieve all (new/changed) items in collection @p collection. + * It is recommended to use incremental retrieval if the backend supports that + * and provide the result by calling itemsRetrievedIncremental(). + * If incremental retrieval is not possible, provide the full listing by calling + * itemsRetrieved( const Item::List& ). + * In any case, ensure that all items have a correctly set remote identifier + * to allow synchronizing with items already existing locally. + * In case you don't want to use the built-in item syncing code, store the retrieved + * items manually and call itemsRetrieved() once you are done. + * @param collection The collection whose items to retrieve. + * @see itemsRetrieved( const Item::List& ), itemsRetrievedIncremental(), itemsRetrieved(), currentCollection(), batchSize() + */ + virtual void retrieveItems(const Akonadi::Collection &collection) = 0; + + /** + * Returns the batch size used during the item sync. + * + * This can be used to throttle the item delivery. + * + * @see retrieveNextItemSyncBatch(int), retrieveItems(Akonadi::Collection) + * @since 4.14 + */ + int itemSyncBatchSize() const; + + /** + * Set the batch size used during the item sync. + * The default is 10. + * + * @see retrieveNextItemSyncBatch(int) + * @since 4.14 + */ + void setItemSyncBatchSize(int batchSize); + + /** + * Set to true to schedule an attribute sync before every item sync. + * The default is false. + * + * @since 4.15 + */ + void setScheduleAttributeSyncBeforeItemSync(bool); + + /** + * Retrieve a single item from the backend. The item to retrieve is provided as @p item. + * Add the requested payload parts and call itemRetrieved() when done. + * @param item The empty item whose payload should be retrieved. Use this object when delivering + * the result instead of creating a new item to ensure conflict detection will work. + * @param parts The item parts that should be retrieved. + * @return false if there is an immediate error when retrieving the item. + * @see itemRetrieved() + * @deprecated Use retrieveItems(const Akonadi::Item::List &, const QSet &) instead. + */ + AKONADIAGENTBASE_DEPRECATED virtual bool retrieveItem(const Akonadi::Item &item, const QSet &parts); + + /** + * Retrieve given @p items from the backend. + * Add the requested payload parts and call itemsRetrieved() when done. + * It is guaranteed that all @p items in the list belong to the same Collection. + * + * @param items The items whose payload should be retrieved. Use those objects + * when delivering the result instead of creating new items to ensure conflict + * detection will work. + * @param parts The item parts that should be retrieved. + * @return false if there is an immediate error when retrieving the items. + * @see itemsRetrieved() + * @since 5.4 + * + * @todo: Make this method pure virtual once retrieveItem() is gone + */ + virtual bool retrieveItems(const Akonadi::Item::List &items, const QSet &parts); + + /** + * Abort any activity in progress in the backend. By default this method does nothing. + * + * @since 4.6 + */ + virtual void abortActivity(); + + /** + * Dump resource internals, for debugging. + * @since 4.9 + */ + virtual QString dumpResourceToString() const + { + return QString(); + } + +protected: + /** + * Creates a base resource. + * + * @param id The instance id of the resource. + */ + ResourceBase(const QString &id); + + /** + * Destroys the base resource. + */ + ~ResourceBase() override; + + /** + * Call this method from retrieveItem() once the result is available. + * + * @param item The retrieved item. + */ + void itemRetrieved(const Item &item); + + /** + * Call this method from retrieveCollectionAttributes() once the result is available. + * + * @param collection The collection whose attributes got retrieved. + * @since 4.6 + */ + void collectionAttributesRetrieved(const Collection &collection); + + /** + * Resets the dirty flag of the given item and updates the remote id. + * + * Call whenever you have successfully written changes back to the server. + * This implicitly calls changeProcessed(). + * @param item The changed item. + */ + void changeCommitted(const Item &item); + + /** + * Resets the dirty flag of all given items and updates remote ids. + * + * Call whenever you have successfully written changes back to the server. + * This implicitly calls changeProcessed(). + * @param items Changed items + * + * @since 4.11 + */ + void changesCommitted(const Item::List &items); + + /** + * Resets the dirty flag of the given tag and updates the remote id. + * + * Call whenever you have successfully written changes back to the server. + * This implicitly calls changeProcessed(). + * @param tag Changed tag. + * + * @since 4.13 + */ + void changeCommitted(const Tag &tag); + + /** + * Call whenever you have successfully handled or ignored a collection + * change notification. + * + * This will update the remote identifier of @p collection if necessary, + * as well as any other collection attributes. + * This implicitly calls changeProcessed(). + * @param collection The collection which changes have been handled. + */ + void changeCommitted(const Collection &collection); + + /** + * Call this to supply the full folder tree retrieved from the remote server. + * + * @param collections A list of collections. + * @see collectionsRetrievedIncremental() + */ + void collectionsRetrieved(const Collection::List &collections); + + void tagsRetrieved(const Tag::List &tags, const QHash &tagMembers); + void relationsRetrieved(const Relation::List &relations); + + /** + * Call this to supply incrementally retrieved collections from the remote server. + * + * @param changedCollections Collections that have been added or changed. + * @param removedCollections Collections that have been deleted. + * @see collectionsRetrieved() + */ + void collectionsRetrievedIncremental(const Collection::List &changedCollections, const Collection::List &removedCollections); + + /** + * Enable collection streaming, that is collections don't have to be delivered at once + * as result of a retrieveCollections() call but can be delivered by multiple calls + * to collectionsRetrieved() or collectionsRetrievedIncremental(). When all collections + * have been retrieved, call collectionsRetrievalDone(). + * @param enable @c true if collection streaming should be enabled, @c false by default + */ + void setCollectionStreamingEnabled(bool enable); + + /** + * Call this method to indicate you finished synchronizing the collection tree. + * + * This is not needed if you use the built in syncing without collection streaming + * and call collectionsRetrieved() or collectionRetrievedIncremental() instead. + * If collection streaming is enabled, call this method once all collections have been delivered + * using collectionsRetrieved() or collectionsRetrievedIncremental(). + */ + void collectionsRetrievalDone(); + + /** + * Allows to keep locally changed collection parts during the collection sync. + * + * This is useful for resources to be able to provide default values during the collection + * sync, while preserving eventual more up-to date values. + * + * Valid values are attribute types and "CONTENTMIMETYPE" for the collections content mimetypes. + * + * By default this is enabled for the EntityDisplayAttribute. + * + * @param parts A set parts for which local changes should be preserved. + * @since 4.14 + */ + void setKeepLocalCollectionChanges(const QSet &parts); + + /** + * Call this method to supply the full collection listing from the remote server. Items not present in the list + * will be dropped from the Akonadi database. + * + * If the remote server supports incremental listing, it's strongly + * recommended to use itemsRetrievedIncremental() instead. + * @param items A list of items. + * @see itemsRetrievedIncremental(). + */ + void itemsRetrieved(const Item::List &items); + + /** + * Call this method when you want to use the itemsRetrieved() method + * in streaming mode and indicate the amount of items that will arrive + * that way. + * + * @warning By default this will end the item sync automatically once + * sufficient items were delivered. To disable this and only make use + * of the progress reporting, use setDisableAutomaticItemDeliveryDone() + * + * @note The recommended way is therefore: + * @code + * setDisableAutomaticItemDeliveryDone(true); + * setItemStreamingEnabled(true); + * setTotalItems(X); // X = sum of all items in all batches + * while (...) { + * itemsRetrievedIncremental(...); + * // or itemsRetrieved(...); + * } + * itemsRetrievalDone(); + * @endcode + * + * @param amount number of items that will arrive in streaming mode + * @see setDisableAutomaticItemDeliveryDone(bool) + * @see setItemStreamingEnabled(bool) + */ + void setTotalItems(int amount); + + /** + * Disables the automatic completion of the item sync, + * based on the number of delivered items. + * + * This ensures that the item sync only finishes once itemsRetrieved() + * is called, while still making it possible to use the automatic progress + * reporting based on setTotalItems(). + * + * @note This needs to be called once, before the item sync is started. + * + * @see setTotalItems(int) + * @since 4.14 + */ + void setDisableAutomaticItemDeliveryDone(bool disable); + + /** + * Enable item streaming, which is disabled by default. + * Item streaming means that the resource can call setTotalItems(), + * and then itemsRetrieved() or itemsRetrievedIncremental() multiple times, + * in chunks. When all is done, the resource should call itemsRetrievalDone(). + * @param enable @c true if items are delivered in chunks rather in one big block. + * @see setTotalItems(int) + */ + void setItemStreamingEnabled(bool enable); + + /** + * Set transaction mode for item sync'ing. + * @param mode item transaction mode + * @see Akonadi::ItemSync::TransactionMode + * @since 4.6 + */ + void setItemTransactionMode(ItemSync::TransactionMode mode); + + /** + * Set merge mode for item sync'ing. + * + * Default merge mode is RIDMerge. + * + * @note This method must be called before first call to itemRetrieved(), + * itemsRetrieved() or itemsRetrievedIncremental(). + * + * @param mode Item merging mode (see ItemCreateJob for details on item merging) + * @see Akonadi::ItemSync::MergeMode + * @since 4.14.11 + */ + void setItemMergingMode(ItemSync::MergeMode mode); + + /** + * Set the fetch scope applied for item synchronization. + * By default, the one set on the changeRecorder() is used. However, it can make sense + * to specify a specialized fetch scope for synchronization to improve performance. + * The rule of thumb is to remove anything from this fetch scope that does not provide + * additional information regarding whether and item has changed or not. This is primarily + * relevant for backends not supporting incremental retrieval. + * @param fetchScope The fetch scope to use by the internal Akonadi::ItemSync instance. + * @see Akonadi::ItemSync + * @since 4.6 + */ + void setItemSynchronizationFetchScope(const ItemFetchScope &fetchScope); + + /** + * Call this method to supply incrementally retrieved items from the remote server. + * + * @param changedItems Items changed in the backend. + * @param removedItems Items removed from the backend. + */ + void itemsRetrievedIncremental(const Item::List &changedItems, const Item::List &removedItems); + + /** + * Call this method to indicate you finished synchronizing the current collection. + * + * This is not needed if you use the built in syncing without item streaming + * and call itemsRetrieved() or itemsRetrievedIncremental() instead. + * If item streaming is enabled, call this method once all items have been delivered + * using itemsRetrieved() or itemsRetrievedIncremental(). + * @see retrieveItems() + */ + void itemsRetrievalDone(); + + /** + * Call this method to remove all items and collections of the resource from the + * server cache. + * + * The method should not be used anymore + * + * @see invalidateCache() + * @since 4.3 + */ + void clearCache(); + + /** + * Call this method to invalidate all cached content in @p collection. + * + * The method should be used when the backend indicated that the cached content + * is no longer valid. + * + * @param collection parent of the content to be invalidated in cache + * @since 4.8 + */ + void invalidateCache(const Collection &collection); + + /** + * Returns the collection that is currently synchronized. + * @note Calling this method is only allowed during a collection synchronization task, that + * is directly or indirectly from retrieveItems(). + */ + Collection currentCollection() const; + + /** + * Returns the item that is currently retrieved. + * @note Calling this method is only allowed during fetching a single item, that + * is directly or indirectly from retrieveItem(). + */ + AKONADIAGENTBASE_DEPRECATED Item currentItem() const; + + /** + * Returns the items that are currently retrieved. + * @note Calling this method is only allowed during item fetch, that is + * directly or indirectly from retrieveItems(Akonadi::Item::List,QSet) + */ + Item::List currentItems() const; + + /** + * This method is called whenever the resource should start synchronize all data. + */ + void synchronize(); + + /** + * This method is called whenever the collection with the given @p id + * shall be synchronized. + */ + void synchronizeCollection(qint64 id); + + /** + * This method is called whenever the collection with the given @p id + * shall be synchronized. + * @param recursive if true, a recursive synchronization is done + */ + void synchronizeCollection(qint64 id, bool recursive); + + /** + * This method is called whenever the collection with the given @p id + * shall have its attributes synchronized. + * + * @param id The id of the collection to synchronize + * @since 4.6 + */ + void synchronizeCollectionAttributes(qint64 id); + + /** + * Synchronizes the collection attributes. + * + * @param col The id of the collection to synchronize + * @since 4.15 + */ + void synchronizeCollectionAttributes(const Akonadi::Collection &col); + + /** + * Refetches the Collections. + */ + void synchronizeCollectionTree(); + + /** + * Refetches Tags. + */ + void synchronizeTags(); + + /** + * Refetches Relations. + */ + void synchronizeRelations(); + + /** + * Stops the execution of the current task and continues with the next one. + */ + void cancelTask(); + + /** + * Stops the execution of the current task and continues with the next one. + * Additionally an error message is emitted. + * @param error additional error message to be emitted + */ + void cancelTask(const QString &error); + + /** + * Suspends the execution of the current task and tries again to execute it. + * + * This can be used to delay the task processing until the resource has reached a safe + * state, e.g. login to a server succeeded. + * + * @note This does not change the order of tasks so if there is no task with higher priority + * e.g. a custom task added with #Prepend the deferred task will be processed again. + * + * @since 4.3 + */ + void deferTask(); + + /** + * Inherited from AgentBase. + * + * When going offline, the scheduler aborts the current task, so you should + * do the same in your resource, if the task implementation is asynchronous. + */ + void doSetOnline(bool online) override; + + /** + * Indicate the use of hierarchical remote identifiers. + * + * This means that it is possible to have two different items with the same + * remoteId in different Collections. + * + * This should be called in the resource constructor as needed. + * + * @param enable whether to enable use of hierarchical remote identifiers + * @since 4.4 + */ + void setHierarchicalRemoteIdentifiersEnabled(bool enable); + + friend class ResourceScheduler; + friend class ::ResourceState; + + /** + * Describes the scheduling priority of a task that has been queued + * for execution. + * + * @see scheduleCustomTask + * @since 4.4 + */ + enum SchedulePriority { + Prepend, ///< The task will be executed as soon as the current task has finished. + AfterChangeReplay, ///< The task is scheduled after the last ChangeReplay task in the queue + Append ///< The task will be executed after all tasks currently in the queue are finished + }; + + /** + * Schedules a custom task in the internal scheduler. It will be queued with + * all other tasks such as change replays and retrieval requests and eventually + * executed by calling the specified method. With the priority parameter the + * time of execution of the Task can be influenced. @see SchedulePriority + * @param receiver The object the slot should be called on. + * @param method The name of the method (and only the name, not signature, not SLOT(...) macro), + * that should be called to execute this task. The method has to be a slot and take a QVariant as + * argument. + * @param argument A QVariant argument passed to the method specified above. Use this to pass task + * parameters. + * @param priority Priority of the task. Use this to influence the position in + * the execution queue. + * @since 4.4 + */ + void scheduleCustomTask(QObject *receiver, const char *method, const QVariant &argument, SchedulePriority priority = Append); + + /** + * Indicate that the current task is finished. Use this method from the slot called via scheduleCustomTaks(). + * As with all the other callbacks, make sure to either call taskDone() or cancelTask()/deferTask() on all + * exit paths, otherwise the resource will hang. + * @since 4.4 + */ + void taskDone(); + + /** + * Dump the contents of the current ChangeReplay + * @since 4.8.1 + */ + QString dumpNotificationListToString() const; + + /** + * Dumps memory usage information to stdout. + * For now it outputs the result of glibc's mallinfo(). + * This is useful to check if your memory problems are due to poor management by glibc. + * Look for a high value on fsmblks when interpreting results. + * man mallinfo for more details. + * @since 4.11 + */ + void dumpMemoryInfo() const; + + /** + * Returns a string with memory usage information. + * @see dumpMemoryInfo() + * + * @since 4.11 + */ + QString dumpMemoryInfoToString() const; + + /** + * Dump the state of the scheduler + * @since 4.8.1 + */ + QString dumpSchedulerToString() const; + +private: + static QString parseArguments(int argc, char **argv); + static int init(ResourceBase &r); + + // dbus resource interface + friend class ::Akonadi__ResourceAdaptor; + + void requestItemDelivery(const QVector &uids, const QByteArrayList &parts); + +private: + Q_DECLARE_PRIVATE(ResourceBase) + + Q_PRIVATE_SLOT(d_func(), void slotAbortRequested()) + Q_PRIVATE_SLOT(d_func(), void slotDeliveryDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotDeleteResourceCollection()) + Q_PRIVATE_SLOT(d_func(), void slotDeleteResourceCollectionDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionDeletionDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotInvalidateCache(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void slotLocalListDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeCollection(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionListDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeCollectionAttributes(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionListForAttributesDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotCollectionAttributesSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotItemSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotPercent(KJob *, quint64)) + Q_PRIVATE_SLOT(d_func(), void slotDelayedEmitProgress()) + Q_PRIVATE_SLOT(d_func(), void slotPrepareItemRetrieval(const Akonadi::Item &items)) + Q_PRIVATE_SLOT(d_func(), void slotPrepareItemRetrievalResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotPrepareItemsRetrieval(const QVector &items)) + Q_PRIVATE_SLOT(d_func(), void slotPrepareItemsRetrievalResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void changeCommittedResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotSessionReconnected()) + Q_PRIVATE_SLOT(d_func(), void slotRecursiveMoveReplay(RecursiveMover *)) + Q_PRIVATE_SLOT(d_func(), void slotRecursiveMoveReplayResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotTagSyncDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotRelationSyncDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeTags()) + Q_PRIVATE_SLOT(d_func(), void slotSynchronizeRelations()) + Q_PRIVATE_SLOT(d_func(), void slotItemRetrievalCollectionFetchDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotAttributeRetrievalCollectionFetchDone(KJob *)) +}; + +} + +#ifndef AKONADI_RESOURCE_MAIN +/** + * Convenience Macro for the most common main() function for Akonadi resources. + */ +#define AKONADI_RESOURCE_MAIN(resourceClass) \ + int main(int argc, char **argv) \ + { \ + return Akonadi::ResourceBase::init(argc, argv); \ + } +#endif + diff --git a/src/agentbase/resourcebase.kcfg b/src/agentbase/resourcebase.kcfg new file mode 100644 index 0000000..9de6327 --- /dev/null +++ b/src/agentbase/resourcebase.kcfg @@ -0,0 +1,13 @@ + + + + + + 5 + This setting allows administrators to set a minimum delay between two mail checks. The user will not be able to choose a value smaller than the value set here. + + + diff --git a/src/agentbase/resourcebasesettings.kcfgc b/src/agentbase/resourcebasesettings.kcfgc new file mode 100644 index 0000000..1605f36 --- /dev/null +++ b/src/agentbase/resourcebasesettings.kcfgc @@ -0,0 +1,9 @@ +File=resourcebase.kcfg +ClassName=ResourceBaseSettings +NameSpace=Akonadi +Singleton=true +ItemAccessors=true +Mutators=true +Visibility=AKONADIAGENTBASE_EXPORT +SetUserTextx=true +IncludeFiles=akonadiagentbase_export.h diff --git a/src/agentbase/resourcescheduler.cpp b/src/agentbase/resourcescheduler.cpp new file mode 100644 index 0000000..dd930f7 --- /dev/null +++ b/src/agentbase/resourcescheduler.cpp @@ -0,0 +1,698 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resourcescheduler_p.h" + +#include "recursivemover_p.h" +#include + +#include "akonadiagentbase_debug.h" +#include "private/instance_p.h" +#include + +#include +#include + +using namespace Akonadi; + +qint64 ResourceScheduler::Task::latestSerial = 0; +static QDBusAbstractInterface *s_resourcetracker = nullptr; + +/// @cond PRIVATE + +ResourceScheduler::ResourceScheduler(QObject *parent) + : QObject(parent) +{ +} + +void ResourceScheduler::scheduleFullSync() +{ + Task t; + t.type = SyncAll; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncAll"); + scheduleNext(); +} + +void ResourceScheduler::scheduleCollectionTreeSync() +{ + Task t; + t.type = SyncCollectionTree; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncCollectionTree"); + scheduleNext(); +} + +void ResourceScheduler::scheduleTagSync() +{ + Task t; + t.type = SyncTags; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncTags"); + scheduleNext(); +} + +void ResourceScheduler::scheduleRelationSync() +{ + Task t; + t.type = SyncRelations; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncRelations"); + scheduleNext(); +} + +void ResourceScheduler::scheduleSync(const Collection &col) +{ + Task t; + t.type = SyncCollection; + t.collection = col; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncCollection", QString::number(col.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleAttributesSync(const Collection &collection) +{ + Task t; + t.type = SyncCollectionAttributes; + t.collection = collection; + + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "SyncCollectionAttributes", QString::number(collection.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleItemFetch(const Akonadi::Item &item, const QSet &parts, const QList &msgs, qint64 parentId) + +{ + Task t; + t.type = FetchItem; + t.items << item; + t.itemParts = parts; + t.dbusMsgs = msgs; + t.argument = parentId; + + TaskList &queue = queueForTaskType(t.type); + queue << t; + + signalTaskToTracker(t, "FetchItem", QString::number(item.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleItemsFetch(const Item::List &items, const QSet &parts, const QDBusMessage &msg) +{ + Task t; + t.type = FetchItems; + t.items = items; + t.itemParts = parts; + + // if the current task does already fetch the requested item, break here but + // keep the dbus message, so we can send the reply later on + if (mCurrentTask == t) { + mCurrentTask.dbusMsgs << msg; + return; + } + + // If this task is already in the queue, merge with it. + TaskList &queue = queueForTaskType(t.type); + const int idx = queue.indexOf(t); + if (idx != -1) { + queue[idx].dbusMsgs << msg; + return; + } + + t.dbusMsgs << msg; + queue << t; + + QStringList ids; + ids.reserve(items.size()); + for (const auto &item : items) { + ids.push_back(QString::number(item.id())); + } + signalTaskToTracker(t, "FetchItems", ids.join(QLatin1String(", "))); + scheduleNext(); +} + +void ResourceScheduler::scheduleResourceCollectionDeletion() +{ + Task t; + t.type = DeleteResourceCollection; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "DeleteResourceCollection"); + scheduleNext(); +} + +void ResourceScheduler::scheduleCacheInvalidation(const Collection &collection) +{ + Task t; + t.type = InvalideCacheForCollection; + t.collection = collection; + TaskList &queue = queueForTaskType(t.type); + if (queue.contains(t) || mCurrentTask == t) { + return; + } + queue << t; + signalTaskToTracker(t, "InvalideCacheForCollection", QString::number(collection.id())); + scheduleNext(); +} + +void ResourceScheduler::scheduleChangeReplay() +{ + Task t; + t.type = ChangeReplay; + TaskList &queue = queueForTaskType(t.type); + // see ResourceBase::changeProcessed() for why we do not check for mCurrentTask == t here like in the other tasks + if (queue.contains(t)) { + return; + } + queue << t; + signalTaskToTracker(t, "ChangeReplay"); + scheduleNext(); +} + +void ResourceScheduler::scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover) +{ + Task t; + t.type = RecursiveMoveReplay; + t.collection = movedCollection; + t.argument = QVariant::fromValue(mover); + TaskList &queue = queueForTaskType(t.type); + + if (queue.contains(t) || mCurrentTask == t) { + return; + } + + queue << t; + signalTaskToTracker(t, "RecursiveMoveReplay", QString::number(t.collection.id())); + scheduleNext(); +} + +void Akonadi::ResourceScheduler::scheduleFullSyncCompletion() +{ + Task t; + t.type = SyncAllDone; + TaskList &queue = queueForTaskType(t.type); + // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost + queue << t; + signalTaskToTracker(t, "SyncAllDone"); + scheduleNext(); +} + +void Akonadi::ResourceScheduler::scheduleCollectionTreeSyncCompletion() +{ + Task t; + t.type = SyncCollectionTreeDone; + TaskList &queue = queueForTaskType(t.type); + // no compression here, all this does is emitting a D-Bus signal anyway, and compression can trigger races on the receiver side with the signal being lost + queue << t; + signalTaskToTracker(t, "SyncCollectionTreeDone"); + scheduleNext(); +} + +void Akonadi::ResourceScheduler::scheduleCustomTask(QObject *receiver, + const char *methodName, + const QVariant &argument, + ResourceBase::SchedulePriority priority) +{ + Task t; + t.type = Custom; + t.receiver = receiver; + t.methodName = methodName; + t.argument = argument; + QueueType queueType = GenericTaskQueue; + if (priority == ResourceBase::AfterChangeReplay) { + queueType = AfterChangeReplayQueue; + } else if (priority == ResourceBase::Prepend) { + queueType = PrependTaskQueue; + } + TaskList &queue = mTaskList[queueType]; + + if (queue.contains(t)) { + return; + } + + switch (priority) { + case ResourceBase::Prepend: + queue.prepend(t); + break; + default: + queue.append(t); + break; + } + + signalTaskToTracker(t, "Custom-" + t.methodName); + scheduleNext(); +} + +void ResourceScheduler::taskDone() +{ + if (isEmpty()) { + Q_EMIT status(AgentBase::Idle, i18nc("@info:status Application ready for work", "Ready")); + } + + if (s_resourcetracker) { + const QList argumentList = {QString::number(mCurrentTask.serial), QString()}; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + + mCurrentTask = Task(); + mCurrentTasksQueue = -1; + scheduleNext(); +} + +void ResourceScheduler::itemFetchDone(const QString &msg) +{ + Q_ASSERT(mCurrentTask.type == FetchItem); + + TaskList &queue = queueForTaskType(mCurrentTask.type); + + const qint64 parentId = mCurrentTask.argument.toLongLong(); + // msg is empty, there was no error + if (msg.isEmpty() && !queue.isEmpty()) { + Task &nextTask = queue[0]; + // If the next task is FetchItem too... + if (nextTask.type != mCurrentTask.type || nextTask.argument.toLongLong() != parentId) { + // If the next task is not FetchItem or the next FetchItem task has + // different parentId then this was the last task in the series, so + // send the DBus replies. + mCurrentTask.sendDBusReplies(msg); + } + } else { + // msg was not empty, there was an error. + // remove all subsequent FetchItem tasks with the same parentId + auto iter = queue.begin(); + while (iter != queue.end()) { + if (iter->type != mCurrentTask.type || iter->argument.toLongLong() == parentId) { + iter = queue.erase(iter); + continue; + } else { + break; + } + } + + // ... and send DBus reply with the error message + mCurrentTask.sendDBusReplies(msg); + } + + taskDone(); +} + +void ResourceScheduler::deferTask() +{ + if (mCurrentTask.type == Invalid) { + return; + } + + if (s_resourcetracker) { + const QList argumentList = {QString::number(mCurrentTask.serial), QString()}; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + + Task t = mCurrentTask; + mCurrentTask = Task(); + + Q_ASSERT(mCurrentTasksQueue >= 0 && mCurrentTasksQueue < NQueueCount); + mTaskList[mCurrentTasksQueue].prepend(t); + mCurrentTasksQueue = -1; + + signalTaskToTracker(t, "DeferedTask"); + + scheduleNext(); +} + +bool ResourceScheduler::isEmpty() +{ + for (int i = 0; i < NQueueCount; ++i) { + if (!mTaskList[i].isEmpty()) { + return false; + } + } + return true; +} + +void ResourceScheduler::scheduleNext() +{ + if (mCurrentTask.type != Invalid || isEmpty() || !mOnline) { + return; + } + QTimer::singleShot(0, this, &ResourceScheduler::executeNext); +} + +void ResourceScheduler::executeNext() +{ + if (mCurrentTask.type != Invalid || isEmpty()) { + return; + } + + for (int i = 0; i < NQueueCount; ++i) { + if (!mTaskList[i].isEmpty()) { + mCurrentTask = mTaskList[i].takeFirst(); + mCurrentTasksQueue = i; + break; + } + } + + if (s_resourcetracker) { + const QList argumentList = {QString::number(mCurrentTask.serial)}; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobStarted"), argumentList); + } + + switch (mCurrentTask.type) { + case SyncAll: + Q_EMIT executeFullSync(); + break; + case SyncCollectionTree: + Q_EMIT executeCollectionTreeSync(); + break; + case SyncCollection: + Q_EMIT executeCollectionSync(mCurrentTask.collection); + break; + case SyncCollectionAttributes: + Q_EMIT executeCollectionAttributesSync(mCurrentTask.collection); + break; + case SyncTags: + Q_EMIT executeTagSync(); + break; + case FetchItem: + Q_EMIT executeItemFetch(mCurrentTask.items.at(0), mCurrentTask.itemParts); + break; + case FetchItems: + Q_EMIT executeItemsFetch(mCurrentTask.items, mCurrentTask.itemParts); + break; + case DeleteResourceCollection: + Q_EMIT executeResourceCollectionDeletion(); + break; + case InvalideCacheForCollection: + Q_EMIT executeCacheInvalidation(mCurrentTask.collection); + break; + case ChangeReplay: + Q_EMIT executeChangeReplay(); + break; + case RecursiveMoveReplay: + Q_EMIT executeRecursiveMoveReplay(mCurrentTask.argument.value()); + break; + case SyncAllDone: + Q_EMIT fullSyncComplete(); + break; + case SyncCollectionTreeDone: + Q_EMIT collectionTreeSyncComplete(); + break; + case SyncRelations: + Q_EMIT executeRelationSync(); + break; + case Custom: { + const QByteArray methodSig = mCurrentTask.methodName + QByteArray("(QVariant)"); + const bool hasSlotWithVariant = mCurrentTask.receiver->metaObject()->indexOfMethod(methodSig.constData()) != -1; + bool success = false; + if (hasSlotWithVariant) { + success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData(), Q_ARG(QVariant, mCurrentTask.argument)); + Q_ASSERT_X(success || !mCurrentTask.argument.isValid(), + "ResourceScheduler::executeNext", + "Valid argument was provided but the method wasn't found"); + } + if (!success) { + success = QMetaObject::invokeMethod(mCurrentTask.receiver, mCurrentTask.methodName.constData()); + } + + if (!success) { + qCCritical(AKONADIAGENTBASE_LOG) << "Could not invoke slot" << mCurrentTask.methodName << "on" << mCurrentTask.receiver << "with argument" + << mCurrentTask.argument; + } + break; + } + default: { + qCCritical(AKONADIAGENTBASE_LOG) << "Unhandled task type" << mCurrentTask.type; + dump(); + Q_ASSERT(false); + } + } +} + +ResourceScheduler::Task ResourceScheduler::currentTask() const +{ + return mCurrentTask; +} + +ResourceScheduler::Task &ResourceScheduler::currentTask() +{ + return mCurrentTask; +} + +void ResourceScheduler::setOnline(bool state) +{ + if (mOnline == state) { + return; + } + mOnline = state; + if (mOnline) { + scheduleNext(); + } else { + if (mCurrentTask.type != Invalid) { + // abort running task + queueForTaskType(mCurrentTask.type).prepend(mCurrentTask); + mCurrentTask = Task(); + mCurrentTasksQueue = -1; + } + // abort pending synchronous tasks, might take longer until the resource goes online again + TaskList &itemFetchQueue = queueForTaskType(FetchItem); + qint64 parentId = -1; + Task lastTask; + for (QList::iterator it = itemFetchQueue.begin(); it != itemFetchQueue.end();) { + if ((*it).type == FetchItem) { + qint64 idx = it->argument.toLongLong(); + if (parentId == -1) { + parentId = idx; + } + if (idx != parentId) { + // Only emit the DBus reply once we reach the last taskwith the + // same "idx" + lastTask.sendDBusReplies(i18nc("@info", "Job canceled.")); + parentId = idx; + } + lastTask = (*it); + it = itemFetchQueue.erase(it); + if (s_resourcetracker) { + const QList argumentList = {QString::number(mCurrentTask.serial), i18nc("@info", "Job canceled.")}; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + } else { + ++it; + } + } + } +} + +void ResourceScheduler::signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString) +{ + // if there's a job tracer running, tell it about the new job + if (!s_resourcetracker) { + const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier(); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) { + s_resourcetracker = new QDBusInterface(QLatin1String("org.kde.akonadiconsole") + suffix, + QStringLiteral("/resourcesJobtracker"), + QStringLiteral("org.freedesktop.Akonadi.JobTracker"), + QDBusConnection::sessionBus(), + nullptr); + } + } + + if (s_resourcetracker) { + const QList argumentList = QList() << static_cast(parent())->identifier() // "session" (in our case resource) + << QString::number(task.serial) // "job" + << QString() // "parent job" + << QString::fromLatin1(taskType) // "job type" + << debugString // "job debugging string" + ; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList); + } +} + +void ResourceScheduler::collectionRemoved(const Akonadi::Collection &collection) +{ + if (!collection.isValid()) { // should not happen, but you never know... + return; + } + TaskList &queue = queueForTaskType(SyncCollection); + for (QList::iterator it = queue.begin(); it != queue.end();) { + if ((*it).type == SyncCollection && (*it).collection == collection) { + it = queue.erase(it); + qCDebug(AKONADIAGENTBASE_LOG) << " erasing"; + } else { + ++it; + } + } +} + +void ResourceScheduler::Task::sendDBusReplies(const QString &errorMsg) +{ + for (const QDBusMessage &msg : std::as_const(dbusMsgs)) { + qCDebug(AKONADIAGENTBASE_LOG) << "Sending dbus reply for method" << methodName << "with error" << errorMsg; + QDBusMessage reply; + if (!errorMsg.isEmpty()) { + reply = msg.createErrorReply(QDBusError::Failed, errorMsg); + } else if (msg.member() == QLatin1String("requestItemDelivery")) { + reply = msg.createReply(); + } else if (msg.member().isEmpty()) { + continue; // unittest calls scheduleItemFetch with empty QDBusMessage + } else { + qCCritical(AKONADIAGENTBASE_LOG) << "ResourceScheduler: got unexpected method name :" << msg.member(); + } + QDBusConnection::sessionBus().send(reply); + } +} + +ResourceScheduler::QueueType ResourceScheduler::queueTypeForTaskType(TaskType type) +{ + switch (type) { + case ChangeReplay: + case RecursiveMoveReplay: + return ChangeReplayQueue; + case FetchItem: + case FetchItems: + case SyncCollectionAttributes: + return UserActionQueue; + default: + return GenericTaskQueue; + } +} + +ResourceScheduler::TaskList &ResourceScheduler::queueForTaskType(TaskType type) +{ + const QueueType qt = queueTypeForTaskType(type); + return mTaskList[qt]; +} + +void ResourceScheduler::dump() const +{ + qCDebug(AKONADIAGENTBASE_LOG) << dumpToString(); +} + +QString ResourceScheduler::dumpToString() const +{ + QString ret; + QTextStream str(&ret); + str << "ResourceScheduler: " << (mOnline ? "Online" : "Offline") << '\n'; + str << " current task: " << mCurrentTask << '\n'; + for (int i = 0; i < NQueueCount; ++i) { + const TaskList &queue = mTaskList[i]; + if (queue.isEmpty()) { + str << " queue " << i << " is empty" << '\n'; + } else { + str << " queue " << i << " " << queue.size() << " tasks:\n"; + const QList::const_iterator queueEnd(queue.constEnd()); + for (QList::const_iterator it = queue.constBegin(); it != queueEnd; ++it) { + str << " " << (*it) << '\n'; + } + } + } + str.flush(); + return ret; +} + +void ResourceScheduler::clear() +{ + qCDebug(AKONADIAGENTBASE_LOG) << "Clearing ResourceScheduler queues:"; + for (int i = 0; i < NQueueCount; ++i) { + TaskList &queue = mTaskList[i]; + queue.clear(); + } + mCurrentTask = Task(); + mCurrentTasksQueue = -1; +} + +void Akonadi::ResourceScheduler::cancelQueues() +{ + for (int i = 0; i < NQueueCount; ++i) { + TaskList &queue = mTaskList[i]; + if (s_resourcetracker) { + for (const Task &t : queue) { + QList argumentList{QString::number(t.serial), QString()}; + s_resourcetracker->asyncCallWithArgumentList(QStringLiteral("jobEnded"), argumentList); + } + } + queue.clear(); + } +} + +static const char s_taskTypes[][27] = {"Invalid (no task)", + "SyncAll", + "SyncCollectionTree", + "SyncCollection", + "SyncCollectionAttributes", + "SyncTags", + "FetchItem", + "FetchItems", + "ChangeReplay", + "RecursiveMoveReplay", + "DeleteResourceCollection", + "InvalideCacheForCollection", + "SyncAllDone", + "SyncCollectionTreeDone", + "SyncRelations", + "Custom"}; + +QTextStream &Akonadi::operator<<(QTextStream &d, const ResourceScheduler::Task &task) +{ + d << task.serial << " " << s_taskTypes[task.type] << " "; + if (task.type != ResourceScheduler::Invalid) { + if (task.collection.isValid()) { + d << "collection " << task.collection.id() << " "; + } + if (!task.items.isEmpty()) { + QStringList ids; + ids.reserve(task.items.size()); + for (const auto &item : std::as_const(task.items)) { + ids.push_back(QString::number(item.id())); + } + d << "items " << ids.join(QLatin1String(", ")) << " "; + } + if (!task.methodName.isEmpty()) { + d << task.methodName << " " << task.argument.toString(); + } + } + return d; +} + +QDebug Akonadi::operator<<(QDebug d, const ResourceScheduler::Task &task) +{ + QString s; + QTextStream str(&s); + str << task; + d << s; + return d; +} + +/// @endcond + +#include "moc_resourcescheduler_p.cpp" diff --git a/src/agentbase/resourcescheduler_p.h b/src/agentbase/resourcescheduler_p.h new file mode 100644 index 0000000..b7d0e5a --- /dev/null +++ b/src/agentbase/resourcescheduler_p.h @@ -0,0 +1,297 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentbase.h" +#include "collection.h" +#include "item.h" +#include "resourcebase.h" + +#include +#include + +namespace Akonadi +{ +class RecursiveMover; + +/// @cond PRIVATE + +/** + @internal + + Manages synchronization and fetch requests for a resource. + + @todo Attach to the ResourceBase Monitor, +*/ +class ResourceScheduler : public QObject +{ + Q_OBJECT + +public: + // If you change this enum, keep s_taskTypes in sync in resourcescheduler.cpp + enum TaskType { + Invalid, + SyncAll, + SyncCollectionTree, + SyncCollection, + SyncCollectionAttributes, + SyncTags, + FetchItem, + FetchItems, + ChangeReplay, + RecursiveMoveReplay, + DeleteResourceCollection, + InvalideCacheForCollection, + SyncAllDone, + SyncCollectionTreeDone, + SyncRelations, + Custom + }; + + class Task + { + static qint64 latestSerial; + + public: + Task() + : serial(++latestSerial) + , type(Invalid) + { + } + qint64 serial; + TaskType type; + Collection collection; + QVector items; + QSet itemParts; + QList dbusMsgs; + QObject *receiver = nullptr; + QByteArray methodName; + QVariant argument; + + void sendDBusReplies(const QString &errorMsg); + + bool operator==(const Task &other) const + { + return type == other.type && (collection == other.collection || (!collection.isValid() && !other.collection.isValid())) && items == other.items + && itemParts == other.itemParts && receiver == other.receiver && methodName == other.methodName && argument == other.argument; + } + }; + + explicit ResourceScheduler(QObject *parent = nullptr); + + /** + Schedules a full synchronization. + */ + void scheduleFullSync(); + + /** + Schedules a collection tree sync. + */ + void scheduleCollectionTreeSync(); + + /** + Schedules the synchronization of a single collection. + @param col The collection to synchronize. + */ + void scheduleSync(const Collection &col); + + /** + Schedules synchronizing the attributes of a single collection. + @param collection The collection to synchronize attributes from. + */ + void scheduleAttributesSync(const Collection &collection); + + void scheduleTagSync(); + void scheduleRelationSync(); + + /** + Schedules fetching of a single PIM item. + + This task is only ever used if the resource still uses the old deprecated + retrieveItem() (instead of retrieveItems(Item::List)) method. This task has + a special meaning to the scheduler and instead of replying to the DBus message + after the single @p item is retrieved, the items are accumulated until all + tasks from the same messages are fetched. + + @param items The items to fetch. + @param parts List of names of the parts of the item to fetch. + @param msg The associated D-Bus message. + @param parentId ID of the original ItemsFetch task that this task was created from. + We can use this ID to group the tasks together + */ + void scheduleItemFetch(const Item &item, const QSet &parts, const QList &msgs, const qint64 parentId); + + /** + Schedules batch-fetching of PIM items. + @param items The items to fetch. + @param parts List of names of the parts of the item to fetch. + @param msg The associated D-Bus message. + */ + void scheduleItemsFetch(const Item::List &item, const QSet &parts, const QDBusMessage &msg); + + /** + Schedules deletion of the resource collection. + This method is used to implement the ResourceBase::clearCache() functionality. + */ + void scheduleResourceCollectionDeletion(); + + /** + * Schedule cache invalidation for @p collection. + * @see ResourceBase::invalidateCache() + */ + void scheduleCacheInvalidation(const Collection &collection); + + /** + Insert synchronization completion marker into the task queue. + */ + void scheduleFullSyncCompletion(); + + /** + Insert collection tree synchronization completion marker into the task queue. + */ + void scheduleCollectionTreeSyncCompletion(); + + /** + Insert a custom task. + @param methodName The method name, without signature, do not use the SLOT() macro + */ + void + scheduleCustomTask(QObject *receiver, const char *methodName, const QVariant &argument, ResourceBase::SchedulePriority priority = ResourceBase::Append); + + /** + * Schedule a recursive move replay. + */ + void scheduleMoveReplay(const Collection &movedCollection, RecursiveMover *mover); + + /** + Returns true if no tasks are running or in the queue. + */ + bool isEmpty(); + + /** + Returns the current task. + */ + Task currentTask() const; + + Task ¤tTask(); + + /** + Sets the online state. + */ + void setOnline(bool state); + + /** + Print debug output showing the state of the scheduler. + */ + void dump() const; + /** + Print debug output showing the state of the scheduler. + */ + QString dumpToString() const; + + /** + Clear the state of the scheduler. Warning: this is intended to be + used purely in debugging scenarios, as it might cause loss of uncommitted + local changes. + */ + void clear(); + + /** + Cancel everything the scheduler has still in queue. Keep the current task running though. + It can be seen as a less aggressive clear() used when the user requested the resource to + abort its activities. It properly cancel all the tasks in there. + */ + void cancelQueues(); + +public Q_SLOTS: + /** + Schedules replaying changes. + */ + void scheduleChangeReplay(); + + /** + The current task has been finished + */ + void taskDone(); + + /** + Like taskDone(), but special case for ItemFetch task + */ + void itemFetchDone(const QString &msg); + + /** + The current task can't be finished now and will be rescheduled later + */ + void deferTask(); + + /** + Remove tasks that affect @p collection. + */ + void collectionRemoved(const Akonadi::Collection &collection); + +Q_SIGNALS: + void executeFullSync(); + void executeCollectionAttributesSync(const Akonadi::Collection &col); + void executeCollectionSync(const Akonadi::Collection &col); + void executeCollectionTreeSync(); + void executeTagSync(); + void executeRelationSync(); + void executeItemFetch(const Akonadi::Item &item, const QSet &parts); + void executeItemsFetch(const QVector &items, const QSet &parts); + void executeResourceCollectionDeletion(); + void executeCacheInvalidation(const Akonadi::Collection &collection); + void executeChangeReplay(); + void executeRecursiveMoveReplay(RecursiveMover *mover); + void collectionTreeSyncComplete(); + void fullSyncComplete(); + void status(int status, const QString &message = QString()); + +private Q_SLOTS: + void scheduleNext(); + void executeNext(); + +private: + void signalTaskToTracker(const Task &task, const QByteArray &taskType, const QString &debugString = QString()); + + // We have a number of task queues, by order of priority. + // * PrependTaskQueue is for deferring the current task + // * ChangeReplay must be first: + // change replays have to happen before we pull changes from the backend, otherwise + // we will overwrite our still unsaved local changes if the backend can't do + // incremental retrieval + // + // * then the stuff that is "immediately after change replay", like writeFile calls. + // * then tasks which the user is waiting for, like ItemFetch (clicking on a mail) or + // SyncCollectionAttributes (folder properties dialog in kmail) + // * then everything else (which includes the background email checking, which can take quite some time). + enum QueueType { + PrependTaskQueue, + ChangeReplayQueue, // one task at most + AfterChangeReplayQueue, // also one task at most, currently + UserActionQueue, + GenericTaskQueue, + NQueueCount + }; + using TaskList = QList; + + static QueueType queueTypeForTaskType(TaskType type); + TaskList &queueForTaskType(TaskType type); + + TaskList mTaskList[NQueueCount]; + + Task mCurrentTask; + int mCurrentTasksQueue = -1; // queue mCurrentTask came from + bool mOnline = false; +}; + +QDebug operator<<(QDebug, const ResourceScheduler::Task &task); +QTextStream &operator<<(QTextStream &, const ResourceScheduler::Task &task); + +/// @endcond + +} + diff --git a/src/agentbase/resourcesettings.cpp b/src/agentbase/resourcesettings.cpp new file mode 100644 index 0000000..1f1fa05 --- /dev/null +++ b/src/agentbase/resourcesettings.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2010-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resourcesettings.h" + +using namespace Akonadi; + +ResourceSettings *ResourceSettings::mSelf = nullptr; + +ResourceSettings *ResourceSettings::self() +{ + if (!mSelf) { + mSelf = new ResourceSettings(); + mSelf->load(); + } + + return mSelf; +} + +ResourceSettings::ResourceSettings() +{ +} + +ResourceSettings::~ResourceSettings() +{ +} diff --git a/src/agentbase/resourcesettings.h b/src/agentbase/resourcesettings.h new file mode 100644 index 0000000..29eef94 --- /dev/null +++ b/src/agentbase/resourcesettings.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2010-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiagentbase_export.h" +#include "resourcebasesettings.h" + +namespace Akonadi +{ +class AKONADIAGENTBASE_EXPORT ResourceSettings : public Akonadi::ResourceBaseSettings // krazy:exclude=dpointer +{ + Q_OBJECT +public: + static ResourceSettings *self(); + +private: + ResourceSettings(); + ~ResourceSettings() override; + static ResourceSettings *mSelf; +}; + +} + diff --git a/src/agentbase/transportresourcebase.cpp b/src/agentbase/transportresourcebase.cpp new file mode 100644 index 0000000..2bee720 --- /dev/null +++ b/src/agentbase/transportresourcebase.cpp @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "transportresourcebase.h" +#include "transportresourcebase_p.h" + +#include "transportadaptor.h" +#include + +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include + +using namespace Akonadi; + +TransportResourceBasePrivate::TransportResourceBasePrivate(TransportResourceBase *qq) + : q(qq) +{ + new Akonadi__TransportAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Transport"), this, QDBusConnection::ExportAdaptors); +} + +void TransportResourceBasePrivate::send(Item::Id id) +{ + auto job = new ItemFetchJob(Item(id)); + job->fetchScope().fetchFullPayload(); + job->setProperty("id", QVariant(id)); + connect(job, &KJob::result, this, &TransportResourceBasePrivate::fetchResult); +} + +void TransportResourceBasePrivate::fetchResult(KJob *job) +{ + if (job->error()) { + const Item::Id id = job->property("id").toLongLong(); + Q_EMIT transportResult(id, static_cast(TransportResourceBase::TransportFailed), job->errorText()); + return; + } + + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + const Item item = fetchJob->items().at(0); + q->sendItem(item); +} + +TransportResourceBase::TransportResourceBase() + : d(new TransportResourceBasePrivate(this)) +{ +} + +TransportResourceBase::~TransportResourceBase() +{ + delete d; +} + +void TransportResourceBase::itemSent(const Item &item, TransportResult result, const QString &message) +{ + Q_EMIT d->transportResult(item.id(), static_cast(result), message); +} + +#include "moc_transportresourcebase_p.cpp" diff --git a/src/agentbase/transportresourcebase.h b/src/agentbase/transportresourcebase.h new file mode 100644 index 0000000..dc3d980 --- /dev/null +++ b/src/agentbase/transportresourcebase.h @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiagentbase_export.h" +#include "item.h" + +#include + +namespace Akonadi +{ +class TransportResourceBasePrivate; + +/** + * @short Resource implementing mail transport capability. + * + * This class allows a resource to provide mail transport (i.e. sending + * mail). A resource than can provide mail transport inherits from both + * ResourceBase and TransportResourceBase, implements the virtual method + * sendItem(), and calls itemSent() when finished sending. + * + * The resource must also have the "MailTransport" capability flag. For example + * the desktop file may contain: + \code + X-Akonadi-Capabilities=Resource,MailTransport + \endcode + * + * For an example of a transport-enabled resource, see + * kdepim/runtime/resources/mailtransport_dummy + * + * @author Constantin Berzan + * @since 4.4 + */ +class AKONADIAGENTBASE_EXPORT TransportResourceBase +{ +public: + /** + * Creates a new transport resource base. + */ + TransportResourceBase(); + + /** + * Destroys the transport resource base. + */ + virtual ~TransportResourceBase(); + + /** + * Describes the result of the transport process. + */ + enum TransportResult { + TransportSucceeded, ///< The transport process succeeded. + TransportFailed ///< The transport process failed. + }; + + /** + * This method is called when the given @p item shall be send. + * When the sending is done or an error occurred during + * sending, call itemSent() with the appropriate result flag. + * + * @param item The message item to be send. + * @see itemSent(). + */ + virtual void sendItem(const Akonadi::Item &item) = 0; + + /** + * This method marks the sending of the passed @p item + * as finished. + * + * @param item The item that was sent. + * @param result The result that indicates whether the sending + * was successful or not. + * @param message An optional text explanation of the result. + * @see Transport. + */ + void itemSent(const Akonadi::Item &item, TransportResult result, const QString &message = QString()); + +private: + /// @cond PRIVATE + TransportResourceBasePrivate *const d; + /// @endcond +}; + +} + diff --git a/src/agentbase/transportresourcebase_p.h b/src/agentbase/transportresourcebase_p.h new file mode 100644 index 0000000..879bacc --- /dev/null +++ b/src/agentbase/transportresourcebase_p.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "transportresourcebase.h" + +#include + +class Akonadi__TransportAdaptor; + +namespace Akonadi +{ +class TransportResourceBase; + +/** + @internal + This class hosts the D-Bus adaptor for TransportResourceBase. +*/ +class TransportResourceBasePrivate : public QObject +{ + Q_OBJECT +public: + explicit TransportResourceBasePrivate(TransportResourceBase *qq); + +Q_SIGNALS: + /** + * Emitted when an item has been sent. + * @param item The id of the item that was sent. + * @param result The result of the sending operation. + * @param message An optional textual explanation of the result. + * @since 4.4 + */ + void transportResult(qlonglong item, int result, const QString &message); // D-Bus signal + +private Q_SLOTS: + void fetchResult(KJob *job); + +private: + friend class TransportResourceBase; + friend class ::Akonadi__TransportAdaptor; + + void send(Akonadi::Item::Id message); // D-Bus call + + TransportResourceBase *const q; +}; + +} // namespace Akonadi + diff --git a/src/agentserver/CMakeLists.txt b/src/agentserver/CMakeLists.txt new file mode 100644 index 0000000..507a618 --- /dev/null +++ b/src/agentserver/CMakeLists.txt @@ -0,0 +1,56 @@ +add_executable(akonadi_agent_server) +# Agent server +target_sources(akonadi_agent_server PRIVATE + agentpluginloader.cpp + agentserver.cpp + agentthread.cpp + main.cpp +) + +ecm_qt_declare_logging_category(akonadi_agent_server HEADER akonadiagentserver_debug.h IDENTIFIER AKONADIAGENTSERVER_LOG CATEGORY_NAME org.kde.pim.akonadiagentserver + DESCRIPTION "akonadi (Akonadi Agent Server)" + OLD_CATEGORY_NAMES log_akonadiagentserver + EXPORT AKONADI + ) + + +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(akonadi_agent_server PROPERTIES UNITY_BUILD ON) +endif() + +set_target_properties(akonadi_agent_server PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(akonadi_agent_server + akonadi_shared + KF5AkonadiPrivate + Qt::Core + Qt::DBus +) + +# Agent plugin launcher +add_executable(akonadi_agent_launcher) +target_sources(akonadi_agent_launcher PRIVATE + agentpluginloader.cpp + agentlauncher.cpp + akonadiagentserver_debug.cpp +) + +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(akonadi_agent_launcher PROPERTIES UNITY_BUILD ON) +endif() + +set_target_properties(akonadi_agent_launcher PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(akonadi_agent_launcher + akonadi_shared + KF5AkonadiPrivate + Qt::Core + Qt::Widgets +) + +# Install both helper apps. +install(TARGETS akonadi_agent_launcher + DESTINATION ${BIN_INSTALL_DIR}) + +install(TARGETS akonadi_agent_server + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/agentserver/TODO b/src/agentserver/TODO new file mode 100644 index 0000000..fb58b82 --- /dev/null +++ b/src/agentserver/TODO @@ -0,0 +1,3 @@ +* When the AgentServer process crashes and is restarted by ProcessControl, + somehow the agents/resources that where running must be restarted as + well. diff --git a/src/agentserver/agentlauncher.cpp b/src/agentserver/agentlauncher.cpp new file mode 100644 index 0000000..3bfa3ba --- /dev/null +++ b/src/agentserver/agentlauncher.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentpluginloader.h" +#include "akonadiagentserver_debug.h" + +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); + + if (app.arguments().size() != 3) { // Expected usage: ./agent_launcher ${plugin_name} ${identifier} + qCDebug(AKONADIAGENTSERVER_LOG) << "Invalid usage: expected: ./agent_launcher pluginName agentIdentifier"; + return 1; + } + + const QString agentPluginName = app.arguments().at(1); + const QString agentIdentifier = app.arguments().at(2); + + AgentPluginLoader loader; + QPluginLoader *factory = loader.load(agentPluginName); + if (!factory) { + return 1; + } + + QObject *instance = nullptr; + const bool invokeSucceeded = QMetaObject::invokeMethod(factory->instance(), + "createInstance", + Qt::DirectConnection, + Q_RETURN_ARG(QObject *, instance), + Q_ARG(QString, agentIdentifier)); + if (invokeSucceeded) { + qCDebug(AKONADIAGENTSERVER_LOG) << "Agent instance created in separate process."; + } else { + qCDebug(AKONADIAGENTSERVER_LOG) << "Agent instance creation in separate process failed"; + return 2; + } + + const int rv = app.exec(); + delete instance; + return rv; +} diff --git a/src/agentserver/agentpluginloader.cpp b/src/agentserver/agentpluginloader.cpp new file mode 100644 index 0000000..4ec11ce --- /dev/null +++ b/src/agentserver/agentpluginloader.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "agentpluginloader.h" +#include "akonadiagentserver_debug.h" + +#include + +#include + +AgentPluginLoader::AgentPluginLoader() +{ +} + +AgentPluginLoader::~AgentPluginLoader() +{ + qDeleteAll(m_pluginLoaders); + m_pluginLoaders.clear(); +} + +QPluginLoader *AgentPluginLoader::load(const QString &pluginName) +{ + QPluginLoader *loader = m_pluginLoaders.value(pluginName); + if (loader) { + return loader; + } else { + loader = new QPluginLoader(pluginName); + if (!loader->load()) { + qCWarning(AKONADIAGENTSERVER_LOG) << "Failed to load agent: " << loader->errorString(); + delete loader; + return nullptr; + } + m_pluginLoaders.insert(pluginName, loader); + return loader; + } +} diff --git a/src/agentserver/agentpluginloader.h b/src/agentserver/agentpluginloader.h new file mode 100644 index 0000000..ec1d635 --- /dev/null +++ b/src/agentserver/agentpluginloader.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include +class QPluginLoader; + +class AgentPluginLoader +{ +public: + AgentPluginLoader(); + + /** + Deletes all instantiated QPluginLoaders. + */ + ~AgentPluginLoader(); + + /** + Returns the loader for plugins with @p pluginName. Callers must not + take ownership over the returned loader. Loaders will be unloaded and deleted + when the AgentPluginLoader goes out of scope/gets deleted. + + @return the plugin for @p pluginName or 0 if the plugin is not found. + */ + Q_REQUIRED_RESULT QPluginLoader *load(const QString &pluginName); + +private: + Q_DISABLE_COPY(AgentPluginLoader) + QHash m_pluginLoaders; +}; + diff --git a/src/agentserver/agentserver.cpp b/src/agentserver/agentserver.cpp new file mode 100644 index 0000000..1b03394 --- /dev/null +++ b/src/agentserver/agentserver.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentserver.h" +#include "agentthread.h" +#include "akonadiagentserver_debug.h" + +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +AgentServer::AgentServer(QObject *parent) + : QObject(parent) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral(AKONADI_DBUS_AGENTSERVER_PATH), this, QDBusConnection::ExportScriptableSlots); +} + +AgentServer::~AgentServer() +{ + qCDebug(AKONADIAGENTSERVER_LOG) << Q_FUNC_INFO; + if (!m_quiting) { + quit(); + } +} + +void AgentServer::agentInstanceConfigure(const QString &identifier, qlonglong windowId) +{ + m_configureQueue.enqueue(ConfigureInfo(identifier, windowId)); + if (!m_processingConfigureRequests) { // Start processing the requests if needed. + QTimer::singleShot(0, this, &AgentServer::processConfigureRequest); + } +} + +bool AgentServer::started(const QString &identifier) const +{ + return m_agents.contains(identifier); +} + +void AgentServer::startAgent(const QString &identifier, const QString &typeIdentifier, const QString &fileName) +{ + qCDebug(AKONADIAGENTSERVER_LOG) << "Starting agent" << identifier << "of type" << typeIdentifier << "(file:" << fileName << ")"; + + // First try to load it staticly + const QObjectList objList = QPluginLoader::staticInstances(); + for (QObject *plugin : objList) { + if (plugin->objectName() == fileName) { + auto thread = new AgentThread(identifier, plugin, this); + m_agents.insert(identifier, thread); + thread->start(); + return; + } + } + + QPluginLoader *loader = m_agentLoader.load(fileName); + if (!loader) { + return; // No plugin found, debug output in AgentLoader. + } + + Q_ASSERT(loader->isLoaded()); + + auto thread = new AgentThread(identifier, loader->instance(), this); + m_agents.insert(identifier, thread); + thread->start(); +} + +void AgentServer::stopAgent(const QString &identifier) +{ + AgentThread *thread = m_agents.take(identifier); + if (thread) { + thread->quit(); + thread->wait(); + delete thread; + } +} + +void AgentServer::quit() +{ + Q_ASSERT(!m_quiting); + m_quiting = true; + + QMutableHashIterator it(m_agents); + while (it.hasNext()) { + stopAgent(it.next().key()); + } + + QCoreApplication::instance()->quit(); +} + +void AgentServer::processConfigureRequest() +{ + if (m_processingConfigureRequests) { + return; // Protect against reentrancy + } + + m_processingConfigureRequests = true; + + while (!m_configureQueue.empty()) { + const ConfigureInfo info = m_configureQueue.dequeue(); + // call configure on the agent with id info.first for windowId info.second. + Q_ASSERT(m_agents.contains(info.first)); + AgentThread *thread = m_agents.value(info.first); + thread->configure(info.second); + } + + m_processingConfigureRequests = false; +} diff --git a/src/agentserver/agentserver.h b/src/agentserver/agentserver.h new file mode 100644 index 0000000..9640d2a --- /dev/null +++ b/src/agentserver/agentserver.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentpluginloader.h" + +#include +#include +#include + +namespace Akonadi +{ +class AgentThread; + +class AgentServer : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.AgentServer") + + using ConfigureInfo = QPair; + +public: + explicit AgentServer(QObject *parent = nullptr); + ~AgentServer(); + +public Q_SLOTS: + Q_SCRIPTABLE void agentInstanceConfigure(const QString &identifier, qlonglong windowId); + Q_SCRIPTABLE bool started(const QString &identifier) const; + Q_SCRIPTABLE void startAgent(const QString &identifier, const QString &typeIdentifier, const QString &fileName); + Q_SCRIPTABLE void stopAgent(const QString &identifier); + Q_SCRIPTABLE void quit(); + +private Q_SLOTS: + void processConfigureRequest(); + +private: + QHash m_agents; + QQueue m_configureQueue; + AgentPluginLoader m_agentLoader; + bool m_processingConfigureRequests = false; + bool m_quiting = false; +}; + +} + diff --git a/src/agentserver/agentthread.cpp b/src/agentserver/agentthread.cpp new file mode 100644 index 0000000..f240891 --- /dev/null +++ b/src/agentserver/agentthread.cpp @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentthread.h" +#include "akonadiagentserver_debug.h" + +#include +#include + +using namespace Akonadi; + +AgentThread::AgentThread(const QString &identifier, QObject *factory, QObject *parent) + : QThread(parent) + , m_identifier(identifier) + , m_factory(factory) +{ +} + +void AgentThread::run() +{ + const bool invokeSucceeded = + QMetaObject::invokeMethod(m_factory, "createInstance", Qt::DirectConnection, Q_RETURN_ARG(QObject *, m_instance), Q_ARG(QString, m_identifier)); + if (invokeSucceeded) { + qCDebug(AKONADIAGENTSERVER_LOG) << Q_FUNC_INFO << "agent instance created: " << m_instance; + } else { + qCDebug(AKONADIAGENTSERVER_LOG) << Q_FUNC_INFO << "agent instance creation failed"; + } + + exec(); + delete m_instance; + m_instance = nullptr; +} + +void AgentThread::configure(qlonglong windowId) +{ + QMetaObject::invokeMethod(m_instance, "configure", Qt::DirectConnection, Q_ARG(quintptr, (quintptr)windowId)); +} diff --git a/src/agentserver/agentthread.h b/src/agentserver/agentthread.h new file mode 100644 index 0000000..0849ccb --- /dev/null +++ b/src/agentserver/agentthread.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +/** + * @short A class that encapsulates an agent instance inside a thread. + */ +class AgentThread : public QThread +{ + Q_OBJECT + +public: + /** + * Creates a new agent thread. + * + * @param identifier The unique identifier for this agent + * @param factory The factory object that creates the agent instance. + * @param parent The parent object. + */ + AgentThread(const QString &identifier, QObject *factory, QObject *parent = nullptr); + + /** + * Configures the agent. + * + * @param windowId The parent window id for the config dialog. + */ + void configure(qlonglong windowId); + +protected: + void run() override; + +private: + const QString m_identifier; + QObject *const m_factory; + QObject *m_instance = nullptr; +}; + +} + diff --git a/src/agentserver/main.cpp b/src/agentserver/main.cpp new file mode 100644 index 0000000..de2ac55 --- /dev/null +++ b/src/agentserver/main.cpp @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentserver.h" +#include "akonadiagentserver_debug.h" + +#include + +#include + +#include +#include + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv, AKONADIAGENTSERVER_LOG()); + app.setDescription(QStringLiteral("Akonadi Agent Server\nDo not run manually, use 'akonadictl' instead to start/stop Akonadi.")); + app.parseCommandLine(); + + if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock))) { + qCCritical(AKONADIAGENTSERVER_LOG) << "Akonadi control process not found - aborting."; + qFatal("If you started akonadi_agent_server manually, try 'akonadictl start' instead."); + } + + new Akonadi::AgentServer(&app); + + if (!QDBusConnection::sessionBus().registerService(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer))) { + qFatal("Unable to connect to dbus service: %s", qPrintable(QDBusConnection::sessionBus().lastError().message())); + } + + return app.exec(); +} diff --git a/src/akonadicontrol/CMakeLists.txt b/src/akonadicontrol/CMakeLists.txt new file mode 100644 index 0000000..122467c --- /dev/null +++ b/src/akonadicontrol/CMakeLists.txt @@ -0,0 +1,76 @@ +########### next target ############### +add_executable(akonadi_control ) + +if (WITH_ACCOUNTS) + target_sources(akonadi_control PRIVATE accountsintegration.cpp) +endif() + +ecm_qt_declare_logging_category(akonadi_control HEADER akonadicontrol_debug.h IDENTIFIER AKONADICONTROL_LOG CATEGORY_NAME org.kde.pim.akonadicontrol + DESCRIPTION "akonadi (Akonadi Control)" + EXPORT AKONADI + ) + +qt_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml agentmanager.h AgentManager) +qt_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml controlmanager.h ControlManager) +qt_add_dbus_adaptor(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml agentmanager.h AgentManager) +qt_add_dbus_interfaces(control_SRCS + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Server.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.kde.Akonadi.Accounts.xml +) +qt_add_dbus_interface(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml resource_manager) +qt_add_dbus_interface(control_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml preprocessor_manager) + +target_sources(akonadi_control PRIVATE + agenttype.cpp + agentinstance.cpp + agentbrokeninstance.cpp + agentprocessinstance.cpp + agentthreadinstance.cpp + agentmanager.cpp + controlmanager.cpp + main.cpp + processcontrol.cpp + ${control_SRCS} +) +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(akonadi_control PROPERTIES UNITY_BUILD ON) +endif() + +target_include_directories(akonadi_control BEFORE PUBLIC $) + +set_target_properties(akonadi_control PROPERTIES MACOSX_BUNDLE FALSE) +set_target_properties(akonadi_control PROPERTIES OUTPUT_NAME akonadi_control) + +if (WIN32) + set_target_properties(akonadi_control PROPERTIES WIN32_EXECUTABLE TRUE) + target_link_libraries(akonadi_control ${QT_QTMAIN_LIBRARY}) +endif() + +target_link_libraries(akonadi_control + akonadi_shared + KF5AkonadiPrivate + KF5::CoreAddons + KF5::ConfigCore + Qt::Core + Qt::DBus + Qt::Gui +) + +if (WITH_ACCOUNTS) + target_include_directories(akonadi_control PRIVATE ${ACCOUNTSQT_INCLUDE_DIRS}) + # We need Qt::Xml because the Accounts framework leaks QDocument includes into public interface + target_link_libraries(akonadi_control ${ACCOUNTSQT_LIBRARIES} Qt::Xml) +endif() + +install(TARGETS akonadi_control + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) + +configure_file(org.freedesktop.Akonadi.Control.service.cmake ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.Control.service) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.Control.service + DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}) diff --git a/src/akonadicontrol/accountsintegration.cpp b/src/akonadicontrol/accountsintegration.cpp new file mode 100644 index 0000000..e3d73a7 --- /dev/null +++ b/src/akonadicontrol/accountsintegration.cpp @@ -0,0 +1,159 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2019 Daniel Vrátil * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "accountsintegration.h" +#include "accountsinterface.h" +#include "agentmanager.h" +#include "akonadicontrol_debug.h" + +#include + +#include +#include + +#include +#include + +using namespace Akonadi; +using namespace std::chrono_literals; + +namespace +{ +const auto akonadiAgentType = QStringLiteral("akonadi/agentType"); + +} + +AccountsIntegration::AccountsIntegration(AgentManager &agentManager) + : mAgentManager(agentManager) +{ + connect(&mAccountsManager, &Accounts::Manager::accountCreated, this, &AccountsIntegration::onAccountAdded); + connect(&mAccountsManager, &Accounts::Manager::accountRemoved, this, &AccountsIntegration::onAccountRemoved); + + const auto accounts = mAccountsManager.accountList(); + for (const auto account : accounts) { + connect(mAccountsManager.account(account), &Accounts::Account::enabledChanged, this, &AccountsIntegration::onAccountServiceEnabled); + } +} + +std::optional AccountsIntegration::agentForAccount(const QString &agentType, Accounts::AccountId accountId) const +{ + const auto instances = mAgentManager.agentInstances(); + for (const auto &identifier : instances) { + if (mAgentManager.agentInstanceType(identifier) == agentType) { + const auto serviceName = Akonadi::DBus::agentServiceName(identifier, Akonadi::DBus::Resource); + org::kde::Akonadi::Accounts accountsIface(serviceName, QStringLiteral("/Accounts"), QDBusConnection::sessionBus()); + if (!accountsIface.isValid()) { + continue; + } + + if (accountsIface.getAccountId() == accountId) { + return identifier; + } + } + } + return std::nullopt; +} + +void AccountsIntegration::configureAgentInstance(const QString &identifier, Accounts::AccountId accountId, int attempt) +{ + const auto serviceName = Akonadi::DBus::agentServiceName(identifier, Akonadi::DBus::Resource); + org::kde::Akonadi::Accounts accountsIface(serviceName, QStringLiteral("/Accounts"), QDBusConnection::sessionBus()); + if (!accountsIface.isValid()) { + if (attempt >= 3) { + qCWarning(AKONADICONTROL_LOG) << "The resource" << identifier << "does not provide the Accounts DBus interface. Will remove the agent"; + mAgentManager.removeAgentInstance(identifier); + } else { + QTimer::singleShot(2s, this, [this, identifier, accountId, attempt]() { + configureAgentInstance(identifier, accountId, attempt + 1); + }); + } + return; + } + + accountsIface.setAccountId(accountId); + qCDebug(AKONADICONTROL_LOG) << "Configured resource" << identifier << "for account" << accountId; +} + +void AccountsIntegration::createAgent(const QString &agentType, Accounts::AccountId accountId) +{ + const auto identifier = mAgentManager.createAgentInstance(agentType); + qCDebug(AKONADICONTROL_LOG) << "Created resource" << identifier << "for account" << accountId; + configureAgentInstance(identifier, accountId); +} + +void AccountsIntegration::removeAgentInstance(const QString &identifier) +{ + mAgentManager.removeAgentInstance(identifier); +} + +void AccountsIntegration::onAccountAdded(Accounts::AccountId accId) +{ + qCDebug(AKONADICONTROL_LOG) << "Online account ID" << accId << "added."; + auto account = mAccountsManager.account(accId); + if (!account || !account->isEnabled()) { + return; + } + + const auto enabledServices = account->enabledServices(); + for (const auto &service : enabledServices) { + account->selectService(service); + const auto agentType = account->valueAsString(akonadiAgentType); + if (agentType.isEmpty()) { + continue; // doesn't support Akonadi :-( + } + const auto agent = agentForAccount(agentType, accId); + if (!agent.has_value()) { + createAgent(agentType, account->id()); + } + // Always go through all services, there may be more! + } + account->selectService(); + + connect(account, &Accounts::Account::enabledChanged, this, &AccountsIntegration::onAccountServiceEnabled); +} + +void AccountsIntegration::onAccountRemoved(Accounts::AccountId accId) +{ + qCDebug(AKONADICONTROL_LOG) << "Online account ID" << accId << "removed."; + + const auto instances = mAgentManager.agentInstances(); + for (const auto &instance : instances) { + const auto serviceName = DBus::agentServiceName(instance, DBus::Resource); + org::kde::Akonadi::Accounts accountIface(serviceName, QStringLiteral("/Accounts"), QDBusConnection::sessionBus()); + if (!accountIface.isValid()) { + continue; + } + + if (accountIface.getAccountId() == accId) { + removeAgentInstance(instance); + } + } +} + +void AccountsIntegration::onAccountServiceEnabled(const QString &serviceType, bool enabled) +{ + if (serviceType.isEmpty()) { + return; + } + + auto account = qobject_cast(sender()); + qCDebug(AKONADICONTROL_LOG) << "Online account ID" << account->id() << "service" << serviceType << "has been" << (enabled ? "enabled" : "disabled"); + + const auto service = mAccountsManager.service(serviceType); + account->selectService(service); + const auto agentType = account->valueAsString(akonadiAgentType); + account->selectService(); + if (agentType.isEmpty()) { + return; // this service does not support Akonadi (yet -;) + } + + const auto identifier = agentForAccount(agentType, account->id()); + if (enabled && !identifier.has_value()) { + createAgent(agentType, account->id()); + } else if (!enabled && identifier.has_value()) { + removeAgentInstance(identifier.value()); + } +} diff --git a/src/akonadicontrol/accountsintegration.h b/src/akonadicontrol/accountsintegration.h new file mode 100644 index 0000000..ba44d6c --- /dev/null +++ b/src/akonadicontrol/accountsintegration.h @@ -0,0 +1,46 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2019 Daniel Vrátil * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +#include + +#include + +namespace Accounts +{ +class Account; +class Service; +} + +class AgentManager; + +class AccountsIntegration : public QObject +{ + Q_OBJECT +public: + explicit AccountsIntegration(AgentManager &agentManager); + +private Q_SLOTS: + void onAccountAdded(Accounts::AccountId); + void onAccountRemoved(Accounts::AccountId); + void onAccountServiceEnabled(const QString &service, bool enabled); + +private: + void configureAgentInstance(const QString &identifier, Accounts::AccountId accountId, int attempt = 0); + std::optional agentForAccount(const QString &agentType, Accounts::AccountId accountId) const; + void createAgent(const QString &agentType, Accounts::AccountId accountId); + void removeAgentInstance(const QString &identifier); + + AgentManager &mAgentManager; + Accounts::Manager mAccountsManager; + + QMap mSupportedServices; +}; + diff --git a/src/akonadicontrol/agentbrokeninstance.cpp b/src/akonadicontrol/agentbrokeninstance.cpp new file mode 100644 index 0000000..e2e6d05 --- /dev/null +++ b/src/akonadicontrol/agentbrokeninstance.cpp @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentbrokeninstance.h" + +using namespace Akonadi; + +AgentBrokenInstance::AgentBrokenInstance(const QString &type, AgentManager &manager) + : AgentInstance(manager) +{ + setAgentType(type); + statusChanged(2 /* Akonadi::AgentBase::Status::Broken */, {}); + onlineChanged(false); +} + +bool AgentBrokenInstance::start(const AgentType & /*agentInfo*/) +{ + return false; +} + +void AgentBrokenInstance::quit() +{ + // no-op +} + +void AgentBrokenInstance::cleanup() +{ + // no-op +} + +void AgentBrokenInstance::restartWhenIdle() +{ + // no-op +} + +void AgentBrokenInstance::configure(qlonglong /*windowId*/) +{ + // no-op +} diff --git a/src/akonadicontrol/agentbrokeninstance.h b/src/akonadicontrol/agentbrokeninstance.h new file mode 100644 index 0000000..586d44f --- /dev/null +++ b/src/akonadicontrol/agentbrokeninstance.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentinstance.h" + +namespace Akonadi +{ +class AgentBrokenInstance : public AgentInstance +{ + Q_OBJECT + +public: + explicit AgentBrokenInstance(const QString &type, AgentManager &manager); + ~AgentBrokenInstance() override = default; + + bool start(const AgentType &agentInfo) override; + void quit() override; + void cleanup() override; + void restartWhenIdle() override; + void configure(qlonglong windowId) override; +}; + +} + diff --git a/src/akonadicontrol/agentinstance.cpp b/src/akonadicontrol/agentinstance.cpp new file mode 100644 index 0000000..0cd5bb8 --- /dev/null +++ b/src/akonadicontrol/agentinstance.cpp @@ -0,0 +1,191 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "akonadicontrol_debug.h" + +#include "agentmanager.h" +#include "agenttype.h" + +AgentInstance::AgentInstance(AgentManager &manager) + : mManager(manager) +{ +} + +AgentInstance::~AgentInstance() = default; + +void AgentInstance::quit() +{ + if (mAgentControlInterface && mAgentControlInterface->isValid()) { + mAgentControlInterface->quit(); + } else { + mPendingQuit = true; + } +} + +void AgentInstance::cleanup() +{ + if (mAgentControlInterface && mAgentControlInterface->isValid()) { + mAgentControlInterface->cleanup(); + } +} + +bool AgentInstance::obtainAgentInterface() +{ + mAgentControlInterface = findInterface(Akonadi::DBus::Agent, "/"); + mAgentStatusInterface = findInterface(Akonadi::DBus::Agent, "/"); + + if (mPendingQuit && mAgentControlInterface && mAgentControlInterface->isValid()) { + mAgentControlInterface->quit(); + mPendingQuit = false; + } + + if (!mAgentControlInterface || !mAgentStatusInterface) { + return false; + } + + mSearchInterface = findInterface(Akonadi::DBus::Agent, "/Search"); + + connect(mAgentStatusInterface.get(), + qOverload(&OrgFreedesktopAkonadiAgentStatusInterface::status), + this, + &AgentInstance::statusChanged); + connect(mAgentStatusInterface.get(), &OrgFreedesktopAkonadiAgentStatusInterface::advancedStatus, this, &AgentInstance::advancedStatusChanged); + connect(mAgentStatusInterface.get(), &OrgFreedesktopAkonadiAgentStatusInterface::percent, this, &AgentInstance::percentChanged); + connect(mAgentStatusInterface.get(), &OrgFreedesktopAkonadiAgentStatusInterface::warning, this, &AgentInstance::warning); + connect(mAgentStatusInterface.get(), &OrgFreedesktopAkonadiAgentStatusInterface::error, this, &AgentInstance::error); + connect(mAgentStatusInterface.get(), &OrgFreedesktopAkonadiAgentStatusInterface::onlineChanged, this, &AgentInstance::onlineChanged); + + refreshAgentStatus(); + return true; +} + +bool AgentInstance::obtainResourceInterface() +{ + mResourceInterface = findInterface(Akonadi::DBus::Resource, "/"); + + if (!mResourceInterface) { + return false; + } + + connect(mResourceInterface.get(), &OrgFreedesktopAkonadiResourceInterface::nameChanged, this, &AgentInstance::resourceNameChanged); + refreshResourceStatus(); + return true; +} + +bool AgentInstance::obtainPreprocessorInterface() +{ + mPreprocessorInterface = findInterface(Akonadi::DBus::Preprocessor, "/"); + return mPreprocessorInterface != nullptr; +} + +void AgentInstance::statusChanged(int status, const QString &statusMsg) +{ + if (mStatus == status && mStatusMessage == statusMsg) { + return; + } + mStatus = status; + mStatusMessage = statusMsg; + Q_EMIT mManager.agentInstanceStatusChanged(mIdentifier, mStatus, mStatusMessage); +} + +void AgentInstance::advancedStatusChanged(const QVariantMap &status) +{ + Q_EMIT mManager.agentInstanceAdvancedStatusChanged(mIdentifier, status); +} + +void AgentInstance::statusStateChanged(int status) +{ + statusChanged(status, mStatusMessage); +} + +void AgentInstance::statusMessageChanged(const QString &msg) +{ + statusChanged(mStatus, msg); +} + +void AgentInstance::percentChanged(int percent) +{ + if (mPercent == percent) { + return; + } + mPercent = percent; + Q_EMIT mManager.agentInstanceProgressChanged(mIdentifier, mPercent, QString()); +} + +void AgentInstance::warning(const QString &msg) +{ + Q_EMIT mManager.agentInstanceWarning(mIdentifier, msg); +} + +void AgentInstance::error(const QString &msg) +{ + Q_EMIT mManager.agentInstanceError(mIdentifier, msg); +} + +void AgentInstance::onlineChanged(bool state) +{ + if (mOnline == state) { + return; + } + mOnline = state; + Q_EMIT mManager.agentInstanceOnlineChanged(mIdentifier, state); +} + +void AgentInstance::resourceNameChanged(const QString &name) +{ + if (name == mResourceName) { + return; + } + mResourceName = name; + Q_EMIT mManager.agentInstanceNameChanged(mIdentifier, name); +} + +void AgentInstance::refreshAgentStatus() +{ + if (!hasAgentInterface()) { + return; + } + + // async calls so we are not blocked by misbehaving agents + mAgentStatusInterface->callWithCallback(QStringLiteral("status"), QList(), this, SLOT(statusStateChanged(int)), SLOT(errorHandler(QDBusError))); + mAgentStatusInterface->callWithCallback(QStringLiteral("statusMessage"), + QList(), + this, + SLOT(statusMessageChanged(QString)), + SLOT(errorHandler(QDBusError))); + mAgentStatusInterface->callWithCallback(QStringLiteral("progress"), QList(), this, SLOT(percentChanged(int)), SLOT(errorHandler(QDBusError))); + mAgentStatusInterface->callWithCallback(QStringLiteral("isOnline"), QList(), this, SLOT(onlineChanged(bool)), SLOT(errorHandler(QDBusError))); +} + +void AgentInstance::refreshResourceStatus() +{ + if (!hasResourceInterface()) { + return; + } + + // async call so we are not blocked by misbehaving resources + mResourceInterface->callWithCallback(QStringLiteral("name"), QList(), this, SLOT(resourceNameChanged(QString)), SLOT(errorHandler(QDBusError))); +} + +void AgentInstance::errorHandler(const QDBusError &error) +{ + // avoid using the server tracer, can result in D-BUS lockups + qCCritical(AKONADICONTROL_LOG) << QStringLiteral("D-Bus communication error '%1': '%2'").arg(error.name(), error.message()); + // TODO try again after some time, esp. on timeout errors +} + +template std::unique_ptr AgentInstance::findInterface(Akonadi::DBus::AgentType agentType, const char *path) +{ + auto iface = std::make_unique(Akonadi::DBus::agentServiceName(mIdentifier, agentType), QLatin1String(path), QDBusConnection::sessionBus(), this); + + if (!iface || !iface->isValid()) { + qCCritical(AKONADICONTROL_LOG) << Q_FUNC_INFO << "Cannot connect to agent instance with identifier" << mIdentifier + << ", error message:" << (iface ? iface->lastError().message() : QString()); + return {}; + } + return iface; +} diff --git a/src/akonadicontrol/agentinstance.h b/src/akonadicontrol/agentinstance.h new file mode 100644 index 0000000..a1cec77 --- /dev/null +++ b/src/akonadicontrol/agentinstance.h @@ -0,0 +1,176 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "controlinterface.h" +#include "preprocessorinterface.h" +#include "resourceinterface.h" +#include "searchinterface.h" +#include "statusinterface.h" + +#include + +#include +#include +#include + +#include + +class AgentManager; +class AgentType; + +/** + * Represents one agent instance and takes care of communication with it. + * + * The agent exposes multiple D-Bus interfaces. The Control and the Status + * interfaces are implemented by all the agents. The Resource and Preprocessor + * interfaces are obviously implemented only by the agents impersonating resources or + * preprocessors. + */ +class AgentInstance : public QObject +{ + Q_OBJECT +public: + using Ptr = QSharedPointer; + + explicit AgentInstance(AgentManager &manager); + ~AgentInstance() override; + + /** Set/get the unique identifier of this AgentInstance */ + Q_REQUIRED_RESULT QString identifier() const + { + return mIdentifier; + } + + void setIdentifier(const QString &identifier) + { + mIdentifier = identifier; + } + + Q_REQUIRED_RESULT QString agentType() const + { + return mType; + } + + Q_REQUIRED_RESULT int status() const + { + return mStatus; + } + + Q_REQUIRED_RESULT QString statusMessage() const + { + return mStatusMessage; + } + + Q_REQUIRED_RESULT int progress() const + { + return mPercent; + } + + Q_REQUIRED_RESULT bool isOnline() const + { + return mOnline; + } + + Q_REQUIRED_RESULT QString resourceName() const + { + return mResourceName; + } + + virtual bool start(const AgentType &agentInfo) = 0; + virtual void quit(); + virtual void cleanup(); + virtual void restartWhenIdle() = 0; + virtual void configure(qlonglong windowId) = 0; + + Q_REQUIRED_RESULT bool hasResourceInterface() const + { + return mResourceInterface != nullptr; + } + + Q_REQUIRED_RESULT bool hasAgentInterface() const + { + return mAgentControlInterface != nullptr && mAgentStatusInterface != nullptr; + } + + Q_REQUIRED_RESULT bool hasPreprocessorInterface() const + { + return mPreprocessorInterface != nullptr; + } + + org::freedesktop::Akonadi::Agent::Control *controlInterface() const + { + return mAgentControlInterface.get(); + } + + org::freedesktop::Akonadi::Agent::Status *statusInterface() const + { + return mAgentStatusInterface.get(); + } + + org::freedesktop::Akonadi::Agent::Search *searchInterface() const + { + return mSearchInterface.get(); + } + + org::freedesktop::Akonadi::Resource *resourceInterface() const + { + return mResourceInterface.get(); + } + + org::freedesktop::Akonadi::Preprocessor *preProcessorInterface() const + { + return mPreprocessorInterface.get(); + } + + bool obtainAgentInterface(); + bool obtainResourceInterface(); + bool obtainPreprocessorInterface(); + +protected Q_SLOTS: + void statusChanged(int status, const QString &statusMsg); + void advancedStatusChanged(const QVariantMap &status); + void statusStateChanged(int status); + void statusMessageChanged(const QString &msg); + void percentChanged(int percent); + void warning(const QString &msg); + void error(const QString &msg); + void onlineChanged(bool state); + void resourceNameChanged(const QString &name); + + void refreshAgentStatus(); + void refreshResourceStatus(); + + void errorHandler(const QDBusError &error); + +private: + template std::unique_ptr findInterface(Akonadi::DBus::AgentType agentType, const char *path = nullptr); + +protected: + void setAgentType(const QString &agentType) + { + mType = agentType; + } + +private: + QString mIdentifier; + QString mType; + AgentManager &mManager; + std::unique_ptr mAgentControlInterface; + std::unique_ptr mAgentStatusInterface; + std::unique_ptr mSearchInterface; + std::unique_ptr mResourceInterface; + std::unique_ptr mPreprocessorInterface; + + QString mResourceName; + QString mStatusMessage; + int mStatus = 0; + int mPercent = 0; + bool mOnline = false; + bool mPendingQuit = false; +}; + diff --git a/src/akonadicontrol/agentmanager.cpp b/src/akonadicontrol/agentmanager.cpp new file mode 100644 index 0000000..80fee31 --- /dev/null +++ b/src/akonadicontrol/agentmanager.cpp @@ -0,0 +1,908 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2007 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "agentmanager.h" + +#include "agentbrokeninstance.h" +#include "agentmanageradaptor.h" +#include "agentmanagerinternaladaptor.h" +#include "agentprocessinstance.h" +#include "agentserverinterface.h" +#include "agentthreadinstance.h" +#include "akonadicontrol_debug.h" +#include "preprocessor_manager.h" +#include "processcontrol.h" +#include "resource_manager.h" +#include "serverinterface.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using Akonadi::ProcessControl; +using namespace std::chrono_literals; + +static const bool enableAgentServerDefault = false; + +class StorageProcessControl : public Akonadi::ProcessControl +{ + Q_OBJECT +public: + explicit StorageProcessControl(const QStringList &args) + { + setShutdownTimeout(15s); + connect(this, &Akonadi::ProcessControl::unableToStart, this, []() { + QCoreApplication::instance()->exit(255); + }); + start(QStringLiteral("akonadiserver"), args, RestartOnCrash); + } + + ~StorageProcessControl() override + { + setCrashPolicy(ProcessControl::StopOnCrash); + org::freedesktop::Akonadi::Server serverIface(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/Server"), + QDBusConnection::sessionBus()); + serverIface.quit(); + } +}; + +class AgentServerProcessControl : public Akonadi::ProcessControl +{ + Q_OBJECT +public: + explicit AgentServerProcessControl(const QStringList &args) + { + connect(this, &Akonadi::ProcessControl::unableToStart, this, []() { + qCCritical(AKONADICONTROL_LOG) << "Failed to start AgentServer!"; + }); + start(QStringLiteral("akonadi_agent_server"), args, RestartOnCrash); + } + + ~AgentServerProcessControl() override + { + setCrashPolicy(ProcessControl::StopOnCrash); + org::freedesktop::Akonadi::AgentServer agentServerIface(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), + QDBusConnection::sessionBus(), + this); + agentServerIface.quit(); + } +}; + +AgentManager::AgentManager(bool verbose, QObject *parent) + : QObject(parent) + , mAgentServer(nullptr) + , mVerbose(verbose) +{ + new AgentManagerAdaptor(this); + new AgentManagerInternalAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/AgentManager"), this); + + connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &AgentManager::serviceOwnerChanged); + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server))) { + qFatal("akonadiserver already running!"); + } + + const QSettings settings(Akonadi::StandardDirs::agentsConfigFile(Akonadi::StandardDirs::ReadOnly), QSettings::IniFormat); + mAgentServerEnabled = settings.value(QStringLiteral("AgentServer/Enabled"), enableAgentServerDefault).toBool(); + + QStringList serviceArgs; + if (Akonadi::Instance::hasIdentifier()) { + serviceArgs << QStringLiteral("--instance") << Akonadi::Instance::identifier(); + } + if (verbose) { + serviceArgs << QStringLiteral("--verbose"); + } + + mStorageController = std::unique_ptr(new StorageProcessControl(serviceArgs)); + + if (mAgentServerEnabled) { + mAgentServer = std::unique_ptr(new AgentServerProcessControl(serviceArgs)); + } +} + +void AgentManager::continueStartup() +{ + // prevent multiple calls in case the server has to be restarted + static bool first = true; + if (!first) { + return; + } + + first = false; + + readPluginInfos(); + for (const AgentType &info : std::as_const(mAgents)) { + Q_EMIT agentTypeAdded(info.identifier); + } + + load(); + for (const AgentType &info : std::as_const(mAgents)) { + ensureAutoStart(info); + } + + // register the real service name once everything is up an running + if (!QDBusConnection::sessionBus().registerService(Akonadi::DBus::serviceName(Akonadi::DBus::Control))) { + // besides a race with an older Akonadi server I have no idea how we could possibly get here... + qFatal("Unable to register service as %s despite having the lock. Error was: %s", + qPrintable(Akonadi::DBus::serviceName(Akonadi::DBus::Control)), + qPrintable(QDBusConnection::sessionBus().lastError().message())); + } + qCInfo(AKONADICONTROL_LOG) << "Akonadi server is now operational."; +} + +AgentManager::~AgentManager() +{ + cleanup(); +} + +void AgentManager::cleanup() +{ + for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) { + instance->quit(); + } + mAgentInstances.clear(); + + mStorageController.reset(); + mStorageController.reset(); +} + +QStringList AgentManager::agentTypes() const +{ + return mAgents.keys(); +} + +QString AgentManager::agentName(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + return mAgents.value(identifier).name; +} + +QString AgentManager::agentComment(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + return mAgents.value(identifier).comment; +} + +QString AgentManager::agentIcon(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + const AgentType info = mAgents.value(identifier); + if (!info.icon.isEmpty()) { + return info.icon; + } + + return QStringLiteral("application-x-executable"); +} + +QStringList AgentManager::agentMimeTypes(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QStringList(); + } + + return mAgents.value(identifier).mimeTypes; +} + +QStringList AgentManager::agentCapabilities(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QStringList(); + } + return mAgents.value(identifier).capabilities; +} + +QVariantMap AgentManager::agentCustomProperties(const QString &identifier) const +{ + if (!checkAgentExists(identifier)) { + return QVariantMap(); + } + + return mAgents.value(identifier).custom; +} + +AgentInstance::Ptr AgentManager::createAgentInstance(const AgentType &info) +{ + switch (info.launchMethod) { + case AgentType::Server: + return QSharedPointer::create(*this); + case AgentType::Launcher: // Fall through + case AgentType::Process: + return QSharedPointer::create(*this); + default: + Q_ASSERT_X(false, "AgentManger::createAgentInstance", "Unhandled AgentType::LaunchMethod case"); + } + + return AgentInstance::Ptr(); +} + +QString AgentManager::createAgentInstance(const QString &identifier) +{ + if (!checkAgentExists(identifier)) { + return QString(); + } + + const AgentType agentInfo = mAgents[identifier]; + mAgents[identifier].instanceCounter++; + + const auto instance = createAgentInstance(agentInfo); + if (agentInfo.capabilities.contains(AgentType::CapabilityUnique)) { + instance->setIdentifier(identifier); + } else { + instance->setIdentifier(QStringLiteral("%1_%2").arg(identifier, QString::number(agentInfo.instanceCounter))); + } + + const QString instanceIdentifier = instance->identifier(); + if (mAgentInstances.contains(instanceIdentifier)) { + qCWarning(AKONADICONTROL_LOG) << "Cannot create another instance of agent" << identifier; + return QString(); + } + + // Return from this dbus call before we do the next. Otherwise dbus brakes for + // this process. + if (calledFromDBus()) { + connection().send(message().createReply(instanceIdentifier)); + } + + if (!instance->start(agentInfo)) { + return QString(); + } + + mAgentInstances.insert(instanceIdentifier, instance); + registerAgentAtServer(instanceIdentifier, agentInfo); + save(); + + return instanceIdentifier; +} + +void AgentManager::removeAgentInstance(const QString &identifier) +{ + const auto instance = mAgentInstances.value(identifier); + if (!instance) { + qCWarning(AKONADICONTROL_LOG) << Q_FUNC_INFO << "Agent instance with identifier" << identifier << "does not exist"; + return; + } + + if (instance->hasAgentInterface()) { + instance->cleanup(); + } else { + qCWarning(AKONADICONTROL_LOG) << "Agent instance" << identifier << "has no interface!"; + } + + mAgentInstances.remove(identifier); + + save(); + + org::freedesktop::Akonadi::ResourceManager resmanager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/ResourceManager"), + QDBusConnection::sessionBus(), + this); + resmanager.removeResourceInstance(instance->identifier()); + + // Kill the preprocessor instance, if any. + org::freedesktop::Akonadi::PreprocessorManager preProcessorManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/PreprocessorManager"), + QDBusConnection::sessionBus(), + this); + + preProcessorManager.unregisterInstance(instance->identifier()); + + if (instance->hasAgentInterface()) { + qCDebug(AKONADICONTROL_LOG) << "AgentManager::removeAgentInstance: calling instance->quit()"; + instance->quit(); + } else { + qCWarning(AKONADICONTROL_LOG) << "Agent instance" << identifier << "has no interface!"; + } + + Q_EMIT agentInstanceRemoved(identifier); +} + +QString AgentManager::agentInstanceType(const QString &identifier) +{ + const AgentInstance::Ptr agent = mAgentInstances.value(identifier); + if (!agent) { + qCWarning(AKONADICONTROL_LOG) << "Agent instance with identifier" << identifier << "does not exist"; + return QString(); + } + + return agent->agentType(); +} + +QStringList AgentManager::agentInstances() const +{ + return mAgentInstances.keys(); +} + +int AgentManager::agentInstanceStatus(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return 2; + } + + return mAgentInstances.value(identifier)->status(); +} + +QString AgentManager::agentInstanceStatusMessage(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return QString(); + } + + return mAgentInstances.value(identifier)->statusMessage(); +} + +uint AgentManager::agentInstanceProgress(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return 0; + } + + return mAgentInstances.value(identifier)->progress(); +} + +QString AgentManager::agentInstanceProgressMessage(const QString &identifier) const +{ + Q_UNUSED(identifier) + + return QString(); +} + +void AgentManager::agentInstanceConfigure(const QString &identifier, qlonglong windowId) +{ + if (!checkAgentInterfaces(identifier, QStringLiteral("agentInstanceConfigure"))) { + return; + } + + mAgentInstances.value(identifier)->configure(windowId); +} + +bool AgentManager::agentInstanceOnline(const QString &identifier) +{ + if (!checkInstance(identifier)) { + return false; + } + + return mAgentInstances.value(identifier)->isOnline(); +} + +void AgentManager::setAgentInstanceOnline(const QString &identifier, bool state) +{ + if (!checkAgentInterfaces(identifier, QStringLiteral("setAgentInstanceOnline"))) { + return; + } + + mAgentInstances.value(identifier)->statusInterface()->setOnline(state); +} + +// resource specific methods // +void AgentManager::setAgentInstanceName(const QString &identifier, const QString &name) +{ + if (!checkResourceInterface(identifier, QStringLiteral("setAgentInstanceName"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->setName(name); +} + +QString AgentManager::agentInstanceName(const QString &identifier) const +{ + if (!checkInstance(identifier)) { + return QString(); + } + + const AgentInstance::Ptr instance = mAgentInstances.value(identifier); + if (!instance->resourceName().isEmpty()) { + return instance->resourceName(); + } + + if (!checkAgentExists(instance->agentType())) { + return QString(); + } + + return mAgents.value(instance->agentType()).name; +} + +void AgentManager::agentInstanceSynchronize(const QString &identifier) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronize"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronize(); +} + +void AgentManager::agentInstanceSynchronizeCollectionTree(const QString &identifier) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeCollectionTree"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronizeCollectionTree(); +} + +void AgentManager::agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection) +{ + agentInstanceSynchronizeCollection(identifier, collection, false); +} + +void AgentManager::agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection, bool recursive) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeCollection"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronizeCollection(collection, recursive); +} + +void AgentManager::agentInstanceSynchronizeTags(const QString &identifier) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeTags"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronizeTags(); +} + +void AgentManager::agentInstanceSynchronizeRelations(const QString &identifier) +{ + if (!checkResourceInterface(identifier, QStringLiteral("agentInstanceSynchronizeRelations"))) { + return; + } + + mAgentInstances.value(identifier)->resourceInterface()->synchronizeRelations(); +} + +void AgentManager::restartAgentInstance(const QString &identifier) +{ + if (!checkInstance(identifier)) { + return; + } + + mAgentInstances.value(identifier)->restartWhenIdle(); +} + +void AgentManager::updatePluginInfos() +{ + const QHash oldInfos = mAgents; + readPluginInfos(); + + for (const AgentType &oldInfo : oldInfos) { + if (!mAgents.contains(oldInfo.identifier)) { + Q_EMIT agentTypeRemoved(oldInfo.identifier); + } + } + + for (const AgentType &newInfo : std::as_const(mAgents)) { + if (!oldInfos.contains(newInfo.identifier)) { + Q_EMIT agentTypeAdded(newInfo.identifier); + ensureAutoStart(newInfo); + } + } +} + +void AgentManager::readPluginInfos() +{ + mAgents.clear(); + + const QStringList pathList = pluginInfoPathList(); + + for (const QString &path : pathList) { + const QDir directory(path, QStringLiteral("*.desktop")); + readPluginInfos(directory); + } +} + +void AgentManager::readPluginInfos(const QDir &directory) +{ + const QStringList files = directory.entryList(); + qCDebug(AKONADICONTROL_LOG) << "PLUGINS: " << directory.canonicalPath(); + qCDebug(AKONADICONTROL_LOG) << "PLUGINS: " << files; + + for (int i = 0; i < files.count(); ++i) { + const QString fileName = directory.absoluteFilePath(files[i]); + + AgentType agentInfo; + if (agentInfo.load(fileName, this)) { + if (mAgents.contains(agentInfo.identifier)) { + qCWarning(AKONADICONTROL_LOG) << "Duplicated agent identifier" << agentInfo.identifier << "from file" << fileName; + continue; + } + + const QString disableAutostart = akGetEnv("AKONADI_DISABLE_AGENT_AUTOSTART"); + if (!disableAutostart.isEmpty()) { + qCDebug(AKONADICONTROL_LOG) << "Autostarting of agents is disabled."; + agentInfo.capabilities.removeOne(AgentType::CapabilityAutostart); + } + + if (!mAgentServerEnabled && agentInfo.launchMethod == AgentType::Server) { + agentInfo.launchMethod = AgentType::Launcher; + } + + if (agentInfo.launchMethod == AgentType::Process) { + const QString executable = Akonadi::StandardDirs::findExecutable(agentInfo.exec); + if (executable.isEmpty()) { + qCWarning(AKONADICONTROL_LOG) << "Executable" << agentInfo.exec << "for agent" << agentInfo.identifier << "could not be found!"; + continue; + } + } + + qCDebug(AKONADICONTROL_LOG) << "PLUGINS inserting: " << agentInfo.identifier << agentInfo.instanceCounter << agentInfo.capabilities; + mAgents.insert(agentInfo.identifier, agentInfo); + } + } +} + +QStringList AgentManager::pluginInfoPathList() +{ + return Akonadi::StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents")); +} + +void AgentManager::load() +{ + org::freedesktop::Akonadi::ResourceManager resmanager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/ResourceManager"), + QDBusConnection::sessionBus(), + this); + const QStringList knownResources = resmanager.resourceInstances(); + + QSettings file(Akonadi::StandardDirs::agentsConfigFile(Akonadi::StandardDirs::ReadOnly), QSettings::IniFormat); + file.beginGroup(QStringLiteral("Instances")); + const QStringList entries = file.childGroups(); + for (int i = 0; i < entries.count(); ++i) { + const QString instanceIdentifier = entries[i]; + + if (mAgentInstances.contains(instanceIdentifier)) { + qCWarning(AKONADICONTROL_LOG) << "Duplicated instance identifier" << instanceIdentifier << "found in agentsrc"; + continue; + } + + file.beginGroup(entries[i]); + + const QString agentType = file.value(QStringLiteral("AgentType")).toString(); + const auto typeIter = mAgents.constFind(agentType); + if (typeIter == mAgents.cend() || typeIter->exec.isEmpty()) { + qCWarning(AKONADICONTROL_LOG) << "Reference to unknown agent type" << agentType << "in agentsrc, creating a fake entry."; + if (typeIter == mAgents.cend()) { + AgentType type; + type.identifier = type.name = agentType; + mAgents.insert(type.identifier, type); + } + + auto brokenInstance = AgentInstance::Ptr{new Akonadi::AgentBrokenInstance{agentType, *this}}; + brokenInstance->setIdentifier(instanceIdentifier); + mAgentInstances.insert(instanceIdentifier, brokenInstance); + file.endGroup(); + continue; + } + + const AgentType &type = *typeIter; + + // recover if the db has been deleted in the meantime or got otherwise corrupted + if (!knownResources.contains(instanceIdentifier) && type.capabilities.contains(AgentType::CapabilityResource)) { + qCDebug(AKONADICONTROL_LOG) << "Recovering instance" << instanceIdentifier << "after database loss"; + registerAgentAtServer(instanceIdentifier, type); + } + + const AgentInstance::Ptr instance = createAgentInstance(type); + instance->setIdentifier(instanceIdentifier); + if (instance->start(type)) { + mAgentInstances.insert(instanceIdentifier, instance); + } + + file.endGroup(); + } + + file.endGroup(); +} + +void AgentManager::save() +{ + QSettings file(Akonadi::StandardDirs::agentsConfigFile(Akonadi::StandardDirs::WriteOnly), QSettings::IniFormat); + + for (const AgentType &info : std::as_const(mAgents)) { + info.save(&file); + } + + file.beginGroup(QStringLiteral("Instances")); + file.remove(QString()); + for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) { + file.beginGroup(instance->identifier()); + file.setValue(QStringLiteral("AgentType"), instance->agentType()); + file.endGroup(); + } + + file.endGroup(); +} + +void AgentManager::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(oldOwner) + // This is called by the D-Bus server when a service comes up, goes down or changes ownership for some reason + // and this is where we "hook up" our different Agent interfaces. + + // Ignore DBus address name (e.g. :1.310) + if (name.startsWith(QLatin1Char(':'))) { + return; + } + + // Ignore services belonging to another Akonadi instance + const auto parsedInstance = Akonadi::DBus::parseInstanceIdentifier(name); + const auto currentInstance = Akonadi::Instance::hasIdentifier() ? std::optional(Akonadi::Instance::identifier()) : std::nullopt; + if (parsedInstance != currentInstance) { + return; + } + + qCDebug(AKONADICONTROL_LOG) << "Service" << name << "owner changed from" << oldOwner << "to" << newOwner; + + if ((name == Akonadi::DBus::serviceName(Akonadi::DBus::Server) || name == Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer)) && !newOwner.isEmpty()) { + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server)) + && (!mAgentServer || QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer)))) { + // server is operational, start agents + continueStartup(); + } + } + + const auto service = Akonadi::DBus::parseAgentServiceName(name); + if (!service.has_value()) { + return; + } + switch (service->agentType) { + case Akonadi::DBus::Agent: { + // An agent service went up or down + if (newOwner.isEmpty()) { + return; // It went down: we don't care here. + } + + if (!mAgentInstances.contains(service->identifier)) { + return; + } + + const AgentInstance::Ptr instance = mAgentInstances.value(service->identifier); + const bool restarting = instance->hasAgentInterface(); + if (!instance->obtainAgentInterface()) { + return; + } + + Q_ASSERT(mAgents.contains(instance->agentType())); + const bool isResource = mAgents.value(instance->agentType()).capabilities.contains(AgentType::CapabilityResource); + + if (!restarting && (!isResource || instance->hasResourceInterface())) { + Q_EMIT agentInstanceAdded(service->identifier); + } + + break; + } + case Akonadi::DBus::Resource: { + // A resource service went up or down + if (newOwner.isEmpty()) { + return; // It went down: we don't care here. + } + + if (!mAgentInstances.contains(service->identifier)) { + return; + } + + const AgentInstance::Ptr instance = mAgentInstances.value(service->identifier); + const bool restarting = instance->hasResourceInterface(); + if (!instance->obtainResourceInterface()) { + return; + } + + if (!restarting && instance->hasAgentInterface()) { + Q_EMIT agentInstanceAdded(service->identifier); + } + + break; + } + case Akonadi::DBus::Preprocessor: { + // A preprocessor service went up or down + + // If the preprocessor is going up then the org.freedesktop.Akonadi.Agent.* interface + // should be already up (as it's registered before the preprocessor one). + // So if we don't know about the preprocessor as agent instance + // then it's not our preprocessor. + + // If the preprocessor is going down then either the agent interface already + // went down (and it has been already unregistered on the manager side) + // or it's still registered as agent and WE have to unregister it. + // The order of interface deletions depends on Qt but we handle both cases. + + // Check if we "know" about it. + qCDebug(AKONADICONTROL_LOG) << "Preprocessor " << service->identifier << " is going up or down..."; + + if (!mAgentInstances.contains(service->identifier)) { + qCDebug(AKONADICONTROL_LOG) << "But it isn't registered as agent... not mine (anymore?)"; + return; // not our agent (?) + } + + org::freedesktop::Akonadi::PreprocessorManager preProcessorManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/PreprocessorManager"), + QDBusConnection::sessionBus(), + this); + + if (!preProcessorManager.isValid()) { + qCWarning(AKONADICONTROL_LOG) << "Could not connect to PreprocessorManager via D-Bus:" << preProcessorManager.lastError().message(); + } else { + if (newOwner.isEmpty()) { + // The preprocessor went down. Unregister it on server side. + + preProcessorManager.unregisterInstance(service->identifier); + + } else { + // The preprocessor went up. Register it on server side. + + if (!mAgentInstances.value(service->identifier)->obtainPreprocessorInterface()) { + // Hm.. couldn't hook up its preprocessor interface.. + // Make sure we don't have it in the preprocessor chain + qCWarning(AKONADICONTROL_LOG) << "Couldn't obtain preprocessor interface for instance" << service->identifier; + + preProcessorManager.unregisterInstance(service->identifier); + return; + } + + qCDebug(AKONADICONTROL_LOG) << "Registering preprocessor instance" << service->identifier; + + // Add to the preprocessor chain + preProcessorManager.registerInstance(service->identifier); + } + } + + break; + } + default: + break; + } +} + +bool AgentManager::checkInstance(const QString &identifier) const +{ + if (!mAgentInstances.contains(identifier)) { + qCWarning(AKONADICONTROL_LOG) << "Agent instance with identifier " << identifier << " does not exist"; + return false; + } + + return true; +} + +bool AgentManager::checkResourceInterface(const QString &identifier, const QString &method) const +{ + if (!checkInstance(identifier)) { + return false; + } + + if (!mAgents[mAgentInstances[identifier]->agentType()].capabilities.contains(QLatin1String("Resource"))) { + return false; + } + + if (!mAgentInstances[identifier]->hasResourceInterface()) { + qCWarning(AKONADICONTROL_LOG) << QLatin1String("AgentManager::") + method << " Agent instance " << identifier << " has no resource interface!"; + return false; + } + + return true; +} + +bool AgentManager::checkAgentExists(const QString &identifier) const +{ + if (!mAgents.contains(identifier)) { + qCWarning(AKONADICONTROL_LOG) << "Agent instance " << identifier << " does not exist."; + return false; + } + + return true; +} + +bool AgentManager::checkAgentInterfaces(const QString &identifier, const QString &method) const +{ + if (!checkInstance(identifier)) { + return false; + } + + if (!mAgentInstances.value(identifier)->hasAgentInterface()) { + qCWarning(AKONADICONTROL_LOG) << "Agent instance (" << method << ") " << identifier << " has no agent interface."; + return false; + } + + return true; +} + +void AgentManager::ensureAutoStart(const AgentType &info) +{ + if (!info.capabilities.contains(AgentType::CapabilityAutostart)) { + return; // no an autostart agent + } + + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), + QDBusConnection::sessionBus(), + this); + + if (mAgentInstances.contains(info.identifier) || (agentServer.isValid() && agentServer.started(info.identifier))) { + return; // already running + } + + const AgentInstance::Ptr instance = createAgentInstance(info); + instance->setIdentifier(info.identifier); + if (instance->start(info)) { + mAgentInstances.insert(instance->identifier(), instance); + registerAgentAtServer(instance->identifier(), info); + save(); + } +} + +void AgentManager::agentExeChanged(const QString &fileName) +{ + if (!QFile::exists(fileName)) { + return; + } + + for (const AgentType &type : std::as_const(mAgents)) { + if (fileName.endsWith(type.exec)) { + for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) { + if (instance->agentType() == type.identifier) { + instance->restartWhenIdle(); + } + } + } + } +} + +void AgentManager::registerAgentAtServer(const QString &agentIdentifier, const AgentType &type) +{ + if (type.capabilities.contains(AgentType::CapabilityResource)) { + QScopedPointer resmanager( + new org::freedesktop::Akonadi::ResourceManager(Akonadi::DBus::serviceName(Akonadi::DBus::Server), + QStringLiteral("/ResourceManager"), + QDBusConnection::sessionBus(), + this)); + resmanager->addResourceInstance(agentIdentifier, type.capabilities); + } +} + +void AgentManager::addSearch(const QString &query, const QString &queryLanguage, qint64 resultCollectionId) +{ + qCDebug(AKONADICONTROL_LOG) << "AgentManager::addSearch" << query << queryLanguage << resultCollectionId; + for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) { + const AgentType type = mAgents.value(instance->agentType()); + if (type.capabilities.contains(AgentType::CapabilitySearch) && instance->searchInterface()) { + instance->searchInterface()->addSearch(query, queryLanguage, resultCollectionId); + } + } +} + +void AgentManager::removeSearch(quint64 resultCollectionId) +{ + qCDebug(AKONADICONTROL_LOG) << "AgentManager::removeSearch" << resultCollectionId; + for (const AgentInstance::Ptr &instance : std::as_const(mAgentInstances)) { + const AgentType type = mAgents.value(instance->agentType()); + if (type.capabilities.contains(AgentType::CapabilitySearch) && instance->searchInterface()) { + instance->searchInterface()->removeSearch(resultCollectionId); + } + } +} + +#include "agentmanager.moc" diff --git a/src/akonadicontrol/agentmanager.h b/src/akonadicontrol/agentmanager.h new file mode 100644 index 0000000..b1ac96b --- /dev/null +++ b/src/akonadicontrol/agentmanager.h @@ -0,0 +1,375 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2007 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +#include "agentinstance.h" +#include "agenttype.h" + +class QDir; + +namespace Akonadi +{ +class ProcessControl; +} + +/** + * The agent manager has knowledge about all available agents (it scans + * for .desktop files in the agent directory) and the available configured + * instances. + */ +class AgentManager : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.AgentManager") + +public: + /** + * Creates a new agent manager. + * + * @param parent The parent object. + */ + explicit AgentManager(bool verbose, QObject *parent = nullptr); + + /** + * Destroys the agent manager. + */ + ~AgentManager(); + + /** + * Called by the crash handler and dtor to terminate + * the child processes. + */ + void cleanup(); + + /** + * Returns the list of identifiers of all available + * agent types. + */ + QStringList agentTypes() const; + + /** + * Returns the i18n'ed name of the agent type for + * the given @p identifier. + */ + QString agentName(const QString &identifier) const; + + /** + * Returns the i18n'ed comment of the agent type for + * the given @p identifier.. + */ + QString agentComment(const QString &identifier) const; + + /** + * Returns the icon name of the agent type for the + * given @p identifier. + */ + QString agentIcon(const QString &identifier) const; + + /** + * Returns a list of supported mimetypes of the agent type + * for the given @p identifier. + */ + QStringList agentMimeTypes(const QString &identifier) const; + + /** + * Returns a list of supported capabilities of the agent type + * for the given @p identifier. + */ + QStringList agentCapabilities(const QString &identifier) const; + + /** + * Returns a list of Custom added propeties of the agent type + * for the given @p identifier + * @since 1.11 + */ + QVariantMap agentCustomProperties(const QString &identifier) const; + + /** + * Creates a new agent of the given agent type @p identifier. + * + * @return The identifier of the new agent if created successfully, + * an empty string otherwise. + * The identifier consists of two parts, the type of the + * agent and an unique instance number, and looks like + * the following: 'file_1' or 'imap_267'. + */ + QString createAgentInstance(const QString &identifier); + + /** + * Removes the agent with the given @p identifier. + */ + void removeAgentInstance(const QString &identifier); + + /** + * Returns the type of the agent instance with the given @p identifier. + */ + QString agentInstanceType(const QString &identifier); + + /** + * Returns the list of identifiers of configured instances. + */ + QStringList agentInstances() const; + + /** + * Returns the current status code of the agent with the given @p identifier. + */ + int agentInstanceStatus(const QString &identifier) const; + + /** + * Returns the i18n'ed description of the current status of the agent with + * the given @p identifier. + */ + QString agentInstanceStatusMessage(const QString &identifier) const; + + /** + * Returns the current progress of the agent with the given @p identifier + * in percentage. + */ + uint agentInstanceProgress(const QString &identifier) const; + + /** + * Returns the i18n'ed description of the current progress of the agent with + * the given @p identifier. + */ + QString agentInstanceProgressMessage(const QString &identifier) const; + + /** + * Sets the @p name of the agent instance with the given @p identifier. + */ + void setAgentInstanceName(const QString &identifier, const QString &name); + + /** + * Returns the name of the agent instance with the given @p identifier. + */ + QString agentInstanceName(const QString &identifier) const; + + /** + * Triggers the agent instance with the given @p identifier to show + * its configuration dialog. + * @param windowId Parent window id for the configuration dialog. + */ + void agentInstanceConfigure(const QString &identifier, qlonglong windowId); + + /** + * Triggers the agent instance with the given @p identifier to start + * synchronization. + */ + void agentInstanceSynchronize(const QString &identifier); + + /** + Trigger a synchronization of the collection tree by the given resource agent. + @param identifier The resource agent identifier. + */ + void agentInstanceSynchronizeCollectionTree(const QString &identifier); + + /** + Trigger a synchronization of the given collection by its owning resource agent. + */ + void agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection); + + /** + Trigger a synchronization of the given collection by its owning resource agent. + @param recursive set it true to have sub-collection synchronized as well + */ + void agentInstanceSynchronizeCollection(const QString &identifier, qint64 collection, bool recursive); + + /** + * Trigger a synchronization of tags by the given resource agent. + * @param identifier The resource agent identifier. + */ + void agentInstanceSynchronizeTags(const QString &identifier); + + /** + * Trigger a synchronization of relations by the given resource agent. + * @param identifier The resource agent identifier. + */ + void agentInstanceSynchronizeRelations(const QString &identifier); + + /** + Returns if the agent instance @p identifier is in online mode. + */ + bool agentInstanceOnline(const QString &identifier); + + /** + Sets agent instance @p identifier to online or offline mode. + */ + void setAgentInstanceOnline(const QString &identifier, bool state); + + /** + Restarts the agent instance @p identifier. This is supposed to be used as a + development aid and not something to use during normal operations. + */ + void restartAgentInstance(const QString &identifier); + + /** + * Add a persistent search to remote search agents. + */ + void addSearch(const QString &query, const QString &queryLanguage, qint64 resultCollectionId); + + /** + * Removes a persistent search for the given result collection. + */ + void removeSearch(quint64 resultCollectionId); + +Q_SIGNALS: + /** + * This signal is emitted whenever a new agent type was installed on the system. + * + * @param agentType The identifier of the new agent type. + */ + void agentTypeAdded(const QString &agentType); + + /** + * This signal is emitted whenever an agent type was removed from the system. + * + * @param agentType The identifier of the removed agent type. + */ + void agentTypeRemoved(const QString &agentType); + + /** + * This signal is emitted whenever a new agent instance was created. + * + * @param agentIdentifier The identifier of the new agent instance. + */ + void agentInstanceAdded(const QString &agentIdentifier); + + /** + * This signal is emitted whenever an agent instance was removed. + * + * @param agentIdentifier The identifier of the removed agent instance. + */ + void agentInstanceRemoved(const QString &agentIdentifier); + + /** + * This signal is emitted whenever the status of an agent instance has + * changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param status The new status code. + * @param message The i18n'ed description of the new status. + */ + void agentInstanceStatusChanged(const QString &agentIdentifier, int status, const QString &message); + + /** + * This signal is emitted whenever the status of an agent instance has + * changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param status The object that describes the status change. + */ + void agentInstanceAdvancedStatusChanged(const QString &agentIdentifier, const QVariantMap &status); + + /** + * This signal is emitted whenever the progress of an agent instance has + * changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param progress The new progress in percentage. + * @param message The i18n'ed description of the new progress. + */ + void agentInstanceProgressChanged(const QString &agentIdentifier, uint progress, const QString &message); + + /** + * This signal is emitted whenever an agent instance raised a warning. + * + * @param agentIdentifier The identifier of the agent instance. + * @param message The i18n'ed warning message. + */ + void agentInstanceWarning(const QString &agentIdentifier, const QString &message); + + /** + * This signal is emitted whenever an agent instance raised an error. + * + * @param agentIdentifier The identifier of the agent instance. + * @param message The i18n'ed error message. + */ + void agentInstanceError(const QString &agentIdentifier, const QString &message); + + /** + * This signal is emitted whenever the name of the agent instance has changed. + * + * @param agentIdentifier The identifier of the agent that has changed. + * @param name The new name of the agent instance. + */ + void agentInstanceNameChanged(const QString &agentIdentifier, const QString &name); + + /** + * Emitted when the online state of an agent changed. + */ + void agentInstanceOnlineChanged(const QString &agentIdentifier, bool state); + +private Q_SLOTS: + void updatePluginInfos(); + void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); + void agentExeChanged(const QString &fileName); + +private: + /** + * Returns the list of directory paths where the .desktop files + * for the plugins are located. + */ + static QStringList pluginInfoPathList(); + + /** + * Loads the internal state from config file. + */ + void load(); + + /** + * Saves internal state to the config file. + */ + void save(); + + /** + * Reads the plugin information from directory. + */ + void readPluginInfos(); + + /** + * Reads the plugin information from directory. + * + * @param directory the directory to get plugin information from + */ + void readPluginInfos(const QDir &directory); + + AgentInstance::Ptr createAgentInstance(const AgentType &type); + bool checkAgentInterfaces(const QString &identifier, const QString &method) const; + bool checkInstance(const QString &identifier) const; + bool checkResourceInterface(const QString &identifier, const QString &method) const; + bool checkAgentExists(const QString &identifier) const; + void ensureAutoStart(const AgentType &info); + void continueStartup(); + void registerAgentAtServer(const QString &agentIdentifier, const AgentType &type); + +private: + /** + * The map which stores the .desktop file + * entries for every agent type. + * + * Key is the agent type (e.g. 'file' or 'imap'). + */ + QHash mAgents; + + /** + * The map which stores the active instances. + * + * Key is the instance identifier. + */ + QHash mAgentInstances; + + std::unique_ptr mAgentServer; + std::unique_ptr mStorageController; + bool mAgentServerEnabled = false; + bool mVerbose = false; + + friend class AgentInstance; +}; + diff --git a/src/akonadicontrol/agentprocessinstance.cpp b/src/akonadicontrol/agentprocessinstance.cpp new file mode 100644 index 0000000..8e92e08 --- /dev/null +++ b/src/akonadicontrol/agentprocessinstance.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "agentprocessinstance.h" + +#include "agenttype.h" +#include "akonadicontrol_debug.h" +#include "private/standarddirs_p.h" +#include "processcontrol.h" + +#include + +using namespace Akonadi; + +AgentProcessInstance::AgentProcessInstance(AgentManager &manager) + : AgentInstance(manager) +{ +} + +bool AgentProcessInstance::start(const AgentType &agentInfo) +{ + Q_ASSERT(!identifier().isEmpty()); + if (identifier().isEmpty()) { + return false; + } + + setAgentType(agentInfo.identifier); + + Q_ASSERT(agentInfo.launchMethod == AgentType::Process || agentInfo.launchMethod == AgentType::Launcher); + + const QString executable = (agentInfo.launchMethod == AgentType::Process) ? Akonadi::StandardDirs::findExecutable(agentInfo.exec) : agentInfo.exec; + + if (executable.isEmpty()) { + qCWarning(AKONADICONTROL_LOG) << "Unable to find agent executable" << agentInfo.exec; + return false; + } + + mController = std::make_unique(); + connect(mController.get(), &ProcessControl::unableToStart, this, &AgentProcessInstance::failedToStart); + + if (agentInfo.launchMethod == AgentType::Process) { + const QStringList arguments = {QStringLiteral("--identifier"), identifier()}; + mController->start(executable, arguments); + } else { + Q_ASSERT(agentInfo.launchMethod == AgentType::Launcher); + const QStringList arguments = QStringList() << executable << identifier(); + const QString agentLauncherExec = Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadi_agent_launcher")); + mController->start(agentLauncherExec, arguments); + } + return true; +} + +void AgentProcessInstance::quit() +{ + mController->setCrashPolicy(Akonadi::ProcessControl::StopOnCrash); + AgentInstance::quit(); +} + +void AgentProcessInstance::cleanup() +{ + mController->setCrashPolicy(Akonadi::ProcessControl::StopOnCrash); + AgentInstance::cleanup(); +} + +void AgentProcessInstance::restartWhenIdle() +{ + if (mController->isRunning()) { + if (status() != 1) { + mController->restartOnceWhenFinished(); + quit(); + } + } else { + mController->start(); + } +} + +void Akonadi::AgentProcessInstance::configure(qlonglong windowId) +{ + controlInterface()->configure(windowId); +} + +void AgentProcessInstance::failedToStart() +{ + statusChanged(2 /*Broken*/, QStringLiteral("Unable to start.")); +} diff --git a/src/akonadicontrol/agentprocessinstance.h b/src/akonadicontrol/agentprocessinstance.h new file mode 100644 index 0000000..4c62a74 --- /dev/null +++ b/src/akonadicontrol/agentprocessinstance.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentinstance.h" + +namespace Akonadi +{ +class ProcessControl; + +class AgentProcessInstance : public AgentInstance +{ + Q_OBJECT + +public: + explicit AgentProcessInstance(AgentManager &manager); + ~AgentProcessInstance() override = default; + + bool start(const AgentType &agentInfo) override; + void quit() override; + void cleanup() override; + void restartWhenIdle() override; + void configure(qlonglong windowId) override; + +private Q_SLOTS: + void failedToStart(); + +private: + std::unique_ptr mController; +}; + +} + diff --git a/src/akonadicontrol/agentthreadinstance.cpp b/src/akonadicontrol/agentthreadinstance.cpp new file mode 100644 index 0000000..a84b299 --- /dev/null +++ b/src/akonadicontrol/agentthreadinstance.cpp @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "agentthreadinstance.h" + +#include "agentserverinterface.h" +#include "akonadicontrol_debug.h" + +#include + +using namespace Akonadi; + +AgentThreadInstance::AgentThreadInstance(AgentManager &manager) + : AgentInstance(manager) + , mServiceWatcher(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration) +{ + connect(&mServiceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &AgentThreadInstance::agentServerRegistered); +} + +bool AgentThreadInstance::start(const AgentType &agentInfo) +{ + Q_ASSERT(!identifier().isEmpty()); + if (identifier().isEmpty()) { + return false; + } + + setAgentType(agentInfo.identifier); + mAgentType = agentInfo; + + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), + QDBusConnection::sessionBus()); + if (!agentServer.isValid()) { + qCDebug(AKONADICONTROL_LOG) << "AgentServer not up (yet?)"; + return false; + } + + // TODO: let startAgent return a bool. + agentServer.startAgent(identifier(), agentInfo.identifier, agentInfo.exec); + return true; +} + +void AgentThreadInstance::quit() +{ + AgentInstance::quit(); + + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), + QDBusConnection::sessionBus()); + agentServer.stopAgent(identifier()); +} + +void AgentThreadInstance::restartWhenIdle() +{ + if (status() != 1 && !identifier().isEmpty()) { + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), + QDBusConnection::sessionBus()); + agentServer.stopAgent(identifier()); + agentServer.startAgent(identifier(), agentType(), mAgentType.exec); + } +} + +void AgentThreadInstance::agentServerRegistered() +{ + start(mAgentType); +} + +void Akonadi::AgentThreadInstance::configure(qlonglong windowId) +{ + org::freedesktop::Akonadi::AgentServer agentServer(Akonadi::DBus::serviceName(Akonadi::DBus::AgentServer), + QStringLiteral("/AgentServer"), + QDBusConnection::sessionBus()); + agentServer.agentInstanceConfigure(identifier(), windowId); +} diff --git a/src/akonadicontrol/agentthreadinstance.h b/src/akonadicontrol/agentthreadinstance.h new file mode 100644 index 0000000..bd71d07 --- /dev/null +++ b/src/akonadicontrol/agentthreadinstance.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2010 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include "agentinstance.h" +#include "agenttype.h" + +#include + +namespace Akonadi +{ +class AgentThreadInstance : public AgentInstance +{ + Q_OBJECT +public: + explicit AgentThreadInstance(AgentManager &manager); + ~AgentThreadInstance() override = default; + + bool start(const AgentType &agentInfo) override; + void quit() override; + void restartWhenIdle() override; + void configure(qlonglong windowId) override; + +private Q_SLOTS: + void agentServerRegistered(); + +private: + AgentType mAgentType; + QDBusServiceWatcher mServiceWatcher; +}; + +} + diff --git a/src/akonadicontrol/agenttype.cpp b/src/akonadicontrol/agenttype.cpp new file mode 100644 index 0000000..086b089 --- /dev/null +++ b/src/akonadicontrol/agenttype.cpp @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agenttype.h" +#include "agentmanager.h" +#include "akonadicontrol_debug.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +const QLatin1String AgentType::CapabilityUnique = QLatin1String(AKONADI_AGENT_CAPABILITY_UNIQUE); +const QLatin1String AgentType::CapabilityResource = QLatin1String(AKONADI_AGENT_CAPABILITY_RESOURCE); +const QLatin1String AgentType::CapabilityAutostart = QLatin1String(AKONADI_AGENT_CAPABILITY_AUTOSTART); +const QLatin1String AgentType::CapabilityPreprocessor = QLatin1String(AKONADI_AGENT_CAPABILITY_PREPROCESSOR); +const QLatin1String AgentType::CapabilitySearch = QLatin1String(AKONADI_AGENT_CAPABILITY_SEARCH); + +AgentType::AgentType() +{ +} + +bool AgentType::load(const QString &fileName, AgentManager *manager) +{ + Q_UNUSED(manager) + + if (!KDesktopFile::isDesktopFile(fileName)) { + return false; + } + + KDesktopFile desktopFile(fileName); + KConfigGroup group = desktopFile.desktopGroup(); + + const QStringList keyList(group.keyList()); + for (const QString &key : keyList) { + if (key.startsWith(QLatin1String("X-Akonadi-Custom-"))) { + const QString customKey = key.mid(17, key.length()); + const QStringList val = group.readEntry(key, QStringList()); + if (val.size() == 1) { + custom[customKey] = QVariant(val[0]); + } else { + custom[customKey] = val; + } + } + } + + name = desktopFile.readName(); + comment = desktopFile.readComment(); + icon = desktopFile.readIcon(); + mimeTypes = group.readEntry(QStringLiteral("X-Akonadi-MimeTypes"), QStringList()); + capabilities = group.readEntry(QStringLiteral("X-Akonadi-Capabilities"), QStringList()); + exec = group.readEntry(QStringLiteral("Exec")); + identifier = group.readEntry(QStringLiteral("X-Akonadi-Identifier")); + launchMethod = Process; // Save default + + const QString method = group.readEntry(QStringLiteral("X-Akonadi-LaunchMethod")); + if (method.compare(QLatin1String("AgentProcess"), Qt::CaseInsensitive) == 0) { + launchMethod = Process; + } else if (method.compare(QLatin1String("AgentServer"), Qt::CaseInsensitive) == 0) { + launchMethod = Server; + } else if (method.compare(QLatin1String("AgentLauncher"), Qt::CaseInsensitive) == 0) { + launchMethod = Launcher; + } else if (!method.isEmpty()) { + qCWarning(AKONADICONTROL_LOG) << "Invalid exec method:" << method << "falling back to AgentProcess"; + } + + if (identifier.isEmpty()) { + qCWarning(AKONADICONTROL_LOG) << "Agent desktop file" << fileName << "contains empty identifier"; + return false; + } + if (exec.isEmpty()) { + qCWarning(AKONADICONTROL_LOG) << "Agent desktop file" << fileName << "contains empty Exec entry"; + return false; + } + + // autostart implies unique + if (capabilities.contains(CapabilityAutostart) && !capabilities.contains(CapabilityUnique)) { + capabilities << CapabilityUnique; + } + + // load instance count if needed + if (!capabilities.contains(CapabilityUnique)) { + QSettings agentrc(StandardDirs::agentsConfigFile(StandardDirs::ReadOnly), QSettings::IniFormat); + instanceCounter = agentrc.value(QStringLiteral("InstanceCounters/%1/InstanceCounter").arg(identifier), 0).toInt(); + } + + return true; +} + +void AgentType::save(QSettings *config) const +{ + Q_ASSERT(config); + if (!capabilities.contains(CapabilityUnique)) { + config->setValue(QStringLiteral("InstanceCounters/%1/InstanceCounter").arg(identifier), instanceCounter); + } +} diff --git a/src/akonadicontrol/agenttype.h b/src/akonadicontrol/agenttype.h new file mode 100644 index 0000000..75b57c6 --- /dev/null +++ b/src/akonadicontrol/agenttype.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2007-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class AgentManager; +class QSettings; + +class AgentType +{ +public: + enum LaunchMethod { + Process, /// Standalone executable + Server, /// Agent plugin launched in AgentManager + Launcher /// Agent plugin launched in own process + }; + +public: + AgentType(); + Q_REQUIRED_RESULT bool load(const QString &fileName, AgentManager *manager); + void save(QSettings *config) const; + + QString identifier; + QString name; + QString comment; + QString icon; + QStringList mimeTypes; + QStringList capabilities; + QString exec; + QVariantMap custom; + uint instanceCounter = 0; + LaunchMethod launchMethod = Process; + + static const QLatin1String CapabilityUnique; + static const QLatin1String CapabilityResource; + static const QLatin1String CapabilityAutostart; + static const QLatin1String CapabilityPreprocessor; + static const QLatin1String CapabilitySearch; +}; + diff --git a/src/akonadicontrol/controlmanager.cpp b/src/akonadicontrol/controlmanager.cpp new file mode 100644 index 0000000..0a61134 --- /dev/null +++ b/src/akonadicontrol/controlmanager.cpp @@ -0,0 +1,28 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2007 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "controlmanager.h" + +#include +#include + +#include "controlmanageradaptor.h" + +ControlManager::ControlManager(QObject *parent) + : QObject(parent) +{ + new ControlManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ControlManager"), this); +} + +ControlManager::~ControlManager() +{ +} + +void ControlManager::shutdown() +{ + QTimer::singleShot(0, QCoreApplication::instance(), &QCoreApplication::quit); +} diff --git a/src/akonadicontrol/controlmanager.h b/src/akonadicontrol/controlmanager.h new file mode 100644 index 0000000..7e3e4f7 --- /dev/null +++ b/src/akonadicontrol/controlmanager.h @@ -0,0 +1,36 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2007 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include + +/** + * The control manager provides a dbus method to shutdown + * the Akonadi Control process cleanly. + */ +class ControlManager : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new control manager. + */ + explicit ControlManager(QObject *parent = nullptr); + + /** + * Destroys the control manager. + */ + ~ControlManager(); + +public Q_SLOTS: + /** + * Shutdown the Akonadi Control process cleanly. + */ + void shutdown(); +}; + diff --git a/src/akonadicontrol/main.cpp b/src/akonadicontrol/main.cpp new file mode 100644 index 0000000..f1f013e --- /dev/null +++ b/src/akonadicontrol/main.cpp @@ -0,0 +1,84 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "agentmanager.h" +#include "akonadi_version.h" +#include "akonadicontrol_debug.h" +#include "config-akonadi.h" +#include "controlmanager.h" +#include "processcontrol.h" + +#ifdef WITH_ACCOUNTS +#include "accountsintegration.h" +#endif + +#include + +#include + +#include +#include +#include + +#include +#include + +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +static AgentManager *sAgentManager = nullptr; + +void crashHandler(int /*unused*/) +{ + if (sAgentManager) { + sAgentManager->cleanup(); + } + + exit(255); +} + +int main(int argc, char **argv) +{ + AkUniqueGuiApplication app(argc, argv, Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock), AKONADICONTROL_LOG()); + app.setDescription(QStringLiteral("Akonadi Control Process\nDo not run this manually, use 'akonadictl' instead to start/stop Akonadi.")); + + KAboutData aboutData(QStringLiteral("akonadi_control"), + QStringLiteral("Akonadi Control"), + QStringLiteral(AKONADI_VERSION_STRING), + QStringLiteral("Akonadi Control"), + KAboutLicense::LGPL_V2); + KAboutData::setApplicationData(aboutData); + + app.parseCommandLine(); + + // older Akonadi server versions don't use the lock service yet, so check if one is already running before we try to start another one + // TODO: Remove this legacy check? + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control))) { + qCWarning(AKONADICONTROL_LOG) << "Another Akonadi control process is already running."; + return -1; + } + + ControlManager controlManager; + + AgentManager agentManager(app.commandLineArguments().isSet(QStringLiteral("verbose"))); +#ifdef WITH_ACCOUNTS + AccountsIntegration accountsIntegration(agentManager); +#endif + KCrash::setEmergencySaveFunction(crashHandler); + + QGuiApplication::setFallbackSessionManagementEnabled(false); + + // akonadi_control is started on-demand, no need to auto restart by session. + auto disableSessionManagement = [](QSessionManager &sm) { + sm.setRestartHint(QSessionManager::RestartNever); + }; + QObject::connect(qApp, &QGuiApplication::commitDataRequest, disableSessionManagement); + QObject::connect(qApp, &QGuiApplication::saveStateRequest, disableSessionManagement); + + return app.exec(); +} diff --git a/src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake b/src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake new file mode 100644 index 0000000..3ded20a --- /dev/null +++ b/src/akonadicontrol/org.freedesktop.Akonadi.Control.service.cmake @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Akonadi.Control +Exec=${CMAKE_INSTALL_FULL_BINDIR}/akonadi_control diff --git a/src/akonadicontrol/processcontrol.cpp b/src/akonadicontrol/processcontrol.cpp new file mode 100644 index 0000000..7565414 --- /dev/null +++ b/src/akonadicontrol/processcontrol.cpp @@ -0,0 +1,261 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "processcontrol.h" +#include "akonadicontrol_debug.h" + +#include + +#include +#include + +#include + +#ifdef Q_OS_UNIX +#include +#include +#endif + +using namespace Akonadi; +using namespace std::chrono_literals; + +static const int s_maxCrashCount = 2; + +ProcessControl::ProcessControl(QObject *parent) + : QObject(parent) + , mShutdownTimeout(1s) +{ + connect(&mProcess, &QProcess::errorOccurred, this, &ProcessControl::slotError); + connect(&mProcess, QOverload::of(&QProcess::finished), this, &ProcessControl::slotFinished); + mProcess.setProcessChannelMode(QProcess::ForwardedChannels); + + if (Akonadi::Instance::hasIdentifier()) { + QProcessEnvironment env = mProcess.processEnvironment(); + if (env.isEmpty()) { + env = QProcessEnvironment::systemEnvironment(); + } + env.insert(QStringLiteral("AKONADI_INSTANCE"), Akonadi::Instance::identifier()); + mProcess.setProcessEnvironment(env); + } +} + +ProcessControl::~ProcessControl() +{ + stop(); +} + +void ProcessControl::start(const QString &application, const QStringList &arguments, CrashPolicy policy) +{ + mFailedToStart = false; + + mApplication = application; + mArguments = arguments; + mPolicy = policy; + + start(); +} + +void ProcessControl::setCrashPolicy(CrashPolicy policy) +{ + mPolicy = policy; +} + +void ProcessControl::stop() +{ + if (mProcess.state() != QProcess::NotRunning) { + mProcess.waitForFinished(mShutdownTimeout.count()); + mProcess.terminate(); + mProcess.waitForFinished(std::chrono::milliseconds{10000}.count()); + mProcess.kill(); + } +} + +void ProcessControl::slotError(QProcess::ProcessError error) +{ + switch (error) { + case QProcess::Crashed: + mCrashCount++; + // do nothing, we'll respawn in slotFinished + break; + case QProcess::FailedToStart: + default: + mFailedToStart = true; + break; + } + + qCWarning(AKONADICONTROL_LOG) << "ProcessControl: Application" << mApplication << "stopped unexpectedly (" << mProcess.errorString() << ")"; +} + +void ProcessControl::slotFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitStatus == QProcess::CrashExit) { + if (mPolicy == RestartOnCrash) { + // don't try to start an unstartable application + if (!mFailedToStart && mCrashCount <= s_maxCrashCount) { + qCWarning(AKONADICONTROL_LOG, "Application '%s' crashed! %d restarts left.", qPrintable(mApplication), s_maxCrashCount - mCrashCount); + start(); + Q_EMIT restarted(); + } else { + if (mFailedToStart) { + qCCritical(AKONADICONTROL_LOG, "Application '%s' failed to start!", qPrintable(mApplication)); + } else { + qCCritical(AKONADICONTROL_LOG, "Application '%s' crashed too often. Giving up!", qPrintable(mApplication)); + } + mPolicy = StopOnCrash; + Q_EMIT unableToStart(); + return; + } + } else { + qCCritical(AKONADICONTROL_LOG, "Application '%s' crashed. No restart!", qPrintable(mApplication)); + } + } else { + if (exitCode != 0) { + qCWarning(AKONADICONTROL_LOG, + "ProcessControl: Application '%s' returned with exit code %d (%s)", + qPrintable(mApplication), + exitCode, + qPrintable(mProcess.errorString())); + if (mPolicy == RestartOnCrash) { + if (mCrashCount > s_maxCrashCount) { + qCCritical(AKONADICONTROL_LOG) << mApplication << "crashed too often and will not be restarted!"; + mPolicy = StopOnCrash; + Q_EMIT unableToStart(); + return; + } + ++mCrashCount; + QTimer::singleShot(std::chrono::seconds{60}, this, &ProcessControl::resetCrashCount); + if (!mFailedToStart) { // don't try to start an unstartable application + start(); + Q_EMIT restarted(); + } + } + } else { + if (mRestartOnceOnExit) { + mRestartOnceOnExit = false; + qCInfo(AKONADICONTROL_LOG, "Restarting application '%s'.", qPrintable(mApplication)); + start(); + } else { + qCInfo(AKONADICONTROL_LOG, "Application '%s' exited normally...", qPrintable(mApplication)); + Q_EMIT unableToStart(); + } + } + } +} + +static bool listContains(const QStringList &list, const QString &pattern) +{ + for (const QString &s : list) { + if (s.contains(pattern)) { + return true; + } + } + return false; +} + +void ProcessControl::start() +{ + // Prefer akonadiserver from the builddir + mApplication = StandardDirs::findExecutable(mApplication); + +#ifdef Q_OS_UNIX + QString agentValgrind = akGetEnv("AKONADI_VALGRIND"); + if (!agentValgrind.isEmpty() && (mApplication.contains(agentValgrind) || listContains(mArguments, agentValgrind))) { + mArguments.prepend(mApplication); + const QString originalArguments = mArguments.join(QString::fromLocal8Bit(" ")); + mApplication = QString::fromLocal8Bit("valgrind"); + + const QString valgrindSkin = akGetEnv("AKONADI_VALGRIND_SKIN", QString::fromLocal8Bit("memcheck")); + mArguments.prepend(QLatin1String("--tool=") + valgrindSkin); + + const QString valgrindOptions = akGetEnv("AKONADI_VALGRIND_OPTIONS"); + if (!valgrindOptions.isEmpty()) { + mArguments = valgrindOptions.split(QLatin1Char(' '), Qt::SkipEmptyParts) << mArguments; + } + + qCDebug(AKONADICONTROL_LOG); + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Valgrinding process" << originalArguments; + if (!valgrindSkin.isEmpty()) { + qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Valgrind skin:" << valgrindSkin; + } + if (!valgrindOptions.isEmpty()) { + qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Additional Valgrind options:" << valgrindOptions; + } + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG); + } + + const QString agentHeaptrack = akGetEnv("AKONADI_HEAPTRACK"); + if (!agentHeaptrack.isEmpty() && (mApplication.contains(agentHeaptrack) || listContains(mArguments, agentHeaptrack))) { + mArguments.prepend(mApplication); + const QString originalArguments = mArguments.join(QLatin1Char(' ')); + mApplication = QStringLiteral("heaptrack"); + + qCDebug(AKONADICONTROL_LOG); + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Heaptracking process" << originalArguments; + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG); + } + + const QString agentPerf = akGetEnv("AKONADI_PERF"); + if (!agentPerf.isEmpty() && (mApplication.contains(agentPerf) || listContains(mArguments, agentPerf))) { + mArguments.prepend(mApplication); + const QString originalArguments = mArguments.join(QLatin1Char(' ')); + mApplication = QStringLiteral("perf"); + + mArguments = QStringList{QStringLiteral("record"), QStringLiteral("--call-graph"), QStringLiteral("dwarf"), QStringLiteral("--")} + mArguments; + + qCDebug(AKONADICONTROL_LOG); + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Perf-recording process" << originalArguments; + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG); + } +#endif + + mProcess.start(mApplication, mArguments); + if (!mProcess.waitForStarted()) { + qCWarning(AKONADICONTROL_LOG, "ProcessControl: Unable to start application '%s' (%s)", qPrintable(mApplication), qPrintable(mProcess.errorString())); + Q_EMIT unableToStart(); + return; + } else { + QString agentDebug = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_WAIT")); + auto pid = mProcess.processId(); + if (!agentDebug.isEmpty() && mApplication.contains(agentDebug)) { + qCDebug(AKONADICONTROL_LOG); + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG) << "ProcessControl: Suspending process" << mApplication; +#ifdef Q_OS_UNIX + qCDebug(AKONADICONTROL_LOG) << "'gdb --pid" << pid << "' to debug"; + qCDebug(AKONADICONTROL_LOG) << "'kill -SIGCONT" << pid << "' to continue"; + kill(pid, SIGSTOP); +#endif +#ifdef Q_OS_WIN + qCDebug(AKONADICONTROL_LOG) << "PID:" << pid; + qCDebug(AKONADICONTROL_LOG) << "Process is waiting for a debugger..."; + // the agent process will wait for a debugger to be attached in AgentBase::debugAgent() +#endif + qCDebug(AKONADICONTROL_LOG) << "============================================================"; + qCDebug(AKONADICONTROL_LOG); + } + } +} + +void ProcessControl::resetCrashCount() +{ + mCrashCount = 0; +} + +bool ProcessControl::isRunning() const +{ + return mProcess.state() != QProcess::NotRunning; +} + +void ProcessControl::setShutdownTimeout(std::chrono::milliseconds timeout) +{ + mShutdownTimeout = timeout; +} diff --git a/src/akonadicontrol/processcontrol.h b/src/akonadicontrol/processcontrol.h new file mode 100644 index 0000000..ed58995 --- /dev/null +++ b/src/akonadicontrol/processcontrol.h @@ -0,0 +1,127 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace Akonadi +{ +/** + * This class starts and observes a process. Depending on the + * policy it also restarts the process when it crashes. + */ +class ProcessControl : public QObject +{ + Q_OBJECT + +public: + /** + * Theses enums describe the behaviour when the observed + * application crashed. + * + * @li StopOnCrash - The application won't be restarted. + * @li RestartOnCrash - The application is restarted with the same arguments. + */ + enum CrashPolicy { + StopOnCrash, + RestartOnCrash, + }; + + /** + * Creates a new process control. + * + * @param parent The parent object. + */ + explicit ProcessControl(QObject *parent = nullptr); + + /** + * Destroys the process control. + */ + ~ProcessControl(); + + /** + * Starts the @p application with the given list of @p arguments. + */ + void start(const QString &application, const QStringList &arguments = QStringList(), CrashPolicy policy = RestartOnCrash); + + /** + * Starts the process with the previously set application and arguments. + */ + void start(); + + /** + * Stops the currently running application. + */ + void stop(); + + /** + * Sets the crash policy. + */ + void setCrashPolicy(CrashPolicy policy); + + /** + * Restart the application the next time it exits normally. + */ + void restartOnceWhenFinished() + { + mRestartOnceOnExit = true; + } + + /** + * Returns true if the process is currently running. + */ + Q_REQUIRED_RESULT bool isRunning() const; + + /** + * Sets the time (in msecs) we wait for the process to shut down before we send terminate/kill signals. + * Default is 1 second. + * Note that it is your responsiblility to ask the process to quit, otherwise this is just + * pointless waiting. + */ + void setShutdownTimeout(std::chrono::milliseconds timeout); + +Q_SIGNALS: + /** + * This signal is emitted whenever the observed application + * writes something to stderr. + * + * @param errorMsg The error output of the observed application. + */ + void processErrorMessages(const QString &errorMsg); + + /** + * This signal is emitted when the server is restarted after a crash. + */ + void restarted(); + + /** + * Emitted if the process could not be started since it terminated + * too often. + */ + void unableToStart(); + +private Q_SLOTS: + void slotError(QProcess::ProcessError); + void slotFinished(int, QProcess::ExitStatus); + void resetCrashCount(); + +private: + QProcess mProcess; + QString mApplication; + QStringList mArguments; + CrashPolicy mPolicy = RestartOnCrash; + bool mFailedToStart = false; + int mCrashCount = 0; + bool mRestartOnceOnExit = false; + std::chrono::milliseconds mShutdownTimeout; +}; + +} + diff --git a/src/akonadictl/CMakeLists.txt b/src/akonadictl/CMakeLists.txt new file mode 100644 index 0000000..9504687 --- /dev/null +++ b/src/akonadictl/CMakeLists.txt @@ -0,0 +1,38 @@ +########### next target ############### + + +add_executable(akonadictl) +ecm_qt_declare_logging_category(akonadictl HEADER akonadictl_debug.h IDENTIFIER AKONADICTL_LOG CATEGORY_NAME org.kde.pim.akonadictl + DESCRIPTION "akonadi (Akonadi Control)" + OLD_CATEGORY_NAMES log_akonadictl + EXPORT AKONADI + ) + + +qt_add_dbus_interfaces(akonadictl_SRCS + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Janitor.xml +) +target_sources(akonadictl PRIVATE ${akonadictl_SRCS} + akonadistarter.cpp + main.cpp +) + +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(akonadictl PROPERTIES UNITY_BUILD ON) +endif() + +set_target_properties(akonadictl PROPERTIES MACOSX_BUNDLE FALSE) +set_target_properties(akonadictl PROPERTIES OUTPUT_NAME akonadictl) +ecm_mark_nongui_executable(akonadictl) +target_link_libraries(akonadictl + akonadi_shared + KF5AkonadiPrivate + KF5::CoreAddons + Qt::Core + Qt::DBus +) + +install(TARGETS akonadictl + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) diff --git a/src/akonadictl/akonadistarter.cpp b/src/akonadictl/akonadistarter.cpp new file mode 100644 index 0000000..944d12a --- /dev/null +++ b/src/akonadictl/akonadistarter.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadistarter.h" +#include "akonadictl_debug.h" + +#include + +#include +#include + +#include +#include +#include +#include + +#include + +AkonadiStarter::AkonadiStarter(QObject *parent) + : QObject(parent) + , mWatcher(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration) +{ + connect(&mWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this]() { + mRegistered = true; + QCoreApplication::instance()->quit(); + }); +} + +bool AkonadiStarter::start(bool verbose) +{ + qCInfo(AKONADICTL_LOG) << "Starting Akonadi Server..."; + + QStringList serverArgs; + if (Akonadi::Instance::hasIdentifier()) { + serverArgs << QStringLiteral("--instance") << Akonadi::Instance::identifier(); + } + if (verbose) { + serverArgs << QStringLiteral("--verbose"); + } + + const bool ok = QProcess::startDetached(QStringLiteral("akonadi_control"), serverArgs); + if (!ok) { + std::cerr << "Error: unable to execute binary akonadi_control" << std::endl; + return false; + } + + // safety timeout + QTimer::singleShot(std::chrono::seconds{5}, QCoreApplication::instance(), &QCoreApplication::quit); + // wait for the server to register with D-Bus + QCoreApplication::instance()->exec(); + + if (!mRegistered) { + std::cerr << "Error: akonadi_control was started but didn't register at D-Bus session bus." << std::endl + << "Make sure your system is set up correctly!" << std::endl; + return false; + } + + qCInfo(AKONADICTL_LOG) << " done."; + return true; +} diff --git a/src/akonadictl/akonadistarter.h b/src/akonadictl/akonadistarter.h new file mode 100644 index 0000000..6c93ca5 --- /dev/null +++ b/src/akonadictl/akonadistarter.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class AkonadiStarter : public QObject +{ + Q_OBJECT +public: + explicit AkonadiStarter(QObject *parent = nullptr); + Q_REQUIRED_RESULT bool start(bool verbose); + +private: + QDBusServiceWatcher mWatcher; + bool mRegistered = false; +}; + diff --git a/src/akonadictl/main.cpp b/src/akonadictl/main.cpp new file mode 100644 index 0000000..e0c342b --- /dev/null +++ b/src/akonadictl/main.cpp @@ -0,0 +1,278 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2007 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "akonadifull-version.h" +#include "akonadistarter.h" +#include "controlmanagerinterface.h" +#include "janitorinterface.h" + +#include +#include +#include +#include + +#include +#include +#include + +static bool startServer(bool verbose) +{ + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control)) + || QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server))) { + std::cerr << "Akonadi is already running." << std::endl; + return false; + } + AkonadiStarter starter; + return starter.start(verbose); +} + +static bool stopServer() +{ + org::freedesktop::Akonadi::ControlManager iface(Akonadi::DBus::serviceName(Akonadi::DBus::Control), + QStringLiteral("/ControlManager"), + QDBusConnection::sessionBus(), + nullptr); + if (!iface.isValid()) { + std::cerr << "Akonadi is not running." << std::endl; + return false; + } + + iface.shutdown(); + + return true; +} + +static bool isAkonadiServerRunning() +{ + return QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Server)); +} + +static bool checkAkonadiControlStatus() +{ + const bool registered = QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control)); + std::cerr << "Akonadi Control: " << (registered ? "running" : "stopped") << std::endl; + return registered; +} + +static bool checkAkonadiServerStatus() +{ + const bool registered = isAkonadiServerRunning(); + std::cerr << "Akonadi Server: " << (registered ? "running" : "stopped") << std::endl; + return registered; +} + +static bool checkSearchSupportStatus() +{ + QStringList searchMethods{QStringLiteral("Remote Search")}; + + const QString pluginOverride = QString::fromLatin1(qgetenv("AKONADI_OVERRIDE_SEARCHPLUGIN")); + if (!pluginOverride.isEmpty()) { + searchMethods << pluginOverride; + } else { + const QStringList dirs = QCoreApplication::libraryPaths(); + for (const QString &pluginDir : dirs) { + QDir dir(pluginDir + QLatin1String("/akonadi/")); + const QStringList pluginFiles = dir.entryList(QDir::Files); + for (const QString &pluginFileName : pluginFiles) { + QPluginLoader loader(dir.absolutePath() + QLatin1Char('/') + pluginFileName); + const QVariantMap metadata = loader.metaData().value(QStringLiteral("MetaData")).toVariant().toMap(); + if (metadata.value(QStringLiteral("X-Akonadi-PluginType")).toString() != QLatin1String("SearchPlugin")) { + continue; + } + if (!metadata.value(QStringLiteral("X-Akonadi-LoadByDefault"), true).toBool()) { + continue; + } + searchMethods << metadata.value(QStringLiteral("X-Akonadi-PluginName")).toString(); + } + } + } + + // There's always at least server-search available + std::cerr << "Akonadi Server Search Support: available (" << searchMethods.join(QLatin1String(", ")).toStdString() << ")" << std::endl; + return true; +} + +static bool checkAvailableAgentTypes() +{ + const auto dirs = Akonadi::StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents")); + QStringList types; + for (const QString &pluginDir : dirs) { + QDir dir(pluginDir); + const QStringList plugins = dir.entryList(QStringList() << QStringLiteral("*.desktop"), QDir::Files); + for (const QString &plugin : plugins) { + QSettings pluginInfo(pluginDir + QLatin1Char('/') + plugin, QSettings::IniFormat); + pluginInfo.beginGroup(QStringLiteral("Desktop Entry")); + types << pluginInfo.value(QStringLiteral("X-Akonadi-Identifier")).toString(); + } + } + + // Remove duplicates from multiple pluginDirs + types.removeDuplicates(); + types.sort(); + + std::cerr << "Available Agent Types: "; + if (types.isEmpty()) { + std::cerr << "No agent types found!" << std::endl; + } else { + std::cerr << types.join(QLatin1String(", ")).toStdString() << std::endl; + } + + return true; +} + +static bool instanceRunning(const QString &instanceName = {}) +{ + const auto oldInstance = Akonadi::Instance::identifier(); + Akonadi::Instance::setIdentifier(instanceName); + const auto service = Akonadi::DBus::serviceName(Akonadi::DBus::Control); + Akonadi::Instance::setIdentifier(oldInstance); + + return QDBusConnection::sessionBus().interface()->isServiceRegistered(service); +} + +static void listInstances() +{ + struct Instance { + QString name; + bool running; + }; + QVector instances{{QStringLiteral("(default)"), instanceRunning()}}; +#ifdef Q_OS_WIN + const QDir instanceDir(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/akonadi/config/instance")); +#else + const QDir instanceDir(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/akonadi/instance")); +#endif + if (instanceDir.exists()) { + const auto list = instanceDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const auto &e : list) { + instances.push_back({e, instanceRunning(e)}); + } + } + + for (const auto &i : std::as_const(instances)) { + std::cout << i.name.toStdString(); + if (i.running) { + std::cout << " (running)"; + } + std::cout << std::endl; + } +} + +static bool statusServer() +{ + checkAkonadiControlStatus(); + checkAkonadiServerStatus(); + checkSearchSupportStatus(); + checkAvailableAgentTypes(); + return true; +} + +static void runJanitor(const QString &operation) +{ + if (!isAkonadiServerRunning()) { + std::cerr << "Akonadi Server is not running, " << operation.toStdString() << " will not run" << std::endl; + return; + } + + org::freedesktop::Akonadi::Janitor janitor(Akonadi::DBus::serviceName(Akonadi::DBus::StorageJanitor), + QStringLiteral(AKONADI_DBUS_STORAGEJANITOR_PATH), + QDBusConnection::sessionBus()); + QObject::connect(&janitor, &org::freedesktop::Akonadi::Janitor::information, [](const QString &msg) { + std::cerr << msg.toStdString() << std::endl; + }); + QObject::connect(&janitor, &org::freedesktop::Akonadi::Janitor::done, []() { + qApp->exit(); + }); + janitor.asyncCall(operation); + qApp->exec(); +} + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv); + + app.setDescription( + QStringLiteral("Akonadi server manipulation tool\n\n" + "Commands:\n" + " start Starts the Akonadi server with all its processes\n" + " stop Stops the Akonadi server and all its processes cleanly\n" + " restart Restart Akonadi server with all its processes\n" + " status Shows a status overview of the Akonadi server\n" + " instances List all existing Akonadi instances\n" + " vacuum Vacuum internal storage (WARNING: needs a lot of time and disk\n" + " space!)\n" + " fsck Check (and attempt to fix) consistency of the internal storage\n" + " (can take some time)")); + + KAboutData aboutData(QStringLiteral("akonadictl"), + QStringLiteral("akonadictl"), + QStringLiteral(AKONADI_FULL_VERSION), + QStringLiteral("akonadictl"), + KAboutLicense::LGPL_V2); + KAboutData::setApplicationData(aboutData); + + app.addPositionalCommandLineOption(QStringLiteral("command"), + QStringLiteral("Command to execute"), + QStringLiteral("start|stop|restart|status|vacuum|fsck|instances")); + + app.parseCommandLine(); + + const auto &cmdArgs = app.commandLineArguments(); + const QStringList commands = cmdArgs.positionalArguments(); + if (commands.size() != 1) { + app.printUsage(); + return -1; + } + const bool verbose = cmdArgs.isSet(QStringLiteral("verbose")); + + const QString command = commands[0]; + if (command == QLatin1String("start")) { + if (!startServer(verbose)) { + return 3; + } + } else if (command == QLatin1String("stop")) { + if (!stopServer()) { + return 4; + } + } else if (command == QLatin1String("status")) { + if (!statusServer()) { + return 5; + } + } else if (command == QLatin1String("restart")) { + if (!stopServer()) { + return 4; + } else { + do { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } while (QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::Control))); + if (!startServer(verbose)) { + return 3; + } + } + } else if (command == QLatin1String("vacuum")) { + runJanitor(QStringLiteral("vacuum")); + } else if (command == QLatin1String("fsck")) { + runJanitor(QStringLiteral("check")); + } else if (command == QLatin1String("instances")) { + listInstances(); + } else { + app.printUsage(); + return -1; + } + return 0; +} diff --git a/src/asapcat/CMakeLists.txt b/src/asapcat/CMakeLists.txt new file mode 100644 index 0000000..931d403 --- /dev/null +++ b/src/asapcat/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable(asapcat) +target_sources(asapcat PRIVATE + main.cpp + session.cpp + ) +set_target_properties(asapcat PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(asapcat + akonadi_shared + KF5AkonadiPrivate + Qt::Core + Qt::Network +) + +install(TARGETS asapcat + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) diff --git a/src/asapcat/main.cpp b/src/asapcat/main.cpp new file mode 100644 index 0000000..d32eab9 --- /dev/null +++ b/src/asapcat/main.cpp @@ -0,0 +1,36 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "session.h" + +#include + +#include + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv); + app.setDescription( + QStringLiteral("Akonadi ASAP cat\n" + "This is a development tool, only use this if you know what you are doing.")); + + app.addPositionalCommandLineOption(QStringLiteral("input"), QStringLiteral("Input file to read commands from")); + app.parseCommandLine(); + + const QStringList args = app.commandLineArguments().positionalArguments(); + if (args.isEmpty()) { + app.printUsage(); + return -1; + } + + Session session(args[0]); + QObject::connect(&session, &Session::disconnected, QCoreApplication::instance(), &QCoreApplication::quit); + QMetaObject::invokeMethod(&session, &Session::connectToHost, Qt::QueuedConnection); + + const int result = app.exec(); + session.printStats(); + return result; +} diff --git a/src/asapcat/session.cpp b/src/asapcat/session.cpp new file mode 100644 index 0000000..2e4f2e6 --- /dev/null +++ b/src/asapcat/session.cpp @@ -0,0 +1,132 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "session.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +Session::Session(const QString &input, QObject *parent) + : QObject(parent) +{ + auto file = new QFile(this); + if (input != QLatin1String("-")) { + file->setFileName(input); + if (!file->open(QFile::ReadOnly)) { + qFatal("Failed to open %s", qPrintable(input)); + } + } else { + // ### does that work on Windows? + const int flags = fcntl(0, F_GETFL); + fcntl(0, F_SETFL, flags | O_NONBLOCK); // NOLINT(hicpp-signed-bitwise) + + if (!file->open(stdin, QFile::ReadOnly | QFile::Unbuffered)) { + qFatal("Failed to open stdin!"); + } + m_notifier = new QSocketNotifier(0, QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, this, &Session::inputAvailable); + } + m_input = file; +} + +Session::~Session() +{ +} + +void Session::connectToHost() +{ + const QSettings connectionSettings(Akonadi::StandardDirs::connectionConfigFile(), QSettings::IniFormat); + + QString serverAddress; +#ifdef Q_OS_WIN + serverAddress = connectionSettings.value(QStringLiteral("Data/NamedPipe"), QString()).toString(); +#else + serverAddress = connectionSettings.value(QStringLiteral("Data/UnixPath"), QString()).toString(); +#endif + if (serverAddress.isEmpty()) { + qFatal("Unable to determine server address."); + } + + auto socket = new QLocalSocket(this); + connect(socket, &QLocalSocket::errorOccurred, this, &Session::serverError); + connect(socket, &QLocalSocket::disconnected, this, &Session::serverDisconnected); + connect(socket, &QIODevice::readyRead, this, &Session::serverRead); + connect(socket, &QLocalSocket::connected, this, &Session::inputAvailable); + + m_session = socket; + socket->connectToServer(serverAddress); + + m_connectionTime.start(); +} + +void Session::inputAvailable() +{ + if (!m_session->isOpen()) { + return; + } + + if (m_notifier) { + m_notifier->setEnabled(false); + } + + if (m_input->atEnd()) { + return; + } + + QByteArray buffer(1024, Qt::Uninitialized); + qint64 readSize = 0; + + while ((readSize = m_input->read(buffer.data(), buffer.size())) > 0) { + m_session->write(buffer.constData(), readSize); + m_sentBytes += readSize; + } + + if (m_notifier) { + m_notifier->setEnabled(true); + } +} + +void Session::serverDisconnected() +{ + QCoreApplication::exit(0); +} + +void Session::serverError(QLocalSocket::LocalSocketError socketError) +{ + if (socketError == QLocalSocket::PeerClosedError) { + QCoreApplication::exit(0); + return; + } + + std::cerr << qPrintable(m_session->errorString()); + QCoreApplication::exit(1); +} + +void Session::serverRead() +{ + QByteArray buffer(1024, Qt::Uninitialized); + qint64 readSize = 0; + + while ((readSize = m_session->read(buffer.data(), buffer.size())) > 0) { + write(1, buffer.data(), readSize); + m_receivedBytes += readSize; + } +} + +void Session::printStats() const +{ + std::cerr << "Connection time: " << m_connectionTime.elapsed() << " ms" << std::endl; + std::cerr << "Sent: " << m_sentBytes << " bytes" << std::endl; + std::cerr << "Received: " << m_receivedBytes << " bytes" << std::endl; +} diff --git a/src/asapcat/session.h b/src/asapcat/session.h new file mode 100644 index 0000000..936d174 --- /dev/null +++ b/src/asapcat/session.h @@ -0,0 +1,47 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include +#include + +class QIODevice; +class QSocketNotifier; + +/** ASAP CLI session. */ +class Session : public QObject +{ + Q_OBJECT +public: + explicit Session(const QString &input, QObject *parent = nullptr); + ~Session(); + + void printStats() const; + +public Q_SLOTS: + void connectToHost(); + +Q_SIGNALS: + void disconnected(); + +private Q_SLOTS: + void inputAvailable(); + void serverDisconnected(); + void serverError(QLocalSocket::LocalSocketError socketError); + void serverRead(); + +private: + QIODevice *m_input = nullptr; + QIODevice *m_session = nullptr; + QSocketNotifier *m_notifier = nullptr; + + QElapsedTimer m_connectionTime; + qint64 m_receivedBytes = 0; + qint64 m_sentBytes = 0; +}; + diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..87536c1 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,365 @@ +configure_file(akonaditests_export.h.in "${CMAKE_CURRENT_BINARY_DIR}/akonaditests_export.h") + +set(akonadicore_base_SRCS + agentconfigurationbase.cpp + agentconfigurationfactorybase.cpp + agentconfigurationmanager.cpp + agentinstance.cpp + agentmanager.cpp + agenttype.cpp + asyncselectionhandler.cpp + attributefactory.cpp + attributestorage.cpp + braveheart.cpp + cachepolicy.cpp + changemediator_p.cpp + changenotification.cpp + changenotificationdependenciesfactory.cpp + changerecorder.cpp + changerecorder_p.cpp + changerecorderjournal.cpp + config.cpp + connection.cpp + collection.cpp + collectionfetchscope.cpp + collectionpathresolver.cpp + collectionstatistics.cpp + collectionsync.cpp + conflicthandler.cpp + control.cpp + entitycache.cpp + exception.cpp + firstrun.cpp + gidextractor.cpp + item.cpp + itemchangelog.cpp + itemfetchscope.cpp + itemmonitor.cpp + itemserializer.cpp + itemserializerplugin.cpp + itemsync.cpp + mimetypechecker.cpp + monitor.cpp + monitor_p.cpp + notificationsource_p.cpp + notificationsubscriber.cpp + partfetcher.cpp + pastehelper.cpp + pluginloader.cpp + protocolhelper.cpp + remotelog.cpp + relation.cpp + relationsync.cpp + searchquery.cpp + servermanager.cpp + session.cpp + sessionthread.cpp + specialcollections.cpp + tag.cpp + tagfetchscope.cpp + tagsync.cpp + trashsettings.cpp + typepluginloader.cpp +) + +ecm_generate_headers(AkonadiCore_base_HEADERS + HEADER_NAMES + AbstractDifferencesReporter + AgentConfigurationBase + AgentConfigurationFactoryBase + AgentInstance + AgentManager + AgentType + AttributeFactory + CachePolicy + ChangeNotification + ChangeRecorder + Collection + CollectionFetchScope + CollectionStatistics + CollectionUtils + Control + DifferencesAlgorithmInterface + ExceptionBase + GidExtractorInterface + Item + ItemFetchScope + ItemMonitor + ItemSerializerPlugin + ItemSync + MimeTypeChecker + NotificationSubscriber + Monitor + PartFetcher + Relation + SearchQuery + ServerManager + Session + SpecialCollections + Supertrait + Tag + TagFetchScope + TrashSettings + CollectionPathResolver + REQUIRED_HEADERS AkonadiCore_base_HEADERS +) + +set(akonadicore_attributes_SRCS + attributes/attribute.cpp + attributes/collectioncolorattribute.cpp + attributes/collectionquotaattribute.cpp + attributes/collectionidentificationattribute.cpp + attributes/collectionrightsattribute.cpp + attributes/entityannotationsattribute.cpp + attributes/entitydeletedattribute.cpp + attributes/entitydeletedattribute.cpp + attributes/entitydisplayattribute.cpp + attributes/entityhiddenattribute.cpp + attributes/favoritecollectionattribute.cpp + attributes/indexpolicyattribute.cpp + attributes/persistentsearchattribute.cpp + attributes/specialcollectionattribute.cpp + attributes/tagattribute.cpp +) + +ecm_generate_headers(AkonadiCore_attributes_HEADERS + HEADER_NAMES + Attribute + CollectionColorAttribute + CollectionIdentificationAttribute + CollectionQuotaAttribute + EntityAnnotationsAttribute + EntityDeletedAttribute + EntityDisplayAttribute + EntityHiddenAttribute + FavoriteCollectionAttribute + IndexPolicyAttribute + PersistentSearchAttribute + SpecialCollectionAttribute + TagAttribute + REQUIRED_HEADERS AkonadiCore_attributes_HEADERS + RELATIVE attributes +) + +set(akonadicore_models_SRCS + models/agentfilterproxymodel.cpp + models/agentinstancemodel.cpp + models/agenttypemodel.cpp + models/collectionfilterproxymodel.cpp + models/entitymimetypefiltermodel.cpp + models/entityorderproxymodel.cpp + models/entityrightsfiltermodel.cpp + models/entitytreemodel.cpp + models/entitytreemodel_p.cpp + models/favoritecollectionsmodel.cpp + models/recursivecollectionfilterproxymodel.cpp + models/selectionproxymodel.cpp + models/statisticsproxymodel.cpp + models/subscriptionmodel.cpp + models/tagmodel.cpp + models/tagmodel_p.cpp + models/trashfilterproxymodel.cpp +) + +ecm_generate_headers(AkonadiCore_models_HEADERS + HEADER_NAMES + AgentFilterProxyModel + AgentInstanceModel + AgentTypeModel + CollectionFilterProxyModel + EntityMimeTypeFilterModel + EntityOrderProxyModel + EntityRightsFilterModel + EntityTreeModel + FavoriteCollectionsModel + RecursiveCollectionFilterProxyModel + SelectionProxyModel + StatisticsProxyModel + TagModel + TrashFilterProxyModel + REQUIRED_HEADERS AkonadiCore_models_HEADERS + RELATIVE models +) + +set(akonadicore_jobs_SRCS + jobs/agentinstancecreatejob.cpp + jobs/collectionattributessynchronizationjob.cpp + jobs/collectioncopyjob.cpp + jobs/collectioncreatejob.cpp + jobs/collectiondeletejob.cpp + jobs/collectionfetchjob.cpp + jobs/collectionmodifyjob.cpp + jobs/collectionmovejob.cpp + jobs/collectionstatisticsjob.cpp + jobs/invalidatecachejob.cpp + jobs/itemcopyjob.cpp + jobs/itemcreatejob.cpp + jobs/itemdeletejob.cpp + jobs/itemfetchjob.cpp + jobs/itemmodifyjob.cpp + jobs/itemmovejob.cpp + jobs/itemsearchjob.cpp + jobs/job.cpp + jobs/kjobprivatebase.cpp + jobs/linkjob.cpp + jobs/recursiveitemfetchjob.cpp + jobs/resourceselectjob.cpp + jobs/resourcesynchronizationjob.cpp + jobs/relationfetchjob.cpp + jobs/relationcreatejob.cpp + jobs/relationdeletejob.cpp + jobs/searchcreatejob.cpp + jobs/searchresultjob.cpp + jobs/specialcollectionsdiscoveryjob.cpp + jobs/specialcollectionshelperjobs.cpp + jobs/specialcollectionsrequestjob.cpp + jobs/subscriptionjob.cpp + jobs/tagcreatejob.cpp + jobs/tagdeletejob.cpp + jobs/tagfetchjob.cpp + jobs/tagmodifyjob.cpp + jobs/transactionjobs.cpp + jobs/transactionsequence.cpp + jobs/trashjob.cpp + jobs/trashrestorejob.cpp + jobs/unlinkjob.cpp +) + +ecm_generate_headers(AkonadiCore_jobs_HEADERS + HEADER_NAMES + AgentInstanceCreateJob + CollectionAttributesSynchronizationJob + CollectionCopyJob + CollectionCreateJob + CollectionDeleteJob + CollectionFetchJob + CollectionModifyJob + CollectionMoveJob + CollectionStatisticsJob + ItemCopyJob + ItemCreateJob + ItemDeleteJob + ItemFetchJob + ItemModifyJob + ItemMoveJob + ItemSearchJob + Job + LinkJob + RecursiveItemFetchJob + ResourceSynchronizationJob + RelationFetchJob + RelationCreateJob + RelationDeleteJob + SearchCreateJob + SpecialCollectionsDiscoveryJob + SpecialCollectionsRequestJob + TagCreateJob + TagDeleteJob + TagFetchJob + TagModifyJob + TransactionJobs + TransactionSequence + TrashJob + TrashRestoreJob + UnlinkJob + REQUIRED_HEADERS AkonadiCore_jobs_HEADERS + RELATIVE jobs +) + +set(akonadicore_dbus_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml) +qt_add_dbus_interface(akonadicore_dbus_SRCS ${akonadicore_dbus_xml} notificationmanagerinterface) + +set(akonadicore_dbus_xml ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml) +set_source_files_properties(${akonadicore_dbus_xml} PROPERTIES INCLUDE "${Akonadi_SOURCE_DIR}/src/private/protocol_p.h" ) +qt_add_dbus_interface(akonadicore_dbus_SRCS ${akonadicore_dbus_xml} notificationsourceinterface) + +qt_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml) +qt_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml) +qt_add_dbus_interfaces(akonadicore_dbus_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml) +qt_add_dbus_interfaces(akonadicore_dbus_SRCS + ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml) + +set(akonadicore_SRCS + ${akonadicore_base_SRCS} + ${akonadicore_attributes_SRCS} + ${akonadicore_jobs_SRCS} + ${akonadicore_models_SRCS} + ${akonadicore_dbus_SRCS} +) + +ecm_qt_declare_logging_category(akonadicore_SRCS HEADER akonadicore_debug.h IDENTIFIER AKONADICORE_LOG CATEGORY_NAME org.kde.pim.akonadicore + DESCRIPTION "akonadi (Akonadi Core Library)" + OLD_CATEGORY_NAMES log_akonadicore + EXPORT AKONADI + ) + + +add_library(KF5AkonadiCore ${akonadicore_SRCS}) +#if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) +# set_target_properties(KF5AkonadiCore PROPERTIES UNITY_BUILD ON) +#endif() + +generate_export_header(KF5AkonadiCore BASE_NAME akonadicore) + +add_library(KF5::AkonadiCore ALIAS KF5AkonadiCore) +target_include_directories(KF5AkonadiCore INTERFACE "$") +target_include_directories(KF5AkonadiCore PUBLIC "$") +target_include_directories(KF5AkonadiCore PUBLIC "$") + +kde_target_enable_exceptions(KF5AkonadiCore PUBLIC) + +target_link_libraries(KF5AkonadiCore +PUBLIC + KF5::CoreAddons # for KJob + KF5::ItemModels + Qt::Gui # for QColor + Boost::boost +PRIVATE + Qt::Network + Qt::Widgets + KF5::AkonadiPrivate + KF5::I18n + KF5::IconThemes + KF5::ConfigCore + KF5AkonadiPrivate + akonadi_shared +) + +set_target_properties(KF5AkonadiCore PROPERTIES + VERSION ${AKONADI_VERSION} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiCore +) + +ecm_generate_pri_file(BASE_NAME AkonadiCore + LIB_NAME KF5AkonadiCore + DEPS "KItemModels KCoreAddons" FILENAME_VAR PRI_FILENAME +) + +install(TARGETS + KF5AkonadiCore + EXPORT KF5AkonadiTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadicore_export.h + ${AkonadiCore_base_HEADERS} + ${AkonadiCore_attributes_HEADERS} + ${AkonadiCore_models_HEADERS} + ${AkonadiCore_jobs_HEADERS} + ${AkonadiCore_HEADERS} + qtest_akonadi.h + itempayloadinternals_p.h + ${Akonadi_BINARY_DIR}/config-akonadi.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiCore COMPONENT Devel +) + +install(FILES + ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR} +) + +install( FILES + kcfg2dbus.xsl + DESTINATION ${KDE_INSTALL_DATADIR_KF5}/akonadi +) diff --git a/src/core/abstractdifferencesreporter.h b/src/core/abstractdifferencesreporter.h new file mode 100644 index 0000000..06d669c --- /dev/null +++ b/src/core/abstractdifferencesreporter.h @@ -0,0 +1,132 @@ +/* + SPDX-FileCopyrightText: 2010 KDAB + SPDX-FileContributor: Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +namespace Akonadi +{ +/** + * @short An interface to report differences between two arbitrary objects. + * + * This interface can be used to report differences between two arbitrary objects + * by describing a virtual table with three columns. The first column contains the name + * of the property that is compared, the second column the property value of the one + * object and the third column the property of the other object. + * + * The rows of this table can have different modes: + * @li NormalMode The left and right columns show the same property values. + * @li ConflictMode The left and right columns show conflicting property values. + * @li AdditionalLeftMode The left column contains a property value that is not available in the right column. + * @li AdditionalRightMode The right column contains a property value that is not available in the left column. + * + * Example: + * + * @code + * // add differences of a contact + * const KContacts::Addressee contact1 = ... + * const KContacts::Addressee contact2 = ... + * + * AbstractDifferencesReporter *reporter = ... + * reporter->setPropertyNameTitle( i18n( "Contact fields" ) ); + * reporter->setLeftPropertyValueTitle( i18n( "Changed Contact" ) ); + * reporter->setRightPropertyValueTitle( i18n( "Conflicting Contact" ) ); + * + * // check given name + * if ( contact1.givenName() != contact2.givenName() ) + * reporter->addProperty( AbstractDifferencesReporter::ConflictMode, i18n( "Given Name" ), + * contact1.givenName(), contact2.givenName() ); + * else + * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Given Name" ), + * contact1.givenName(), contact2.givenName() ); + * + * // check family name + * if ( contact1.familyName() != contact2.familyName() ) + * reporter->addProperty( AbstractDifferencesReporter::ConflictMode, i18n( "Family Name" ), + * contact1.givenName(), contact2.givenName() ); + * else + * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Family Name" ), + * contact1.givenName(), contact2.givenName() ); + * + * // check emails + * const QStringList leftEmails = contact1.emails(); + * const QStringList rightEmails = contact2.emails(); + * + * for ( const QString &leftEmail : leftEmails ) { + * if ( rightEmails.contains( leftEmail ) ) + * reporter->addProperty( AbstractDifferencesReporter::NormalMode, i18n( "Email" ), + * leftEmail, leftEmail ); + * else + * reporter->addProperty( AbstractDifferencesReporter::AdditionalLeftMode, i18n( "Email" ), + * leftEmail, QString() ); + * } + * + * for( const QString &rightEmail : rightEmails ) { + * if ( !leftEmails.contains( rightEmail ) ) + * reporter->addProperty( AbstractDifferencesReporter::AdditionalRightMode, i18n( "Email" ), + * QString(), rightEmail ); + * } + * + * @endcode + * + * @author Tobias Koenig + * @since 4.6 + */ +class AbstractDifferencesReporter +{ +public: + /** + * Describes the property modes. + */ + enum Mode { + NormalMode, ///< The left and right column show the same property values. + ConflictMode, ///< The left and right column show conflicting property values. + AdditionalLeftMode, ///< The left column contains a property value that is not available in the right column. + AdditionalRightMode ///< The right column contains a property value that is not available in the left column. + }; + + /** + * Destroys the abstract differences reporter. + */ + virtual ~AbstractDifferencesReporter() = default; + + /** + * Sets the @p title of the property name column. + */ + virtual void setPropertyNameTitle(const QString &title) = 0; + + /** + * Sets the @p title of the column that shows the property values + * of the left object. + */ + virtual void setLeftPropertyValueTitle(const QString &title) = 0; + + /** + * Sets the @p title of the column that shows the property values + * of the right object. + */ + virtual void setRightPropertyValueTitle(const QString &title) = 0; + + /** + * Adds a new property entry to the table. + * + * @param mode Describes the mode of the property. If mode is AdditionalLeftMode or AdditionalRightMode, rightValue resp. leftValue + * should be QString(). + * @param name The user visible name of the property. + * @param leftValue The user visible property value of the left object. + * @param rightValue The user visible property value of the right object. + */ + virtual void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue) = 0; + +protected: + explicit AbstractDifferencesReporter() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractDifferencesReporter) +}; + +} + diff --git a/src/core/agentconfigurationbase.cpp b/src/core/agentconfigurationbase.cpp new file mode 100644 index 0000000..5411312 --- /dev/null +++ b/src/core/agentconfigurationbase.cpp @@ -0,0 +1,99 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentconfigurationbase.h" +#include "agentmanager.h" +#include "akonadicore_debug.h" + +#include +#include + +namespace Akonadi +{ +class Q_DECL_HIDDEN AgentConfigurationBase::Private +{ +public: + Private(const KSharedConfigPtr &config, QWidget *parentWidget, const QVariantList &args) + : config(config) + , parentWidget(parentWidget) + { + Q_ASSERT(!args.empty()); + if (args.empty()) { + qCCritical(AKONADICORE_LOG, "AgentConfigurationBase instantiated with invalid arguments"); + return; + } + identifier = args.at(0).toString(); + } + + KSharedConfigPtr config; + QString identifier; + QScopedPointer aboutData; + QWidget *parentWidget = nullptr; +}; +} // namespace Akonadi + +using namespace Akonadi; + +AgentConfigurationBase::AgentConfigurationBase(const KSharedConfigPtr &config, QWidget *parentWidget, const QVariantList &args) + : QObject(reinterpret_cast(parentWidget)) + , d(new Private(config, parentWidget, args)) +{ +} + +AgentConfigurationBase::~AgentConfigurationBase() +{ +} + +KSharedConfigPtr AgentConfigurationBase::config() const +{ + return d->config; +} + +QString AgentConfigurationBase::identifier() const +{ + return d->identifier; +} + +void AgentConfigurationBase::load() +{ + d->config->reparseConfiguration(); +} + +bool AgentConfigurationBase::save() const +{ + d->config->sync(); + d->config->reparseConfiguration(); + return true; +} + +QWidget *AgentConfigurationBase::parentWidget() const +{ + return d->parentWidget; +} + +void AgentConfigurationBase::setKAboutData(const KAboutData &aboutData) +{ + d->aboutData.reset(new KAboutData(aboutData)); +} + +KAboutData *AgentConfigurationBase::aboutData() const +{ + return d->aboutData.data(); +} + +QSize AgentConfigurationBase::restoreDialogSize() const +{ + return {}; +} + +void AgentConfigurationBase::saveDialogSize(const QSize & /*unused*/) // clazy:exclude=function-args-by-value +{ +} + +QDialogButtonBox::StandardButtons AgentConfigurationBase::standardButtons() const +{ + return QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel; +} diff --git a/src/core/agentconfigurationbase.h b/src/core/agentconfigurationbase.h new file mode 100644 index 0000000..90be5e0 --- /dev/null +++ b/src/core/agentconfigurationbase.h @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentconfigurationfactorybase.h" +#include "akonadicore_export.h" + +#include +#include +#include + +class KAboutData; + +namespace Akonadi +{ +/** + * @brief Base class for configuration UI for Akonadi agents + * + * Each agent that has a graphical configuration should subclass this class + * and create its configuration UI there. + * + * The subclass must reimplement load() and save() virtual methods which + * are called automatically. The load() method is called on start to initialize + * widgets (thus subclasses don't need to call it themselves) or when user + * clicks a "Reset" button. The save() method is called whenever user decides to + * save changes. + * + * Since each Akonadi agent instance has its own configuration file whose + * location and name is opaque to the implementation, config() method can be + * used to get access to the current configuration object. + * + * The widget will not run in the same process as the Akonadi agent, thus all + * communication with the resource (if needed) should be done over DBus. The + * identifier of the instance currently being configured is accessible from the + * identifier() method. + * + * There is no need to signal back to the resource when configuration is changed. + * When save() is called and the dialog is destroyed, Akonadi will automatically + * call AgentBase::reconfigure() in the respective Akonadi agent instance. + * + * It is guaranteed that only a single instance of the configuration dialog for + * given agent will be opened at the same time. + * + * Subclasses of ConfigurationBase must be registered as Akonadi plugins using + * AKONADI_AGENTCONFIG_FACTORY macro. + * + * The metadata JSON file then must contain the following values: + * @code + * { + * "X-Akonadi-PluginType": "AgentConfig", + * "X-Akonadi-Library": "exampleresourceconfig", + * "X-Akonadi-AgentConfig-Type": "akonadi_example_resource" + * } + * @endcode + * + * The @p X-Akonadi-Library value must match the name of the plugin binary without + * the (optional) "lib" prefix and file extension. The @p X-Akonadi-AgentConfig-Type + * value must match the name of the @p X-Akonadi-Identifier value from the agent's + * desktop file. + * + * The plugin binary should be installed into akonadi/config subdirectory in one + * of the paths search by QCoreApplication::libraryPaths(). + */ + +class AKONADICORE_EXPORT AgentConfigurationBase : public QObject +{ + Q_OBJECT +public: + /** + * Creates a new AgentConfigurationBase objects. + * + * The @p parentWidget should be used as a parent widget for the configuration + * widgets. + * + * Subclasses must provide a constructor with this exact signature. + */ + explicit AgentConfigurationBase(const KSharedConfigPtr &config, QWidget *parentWidget, const QVariantList &args); + + ~AgentConfigurationBase() override; + + /** + * Reimplement to load settings from the configuration object into widgets. + * + * @warning Always call the base class implementation at the beginning of + * your overridden method! + * + * @see config(), save() + */ + virtual void load(); + + /** + * Reimplement to save new settings into the configuration object. + * + * Return true if the configuration has been successfully saved and should + * be applied to the agent, return false otherwise. + * + * @warning Always remember call the base class implementation at the end + * of your overridden method! + * + * @see config(), load() + */ + virtual bool save() const; + + /** + * Returns about data for the currently configured component. + * + * May return a null pointer. + */ + KAboutData *aboutData() const; + + /** + * Reimplement to restore dialog size. + */ + virtual QSize restoreDialogSize() const; + + /** + * Reimplement to save dialog size. + */ + virtual void saveDialogSize(const QSize &size); + + virtual QDialogButtonBox::StandardButtons standardButtons() const; + +protected: + QWidget *parentWidget() const; + + /** + * Returns KConfig object belonging to the current Akonadi agent instance. + */ + KSharedConfigPtr config() const; + + /** + * Returns identifier of the Akonadi agent instance currently being configured. + */ + QString identifier() const; + + /** + * When KAboutData is provided the dialog will also contain KHelpMenu with + * access to user help etc. + */ + void setKAboutData(const KAboutData &aboutData); + +Q_SIGNALS: + void enableOkButton(bool enabled); + +private: + class Private; + friend class Private; + QScopedPointer d; +}; + +} // namespace + diff --git a/src/core/agentconfigurationfactorybase.cpp b/src/core/agentconfigurationfactorybase.cpp new file mode 100644 index 0000000..2419978 --- /dev/null +++ b/src/core/agentconfigurationfactorybase.cpp @@ -0,0 +1,14 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentconfigurationfactorybase.h" + +using namespace Akonadi; + +AgentConfigurationFactoryBase::AgentConfigurationFactoryBase(QObject *parent) + : QObject(parent) +{ +} diff --git a/src/core/agentconfigurationfactorybase.h b/src/core/agentconfigurationfactorybase.h new file mode 100644 index 0000000..bdd6766 --- /dev/null +++ b/src/core/agentconfigurationfactorybase.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ +class AgentConfigurationBase; +class AKONADICORE_EXPORT AgentConfigurationFactoryBase : public QObject +{ + Q_OBJECT +public: + explicit AgentConfigurationFactoryBase(QObject *parent = nullptr); + ~AgentConfigurationFactoryBase() override = default; + + virtual AgentConfigurationBase *create(const KSharedConfigPtr &config, QWidget *parent, const QVariantList &args) const = 0; +}; + +} + +#define AKONADI_AGENTCONFIG_FACTORY(FactoryName, metadata, ClassName) \ + class FactoryName : public Akonadi::AgentConfigurationFactoryBase \ + { \ + Q_OBJECT \ + Q_PLUGIN_METADATA(IID "org.freedesktop.Akonadi.AgentConfig" FILE metadata) \ + public: \ + FactoryName(QObject *parent = nullptr) \ + : Akonadi::AgentConfigurationFactoryBase(parent) \ + { \ + } \ + Akonadi::AgentConfigurationBase *create(const KSharedConfigPtr &config, QWidget *parent, const QVariantList &args) const override \ + { \ + return new ClassName(config, parent, args); \ + } \ + }; + diff --git a/src/core/agentconfigurationmanager.cpp b/src/core/agentconfigurationmanager.cpp new file mode 100644 index 0000000..09693c6 --- /dev/null +++ b/src/core/agentconfigurationmanager.cpp @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentconfigurationmanager_p.h" +#include "akonadicore_debug.h" +#include "servermanager.h" + +#include +#include +#include +#include + +#include + +namespace Akonadi +{ +class Q_DECL_HIDDEN AgentConfigurationManager::Private +{ +public: + QString serviceName(const QString &instance) const + { + QString service = QStringLiteral("org.freedesktop.Akonadi.AgentConfig.%1").arg(instance); + if (ServerManager::self()->hasInstanceIdentifier()) { + service += QLatin1Char('.') + ServerManager::self()->instanceIdentifier(); + } + return service; + } +}; +} // namespace Akonadi + +using namespace Akonadi; + +AgentConfigurationManager *AgentConfigurationManager::sInstance = nullptr; + +AgentConfigurationManager::AgentConfigurationManager(QObject *parent) + : QObject(parent) +{ +} + +AgentConfigurationManager *AgentConfigurationManager::self() +{ + if (sInstance == nullptr) { + sInstance = new AgentConfigurationManager(); + } + return sInstance; +} + +AgentConfigurationManager::~AgentConfigurationManager() +{ +} + +bool AgentConfigurationManager::registerInstanceConfiguration(const QString &instance) +{ + const auto serviceName = d->serviceName(instance); + QDBusConnection conn = QDBusConnection::sessionBus(); + if (conn.interface()->isServiceRegistered(serviceName)) { + qCDebug(AKONADICORE_LOG) << "Service " << serviceName << " is already registered"; + return false; + } + + return conn.registerService(serviceName); +} + +void AgentConfigurationManager::unregisterInstanceConfiguration(const QString &instance) +{ + const auto serviceName = d->serviceName(instance); + QDBusConnection::sessionBus().unregisterService(serviceName); +} + +bool AgentConfigurationManager::isInstanceRegistered(const QString &instance) const +{ + const auto serviceName = d->serviceName(instance); + return QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName); +} + +QString AgentConfigurationManager::findConfigPlugin(const QString &type) const +{ + const auto libPaths = QCoreApplication::libraryPaths(); + for (const auto &libPath : libPaths) { + const QString pluginPath = QStringLiteral("%1/akonadi/config/").arg(libPath); + const auto libs = QDir(pluginPath).entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + for (const auto &lib : libs) { + QPluginLoader loader(lib.absoluteFilePath()); + const auto metaData = loader.metaData().toVariantMap(); + if (metaData.value(QStringLiteral("IID")).toString() != QLatin1String("org.freedesktop.Akonadi.AgentConfig")) { + continue; + } + const auto md = metaData.value(QStringLiteral("MetaData")).toMap(); + if (md.value(QStringLiteral("X-Akonadi-PluginType")).toString() != QLatin1String("AgentConfig")) { + continue; + } + if (!type.startsWith(md.value(QStringLiteral("X-Akonadi-AgentConfig-Type")).toString())) { + continue; + } + return loader.fileName(); + } + } + + return {}; +} diff --git a/src/core/agentconfigurationmanager_p.h b/src/core/agentconfigurationmanager_p.h new file mode 100644 index 0000000..f64e6e4 --- /dev/null +++ b/src/core/agentconfigurationmanager_p.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ +class AKONADICORE_EXPORT AgentConfigurationManager : public QObject +{ + Q_OBJECT +public: + static AgentConfigurationManager *self(); + ~AgentConfigurationManager() override; + + bool registerInstanceConfiguration(const QString &instance); + void unregisterInstanceConfiguration(const QString &instance); + + bool isInstanceRegistered(const QString &instance) const; + + QString findConfigPlugin(const QString &type) const; + +private: + AgentConfigurationManager(QObject *parent = nullptr); + + class Private; + friend class Private; + QScopedPointer const d; + static AgentConfigurationManager *sInstance; +}; + +} + diff --git a/src/core/agentinstance.cpp b/src/core/agentinstance.cpp new file mode 100644 index 0000000..997889a --- /dev/null +++ b/src/core/agentinstance.cpp @@ -0,0 +1,174 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstance.h" +#include "agentinstance_p.h" + +#include "agentmanager.h" +#include "agentmanager_p.h" +#include "servermanager.h" + +#include "akonadicore_debug.h" + +using namespace Akonadi; + +AgentInstance::AgentInstance() + : d(new Private) +{ +} + +AgentInstance::AgentInstance(const AgentInstance &other) + : d(other.d) +{ +} + +AgentInstance::~AgentInstance() +{ +} + +bool AgentInstance::isValid() const +{ + return !d->mIdentifier.isEmpty(); +} + +AgentType AgentInstance::type() const +{ + return d->mType; +} + +QString AgentInstance::identifier() const +{ + return d->mIdentifier; +} + +void AgentInstance::setName(const QString &name) +{ + AgentManager::self()->d->setName(*this, name); +} + +QString AgentInstance::name() const +{ + return d->mName; +} + +AgentInstance::Status AgentInstance::status() const +{ + switch (d->mStatus) { + case 0: + return Idle; + case 1: + return Running; + case 3: + return NotConfigured; + case 2: + default: + return Broken; + } +} + +QString AgentInstance::statusMessage() const +{ + return d->mStatusMessage; +} + +int AgentInstance::progress() const +{ + return d->mProgress; +} + +bool AgentInstance::isOnline() const +{ + return d->mIsOnline; +} + +void AgentInstance::setIsOnline(bool online) +{ + AgentManager::self()->d->setOnline(*this, online); +} + +void AgentInstance::configure(QWidget *parent) +{ + AgentManager::self()->d->configure(*this, parent); +} + +void AgentInstance::synchronize() +{ + AgentManager::self()->d->synchronize(*this); +} + +void AgentInstance::synchronizeCollectionTree() +{ + AgentManager::self()->d->synchronizeCollectionTree(*this); +} + +void AgentInstance::synchronizeTags() +{ + AgentManager::self()->d->synchronizeTags(*this); +} + +void AgentInstance::synchronizeRelations() +{ + AgentManager::self()->d->synchronizeRelations(*this); +} + +AgentInstance &AgentInstance::operator=(const AgentInstance &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool AgentInstance::operator==(const AgentInstance &other) const +{ + return (d->mIdentifier == other.d->mIdentifier); +} + +void AgentInstance::abortCurrentTask() const +{ + QDBusInterface iface(ServerManager::agentServiceName(ServerManager::Agent, identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Agent.Control")); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("abort")); + if (!reply.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to place D-Bus call."; + } + } else { + qCWarning(AKONADICORE_LOG) << "Unable to obtain agent interface"; + } +} + +void AgentInstance::reconfigure() const +{ + QDBusInterface iface(ServerManager::agentServiceName(ServerManager::Agent, identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Agent.Control")); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("reconfigure")); + if (!reply.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to place D-Bus call."; + } + } else { + qCWarning(AKONADICORE_LOG) << "Unable to obtain agent interface"; + } +} + +void Akonadi::AgentInstance::restart() const +{ + QDBusInterface iface(ServerManager::serviceName(Akonadi::ServerManager::Control), + QStringLiteral("/AgentManager"), + QStringLiteral("org.freedesktop.Akonadi.AgentManager")); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("restartAgentInstance"), identifier()); + if (!reply.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to place D-Bus call."; + } + } else { + qCWarning(AKONADICORE_LOG) << "Unable to obtain control interface" << iface.lastError().message(); + } +} diff --git a/src/core/agentinstance.h b/src/core/agentinstance.h new file mode 100644 index 0000000..0c66585 --- /dev/null +++ b/src/core/agentinstance.h @@ -0,0 +1,203 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +class QString; +class QWidget; + +namespace Akonadi +{ +class AgentType; + +/** + * @short A representation of an agent instance. + * + * The agent instance is a representation of a running agent process. + * It provides information about the instance and a reference to the + * AgentType of that instance. + * + * All available agent instances can be retrieved from the AgentManager. + * + * @code + * + * Akonadi::AgentInstance::List instances = Akonadi::AgentManager::self()->instances(); + * for( const Akonadi::AgentInstance &instance : instances ) { + * qDebug() << "Name:" << instance.name() << "(" << instance.identifier() << ")"; + * } + * + * @endcode + * + * @note To find the collections belonging to an AgentInstance, use + * CollectionFetchJob and supply AgentInstance::identifier() as the parameter + * to CollectionFetchScope::setResource(). + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentInstance +{ + friend class AgentManager; + friend class AgentManagerPrivate; + +public: + /** + * Describes a list of agent instances. + */ + using List = QVector; + + /** + * Describes the status of the agent instance. + */ + enum Status { + Idle = 0, ///< The agent instance does currently nothing. + Running, ///< The agent instance is working on something. + Broken, ///< The agent instance encountered an error state. + NotConfigured ///< The agent is lacking required configuration + }; + + /** + * Creates a new agent instance object. + */ + AgentInstance(); + + /** + * Creates an agent instance from an @p other agent instance. + */ + AgentInstance(const AgentInstance &other); + + /** + * Destroys the agent instance object. + */ + ~AgentInstance(); + + /** + * Returns whether the agent instance object is valid. + */ + Q_REQUIRED_RESULT bool isValid() const; + + /** + * Returns the agent type of this instance. + */ + Q_REQUIRED_RESULT AgentType type() const; + + /** + * Returns the unique identifier of the agent instance. + */ + Q_REQUIRED_RESULT QString identifier() const; + + /** + * Returns the user visible name of the agent instance. + */ + Q_REQUIRED_RESULT QString name() const; + + /** + * Sets the user visible @p name of the agent instance. + */ + void setName(const QString &name); + + /** + * Returns the status of the agent instance. + */ + Q_REQUIRED_RESULT Status status() const; + + /** + * Returns a textual presentation of the status of the agent instance. + */ + Q_REQUIRED_RESULT QString statusMessage() const; + + /** + * Returns the progress of the agent instance in percent, or -1 if no + * progress information are available. + */ + Q_REQUIRED_RESULT int progress() const; + + /** + * Returns whether the agent instance is online currently. + */ + Q_REQUIRED_RESULT bool isOnline() const; + + /** + * Sets @p online status of the agent instance. + */ + void setIsOnline(bool online); + + /** + * Triggers the agent instance to show its configuration dialog. + * + * @deprecated Use the new Akonadi::AgentConfigurationWidget and + * Akonadi::AgentConfigurationDialog to display configuration dialogs + * in-process + * + * @param parent Parent window for the configuration dialog. + */ + AKONADICORE_DEPRECATED void configure(QWidget *parent = nullptr); + + /** + * Triggers the agent instance to start synchronization. + */ + void synchronize(); + + /** + * Triggers a synchronization of the collection tree by the given agent instance. + */ + void synchronizeCollectionTree(); + + /** + * Triggers a synchronization of tags by the given agent instance. + */ + void synchronizeTags(); + + /** + * Triggers a synchronization of relations by the given agent instance. + */ + void synchronizeRelations(); + + /** + * @internal + * @param other other agent instance + */ + AgentInstance &operator=(const AgentInstance &other); + + /** + * @internal + * @param other other agent instance + */ + Q_REQUIRED_RESULT bool operator==(const AgentInstance &other) const; + + /** + * Tell the agent to abort its current operation. + * @since 4.4 + */ + void abortCurrentTask() const; + + /** + * Tell the agent that its configuration has been changed remotely via D-Bus + */ + void reconfigure() const; + + /** + * Restart the agent process. + */ + void restart() const; + +private: + /// @cond PRIVATE + class Private; + QSharedDataPointer d; + /// @endcond +}; + +} + +Q_DECLARE_TYPEINFO(Akonadi::AgentInstance, Q_MOVABLE_TYPE); + +Q_DECLARE_METATYPE(Akonadi::AgentInstance) + diff --git a/src/core/agentinstance_p.h b/src/core/agentinstance_p.h new file mode 100644 index 0000000..be35507 --- /dev/null +++ b/src/core/agentinstance_p.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agenttype.h" + +#include +#include + +namespace Akonadi +{ +/** + * @internal + */ +class Q_DECL_HIDDEN AgentInstance::Private : public QSharedData +{ +public: + Private() + : mStatus(0) + , mProgress(0) + , mIsOnline(false) + { + } + + Private(const Private &other) + : QSharedData(other) + { + mType = other.mType; + mIdentifier = other.mIdentifier; + mName = other.mName; + mStatus = other.mStatus; + mStatusMessage = other.mStatusMessage; + mProgress = other.mProgress; + mIsOnline = other.mIsOnline; + } + + AgentType mType; + QString mIdentifier; + QString mName; + int mStatus; + QString mStatusMessage; + int mProgress; + bool mIsOnline; +}; + +} + diff --git a/src/core/agentmanager.cpp b/src/core/agentmanager.cpp new file mode 100644 index 0000000..6aef8f7 --- /dev/null +++ b/src/core/agentmanager.cpp @@ -0,0 +1,407 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentmanager.h" +#include "agentmanager_p.h" + +#include "agentinstance_p.h" +#include "agenttype_p.h" +#include "collection.h" +#include "servermanager.h" +#include + +#include "shared/akranges.h" + +#include +#include + +using namespace Akonadi; +using namespace AkRanges; + +// @cond PRIVATE + +AgentInstance AgentManagerPrivate::createInstance(const AgentType &type) +{ + const QString &identifier = mManager->createAgentInstance(type.identifier()); + if (identifier.isEmpty()) { + return AgentInstance(); + } + + return fillAgentInstanceLight(identifier); +} + +void AgentManagerPrivate::agentTypeAdded(const QString &identifier) +{ + // Ignore agent types we already know about, for example because we called + // readAgentTypes before. + if (mTypes.contains(identifier)) { + return; + } + + if (mTypes.isEmpty()) { + // The Akonadi ServerManager assumes that the server is up and running as soon + // as it knows about at least one agent type. + // If we Q_EMIT the typeAdded() signal here, it therefore thinks the server is + // running. However, the AgentManager does not know about all agent types yet, + // as the server might still have pending agentTypeAdded() signals, even though + // it internally knows all agent types already. + // This can cause situations where the client gets told by the ServerManager that + // the server is running, yet the client will encounter an error because the + // AgentManager doesn't know all types yet. + // + // Therefore, we read all agent types from the server here so they are known. + readAgentTypes(); + } + + const AgentType type = fillAgentType(identifier); + if (type.isValid()) { + mTypes.insert(identifier, type); + + Q_EMIT mParent->typeAdded(type); + } +} + +void AgentManagerPrivate::agentTypeRemoved(const QString &identifier) +{ + if (!mTypes.contains(identifier)) { + return; + } + + const AgentType type = mTypes.take(identifier); + Q_EMIT mParent->typeRemoved(type); +} + +void AgentManagerPrivate::agentInstanceAdded(const QString &identifier) +{ + const AgentInstance instance = fillAgentInstance(identifier); + if (instance.isValid()) { + // It is possible that this function is called when the instance is already + // in our list we filled initially in the constructor. + // This happens when the constructor is called during Akonadi startup, when + // the agent processes are not fully loaded and have no D-Bus interface yet. + // The server-side agent manager then emits the instance added signal when + // the D-Bus interface for the agent comes up. + // In this case, we simply notify that the instance status has changed. + const bool newAgentInstance = !mInstances.contains(identifier); + if (newAgentInstance) { + mInstances.insert(identifier, instance); + Q_EMIT mParent->instanceAdded(instance); + } else { + mInstances.remove(identifier); + mInstances.insert(identifier, instance); + Q_EMIT mParent->instanceStatusChanged(instance); + } + } +} + +void AgentManagerPrivate::agentInstanceRemoved(const QString &identifier) +{ + if (!mInstances.contains(identifier)) { + return; + } + + const AgentInstance instance = mInstances.take(identifier); + Q_EMIT mParent->instanceRemoved(instance); +} + +void AgentManagerPrivate::agentInstanceStatusChanged(const QString &identifier, int status, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mStatus = status; + instance.d->mStatusMessage = msg; + + Q_EMIT mParent->instanceStatusChanged(instance); +} + +void AgentManagerPrivate::agentInstanceProgressChanged(const QString &identifier, uint progress, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mProgress = progress; + if (!msg.isEmpty()) { + instance.d->mStatusMessage = msg; + } + + Q_EMIT mParent->instanceProgressChanged(instance); +} + +void AgentManagerPrivate::agentInstanceWarning(const QString &identifier, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + Q_EMIT mParent->instanceWarning(instance, msg); +} + +void AgentManagerPrivate::agentInstanceError(const QString &identifier, const QString &msg) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + Q_EMIT mParent->instanceError(instance, msg); +} + +void AgentManagerPrivate::agentInstanceOnlineChanged(const QString &identifier, bool state) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mIsOnline = state; + Q_EMIT mParent->instanceOnline(instance, state); +} + +void AgentManagerPrivate::agentInstanceNameChanged(const QString &identifier, const QString &name) +{ + if (!mInstances.contains(identifier)) { + return; + } + + AgentInstance &instance = mInstances[identifier]; + instance.d->mName = name; + + Q_EMIT mParent->instanceNameChanged(instance); +} + +void AgentManagerPrivate::readAgentTypes() +{ + const QDBusReply types = mManager->agentTypes(); + if (types.isValid()) { + const QStringList lst = types.value(); + for (const QString &type : lst) { + const AgentType agentType = fillAgentType(type); + if (agentType.isValid()) { + mTypes.insert(type, agentType); + Q_EMIT mParent->typeAdded(agentType); + } + } + } +} + +void AgentManagerPrivate::readAgentInstances() +{ + const QDBusReply instances = mManager->agentInstances(); + if (instances.isValid()) { + const QStringList lst = instances.value(); + for (const QString &instance : lst) { + const AgentInstance agentInstance = fillAgentInstance(instance); + if (agentInstance.isValid()) { + mInstances.insert(instance, agentInstance); + Q_EMIT mParent->instanceAdded(agentInstance); + } + } + } +} + +AgentType AgentManagerPrivate::fillAgentType(const QString &identifier) const +{ + AgentType type; + type.d->mIdentifier = identifier; + type.d->mName = mManager->agentName(identifier); + type.d->mDescription = mManager->agentComment(identifier); + type.d->mIconName = mManager->agentIcon(identifier); + type.d->mMimeTypes = mManager->agentMimeTypes(identifier); + type.d->mCapabilities = mManager->agentCapabilities(identifier); + type.d->mCustomProperties = mManager->agentCustomProperties(identifier); + + return type; +} + +void AgentManagerPrivate::setName(const AgentInstance &instance, const QString &name) +{ + mManager->setAgentInstanceName(instance.identifier(), name); +} + +void AgentManagerPrivate::setOnline(const AgentInstance &instance, bool state) +{ + mManager->setAgentInstanceOnline(instance.identifier(), state); +} + +void AgentManagerPrivate::configure(const AgentInstance &instance, QWidget *parent) +{ + qlonglong winId = 0; + if (parent) { + winId = static_cast(parent->window()->winId()); + } + + mManager->agentInstanceConfigure(instance.identifier(), winId); +} + +void AgentManagerPrivate::synchronize(const AgentInstance &instance) +{ + mManager->agentInstanceSynchronize(instance.identifier()); +} + +void AgentManagerPrivate::synchronizeCollectionTree(const AgentInstance &instance) +{ + mManager->agentInstanceSynchronizeCollectionTree(instance.identifier()); +} + +void AgentManagerPrivate::synchronizeTags(const AgentInstance &instance) +{ + mManager->agentInstanceSynchronizeTags(instance.identifier()); +} + +void AgentManagerPrivate::synchronizeRelations(const AgentInstance &instance) +{ + mManager->agentInstanceSynchronizeRelations(instance.identifier()); +} + +AgentInstance AgentManagerPrivate::fillAgentInstance(const QString &identifier) const +{ + AgentInstance instance; + + const QString agentTypeIdentifier = mManager->agentInstanceType(identifier); + if (!mTypes.contains(agentTypeIdentifier)) { + return instance; + } + + instance.d->mType = mTypes.value(agentTypeIdentifier); + instance.d->mIdentifier = identifier; + instance.d->mName = mManager->agentInstanceName(identifier); + instance.d->mStatus = mManager->agentInstanceStatus(identifier); + instance.d->mStatusMessage = mManager->agentInstanceStatusMessage(identifier); + instance.d->mProgress = mManager->agentInstanceProgress(identifier); + instance.d->mIsOnline = mManager->agentInstanceOnline(identifier); + + return instance; +} + +AgentInstance AgentManagerPrivate::fillAgentInstanceLight(const QString &identifier) const +{ + AgentInstance instance; + + const QString agentTypeIdentifier = mManager->agentInstanceType(identifier); + Q_ASSERT_X(mTypes.contains(agentTypeIdentifier), "fillAgentInstanceLight", "Requests non-existing agent type"); + + instance.d->mType = mTypes.value(agentTypeIdentifier); + instance.d->mIdentifier = identifier; + + return instance; +} + +void AgentManagerPrivate::createDBusInterface() +{ + mTypes.clear(); + mInstances.clear(); + + using AgentManagerIface = org::freedesktop::Akonadi::AgentManager; + mManager = std::make_unique(ServerManager::serviceName(ServerManager::Control), + QStringLiteral("/AgentManager"), + QDBusConnection::sessionBus(), + mParent); + + connect(mManager.get(), &AgentManagerIface::agentTypeAdded, this, &AgentManagerPrivate::agentTypeAdded); + connect(mManager.get(), &AgentManagerIface::agentTypeRemoved, this, &AgentManagerPrivate::agentTypeRemoved); + connect(mManager.get(), &AgentManagerIface::agentInstanceAdded, this, &AgentManagerPrivate::agentInstanceAdded); + connect(mManager.get(), &AgentManagerIface::agentInstanceRemoved, this, &AgentManagerPrivate::agentInstanceRemoved); + connect(mManager.get(), &AgentManagerIface::agentInstanceStatusChanged, this, &AgentManagerPrivate::agentInstanceStatusChanged); + connect(mManager.get(), &AgentManagerIface::agentInstanceProgressChanged, this, &AgentManagerPrivate::agentInstanceProgressChanged); + connect(mManager.get(), &AgentManagerIface::agentInstanceNameChanged, this, &AgentManagerPrivate::agentInstanceNameChanged); + connect(mManager.get(), &AgentManagerIface::agentInstanceWarning, this, &AgentManagerPrivate::agentInstanceWarning); + connect(mManager.get(), &AgentManagerIface::agentInstanceError, this, &AgentManagerPrivate::agentInstanceError); + connect(mManager.get(), &AgentManagerIface::agentInstanceOnlineChanged, this, &AgentManagerPrivate::agentInstanceOnlineChanged); + + if (mManager->isValid()) { + readAgentTypes(); + readAgentInstances(); + } +} + +AgentManager *AgentManagerPrivate::mSelf = nullptr; + +AgentManager::AgentManager() + : QObject(nullptr) + , d(new AgentManagerPrivate(this)) +{ + // needed for queued connections on our signals + qRegisterMetaType(); + qRegisterMetaType(); + + d->createDBusInterface(); + + d->mServiceWatcher = std::make_unique(ServerManager::serviceName(ServerManager::Control), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration); + connect(d->mServiceWatcher.get(), &QDBusServiceWatcher::serviceRegistered, this, [this]() { + if (d->mTypes.isEmpty()) { // just to be safe + d->readAgentTypes(); + } + if (d->mInstances.isEmpty()) { + d->readAgentInstances(); + } + }); +} + +/// @endcond + +AgentManager::~AgentManager() = default; + +AgentManager *AgentManager::self() +{ + if (!AgentManagerPrivate::mSelf) { + AgentManagerPrivate::mSelf = new AgentManager(); + } + + return AgentManagerPrivate::mSelf; +} + +AgentType::List AgentManager::types() const +{ + // Maybe the Control process is up and ready but we haven't been to the event loop yet so + // QDBusServiceWatcher hasn't notified us yet. + // In that case make sure to do it here, to avoid going into Broken state. + if (d->mTypes.isEmpty()) { + d->readAgentTypes(); + } + return d->mTypes | Views::values | Actions::toQVector; +} + +AgentType AgentManager::type(const QString &identifier) const +{ + return d->mTypes.value(identifier); +} + +AgentInstance::List AgentManager::instances() const +{ + return d->mInstances | Views::values | Actions::toQVector; +} + +AgentInstance AgentManager::instance(const QString &identifier) const +{ + return d->mInstances.value(identifier); +} + +void AgentManager::removeInstance(const AgentInstance &instance) +{ + d->mManager->removeAgentInstance(instance.identifier()); +} + +void AgentManager::synchronizeCollection(const Collection &collection) +{ + synchronizeCollection(collection, false); +} + +void AgentManager::synchronizeCollection(const Collection &collection, bool recursive) +{ + const QString resId = collection.resource(); + Q_ASSERT(!resId.isEmpty()); + d->mManager->agentInstanceSynchronizeCollection(resId, collection.id(), recursive); +} + +#include "moc_agentmanager.cpp" diff --git a/src/core/agentmanager.h b/src/core/agentmanager.h new file mode 100644 index 0000000..97f0841 --- /dev/null +++ b/src/core/agentmanager.h @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include "agentinstance.h" +#include "agenttype.h" + +#include + +#include + +namespace Akonadi +{ +class AgentManagerPrivate; +class Collection; + +/** + * @short Provides an interface to retrieve agent types and manage agent instances. + * + * This singleton class can be used to create or remove agent instances or trigger + * synchronization of collections. Furthermore it provides information about status + * changes of the agents. + * + * @code + * + * Akonadi::AgentManager *manager = Akonadi::AgentManager::self(); + * + * Akonadi::AgentType::List types = manager->types(); + * for ( const Akonadi::AgentType& type : types ) { + * qDebug() << "Type:" << type.name() << type.description(); + * } + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentManager : public QObject +{ + friend class AgentInstance; + friend class AgentInstanceCreateJobPrivate; + friend class AgentManagerPrivate; + + Q_OBJECT + +public: + /** + * Returns the global instance of the agent manager. + */ + static AgentManager *self(); + + /** + * Destroys the agent manager. + */ + ~AgentManager(); + + /** + * Returns the list of all available agent types. + */ + Q_REQUIRED_RESULT AgentType::List types() const; + + /** + * Returns the agent type with the given @p identifier or + * an invalid agent type if the identifier does not exist. + */ + Q_REQUIRED_RESULT AgentType type(const QString &identifier) const; + + /** + * Returns the list of all available agent instances. + */ + Q_REQUIRED_RESULT AgentInstance::List instances() const; + + /** + * Returns the agent instance with the given @p identifier or + * an invalid agent instance if the identifier does not exist. + * + * Note that because a resource is a special case of an agent, the + * identifier of a resource is the same as that of its agent instance. + * @param identifier identifier to choose the agent instance + */ + Q_REQUIRED_RESULT AgentInstance instance(const QString &identifier) const; + + /** + * Removes the given agent @p instance. + */ + void removeInstance(const AgentInstance &instance); + + /** + * Trigger a synchronization of the given collection by its owning resource agent. + * + * @param collection The collection to synchronize. + */ + void synchronizeCollection(const Collection &collection); + + /** + * Trigger a synchronization of the given collection by its owning resource agent. + * + * @param collection The collection to synchronize. + * @param recursive If true, the sub-collections are also synchronized + * + * @since 4.6 + */ + void synchronizeCollection(const Collection &collection, bool recursive); + +Q_SIGNALS: + /** + * This signal is emitted whenever a new agent type was installed on the system. + * + * @param type The new agent type. + */ + void typeAdded(const Akonadi::AgentType &type); + + /** + * This signal is emitted whenever an agent type was removed from the system. + * + * @param type The removed agent type. + */ + void typeRemoved(const Akonadi::AgentType &type); + + /** + * This signal is emitted whenever a new agent instance was created. + * + * @param instance The new agent instance. + */ + void instanceAdded(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever an agent instance was removed. + * + * @param instance The removed agent instance. + */ + void instanceRemoved(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the status of an agent instance has + * changed. + * + * @param instance The agent instance that status has changed. + */ + void instanceStatusChanged(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the progress of an agent instance has + * changed. + * + * @param instance The agent instance that progress has changed. + */ + void instanceProgressChanged(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the name of the agent instance has changed. + * + * @param instance The agent instance that name has changed. + */ + void instanceNameChanged(const Akonadi::AgentInstance &instance); + + /** + * This signal is emitted whenever the agent instance raised an error. + * + * @param instance The agent instance that raised the error. + * @param message The i18n'ed error message. + */ + void instanceError(const Akonadi::AgentInstance &instance, const QString &message); + + /** + * This signal is emitted whenever the agent instance raised a warning. + * + * @param instance The agent instance that raised the warning. + * @param message The i18n'ed warning message. + */ + void instanceWarning(const Akonadi::AgentInstance &instance, const QString &message); + + /** + * This signal is emitted whenever the online state of an agent changed. + * + * @param instance The agent instance that changed its online state. + * @param online The new online state. + * @since 4.2 + */ + void instanceOnline(const Akonadi::AgentInstance &instance, bool online); + +private: + /// @cond PRIVATE + explicit AgentManager(); + + std::unique_ptr const d; + /// @endcond +}; + +} + diff --git a/src/core/agentmanager_p.h b/src/core/agentmanager_p.h new file mode 100644 index 0000000..be83821 --- /dev/null +++ b/src/core/agentmanager_p.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentmanagerinterface.h" + +#include "agentinstance.h" +#include "agenttype.h" + +#include + +#include + +class QDBusServiceWatcher; + +namespace Akonadi +{ +class AgentManager; + +/** + * @internal + */ +class AgentManagerPrivate : public QObject +{ + friend class AgentManager; + + Q_OBJECT +public: + explicit AgentManagerPrivate(AgentManager *parent) + : mParent(parent) + { + } + + /* + * Used by AgentInstanceCreateJob + */ + AgentInstance createInstance(const AgentType &type); + + void agentTypeAdded(const QString &identifier); + void agentTypeRemoved(const QString &identifier); + void agentInstanceAdded(const QString &identifier); + void agentInstanceRemoved(const QString &identifier); + void agentInstanceStatusChanged(const QString &identifier, int status, const QString &msg); + void agentInstanceProgressChanged(const QString &identifier, uint progress, const QString &msg); + void agentInstanceNameChanged(const QString &identifier, const QString &name); + void agentInstanceWarning(const QString &identifier, const QString &msg); + void agentInstanceError(const QString &identifier, const QString &msg); + void agentInstanceOnlineChanged(const QString &identifier, bool state); + + /** + * Reads the information about all known agent types from the serverside + * agent manager and updates mTypes, like agentTypeAdded() does. + * + * This will not remove agents from the internal map that are no longer on + * the server. + */ + void readAgentTypes(); + + /** + * Reads the information about all known agent instances from the server. If AgentManager + * is created before the Akonadi.Control interface is registered, the agent + * instances aren't immediately found then. + */ + void readAgentInstances(); + + void setName(const AgentInstance &instance, const QString &name); + void setOnline(const AgentInstance &instance, bool state); + void configure(const AgentInstance &instance, QWidget *parent); + void synchronize(const AgentInstance &instance); + void synchronizeCollectionTree(const AgentInstance &instance); + void synchronizeTags(const AgentInstance &instance); + void synchronizeRelations(const AgentInstance &instance); + + void createDBusInterface(); + + AgentType fillAgentType(const QString &identifier) const; + AgentInstance fillAgentInstance(const QString &identifier) const; + AgentInstance fillAgentInstanceLight(const QString &identifier) const; + + static AgentManager *mSelf; + + AgentManager *mParent = nullptr; + std::unique_ptr mManager; + + QHash mTypes; + QHash mInstances; + + std::unique_ptr mServiceWatcher; +}; + +} + diff --git a/src/core/agenttype.cpp b/src/core/agenttype.cpp new file mode 100644 index 0000000..22ebf59 --- /dev/null +++ b/src/core/agenttype.cpp @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agenttype.h" +#include "agenttype_p.h" + +#include + +using namespace Akonadi; + +AgentType::AgentType() + : d(new Private) +{ +} + +AgentType::AgentType(const AgentType &other) + : d(other.d) +{ +} + +AgentType::~AgentType() +{ +} + +bool AgentType::isValid() const +{ + return !d->mIdentifier.isEmpty(); +} + +QString AgentType::identifier() const +{ + return d->mIdentifier; +} + +QString AgentType::name() const +{ + return d->mName; +} + +QString AgentType::description() const +{ + return d->mDescription; +} + +QString AgentType::iconName() const +{ + return d->mIconName; +} + +QIcon AgentType::icon() const +{ + return QIcon::fromTheme(d->mIconName); +} + +QStringList AgentType::mimeTypes() const +{ + return d->mMimeTypes; +} + +QStringList AgentType::capabilities() const +{ + return d->mCapabilities; +} + +QVariantMap AgentType::customProperties() const +{ + return d->mCustomProperties; +} + +AgentType &AgentType::operator=(const AgentType &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool AgentType::operator==(const AgentType &other) const +{ + return (d->mIdentifier == other.d->mIdentifier); +} diff --git a/src/core/agenttype.h b/src/core/agenttype.h new file mode 100644 index 0000000..0e98425 --- /dev/null +++ b/src/core/agenttype.h @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +class QIcon; +class QString; +#include +class QVariant; +using QVariantMap = QMap; + +namespace Akonadi +{ +/** + * @short A representation of an agent type. + * + * The agent type is a representation of an available agent, that can + * be started as an agent instance. + * It provides all information about the type. + * + * All available agent types can be retrieved from the AgentManager. + * + * @code + * + * Akonadi::AgentType::List types = Akonadi::AgentManager::self()->types(); + * for ( const Akonadi::AgentType &type : types ) { + * qDebug() << "Name:" << type.name() << "(" << type.identifier() << ")"; + * } + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentType +{ + friend class AgentManager; + friend class AgentManagerPrivate; + +public: + /** + * Describes a list of agent types. + */ + using List = QVector; + + /** + * Creates a new agent type. + */ + AgentType(); + + /** + * Creates an agent type from an @p other agent type. + */ + AgentType(const AgentType &other); + + /** + * Destroys the agent type. + */ + ~AgentType(); + + /** + * Returns whether the agent type is valid. + */ + Q_REQUIRED_RESULT bool isValid() const; + + /** + * Returns the unique identifier of the agent type. + */ + Q_REQUIRED_RESULT QString identifier() const; + + /** + * Returns the i18n'ed name of the agent type. + */ + Q_REQUIRED_RESULT QString name() const; + + /** + * Returns the description of the agent type. + */ + Q_REQUIRED_RESULT QString description() const; + + /** + * Returns the name of the icon of the agent type. + */ + Q_REQUIRED_RESULT QString iconName() const; + + /** + * Returns the icon of the agent type. + */ + Q_REQUIRED_RESULT QIcon icon() const; + + /** + * Returns the list of supported mime types of the agent type. + */ + Q_REQUIRED_RESULT QStringList mimeTypes() const; + + /** + * Returns the list of supported capabilities of the agent type. + */ + Q_REQUIRED_RESULT QStringList capabilities() const; + + /** + * Returns a Map of custom properties of the agent type. + * @since 4.12 + */ + Q_REQUIRED_RESULT QVariantMap customProperties() const; + + /** + * @internal + * @param other other agent type + */ + AgentType &operator=(const AgentType &other); + + /** + * @internal + * @param other other agent type + */ + bool operator==(const AgentType &other) const; + +private: + /// @cond PRIVATE + class Private; + QSharedDataPointer d; + /// @endcond +}; + +} + +Q_DECLARE_TYPEINFO(Akonadi::AgentType, Q_MOVABLE_TYPE); + +Q_DECLARE_METATYPE(Akonadi::AgentType) + diff --git a/src/core/agenttype_p.h b/src/core/agenttype_p.h new file mode 100644 index 0000000..249a0f5 --- /dev/null +++ b/src/core/agenttype_p.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace Akonadi +{ +/** + * @internal + */ +class AgentType::Private : public QSharedData +{ +public: + Private() + { + } + + Private(const Private &other) + : QSharedData(other) + { + mIdentifier = other.mIdentifier; + mName = other.mName; + mDescription = other.mDescription; + mIconName = other.mIconName; + mMimeTypes = other.mMimeTypes; + mCapabilities = other.mCapabilities; + mCustomProperties = other.mCustomProperties; + } + + QString mIdentifier; + QString mName; + QString mDescription; + QString mIconName; + QStringList mMimeTypes; + QStringList mCapabilities; + QVariantMap mCustomProperties; +}; + +} + diff --git a/src/core/akonaditests_export.h.in b/src/core/akonaditests_export.h.in new file mode 100644 index 0000000..654c008 --- /dev/null +++ b/src/core/akonaditests_export.h.in @@ -0,0 +1,2 @@ +#include "akonadicore_export.h" +#define AKONADI_TESTS_EXPORT @AKONADI_TESTS_EXPORT@ diff --git a/src/core/asyncselectionhandler.cpp b/src/core/asyncselectionhandler.cpp new file mode 100644 index 0000000..bb1d0c4 --- /dev/null +++ b/src/core/asyncselectionhandler.cpp @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "asyncselectionhandler_p.h" +#include "models/entitytreemodel.h" + +using namespace Akonadi; + +AsyncSelectionHandler::AsyncSelectionHandler(QAbstractItemModel *model, QObject *parent) + : QObject(parent) + , mModel(model) +{ + Q_ASSERT(mModel); + + connect(mModel, &QAbstractItemModel::rowsInserted, this, &AsyncSelectionHandler::rowsInserted); +} + +AsyncSelectionHandler::~AsyncSelectionHandler() +{ +} + +bool AsyncSelectionHandler::scanSubTree(const QModelIndex &index, bool searchForItem) +{ + if (searchForItem) { + const Item::Id id = index.data(EntityTreeModel::ItemIdRole).toLongLong(); + + if (mItem.id() == id) { + Q_EMIT itemAvailable(index); + return true; + } + } else { + const Collection::Id id = index.data(EntityTreeModel::CollectionIdRole).toLongLong(); + + if (mCollection.id() == id) { + Q_EMIT collectionAvailable(index); + return true; + } + } + + for (int row = 0; row < mModel->rowCount(index); ++row) { + const QModelIndex childIndex = mModel->index(row, 0, index); + // This should not normally happen, but if it does we end up in an endless loop + if (!childIndex.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid child detected: " << index.data().toString(); + Q_ASSERT(false); + return false; + } + if (scanSubTree(childIndex, searchForItem)) { + return true; + } + } + + return false; +} + +void AsyncSelectionHandler::waitForCollection(const Collection &collection) +{ + mCollection = collection; + + scanSubTree(QModelIndex(), false); +} + +void AsyncSelectionHandler::waitForItem(const Item &item) +{ + mItem = item; + + scanSubTree(QModelIndex(), true); +} + +void AsyncSelectionHandler::rowsInserted(const QModelIndex &parent, int start, int end) +{ + for (int i = start; i <= end; ++i) { + scanSubTree(mModel->index(i, 0, parent), false); + scanSubTree(mModel->index(i, 0, parent), true); + } +} + +#include "moc_asyncselectionhandler_p.cpp" diff --git a/src/core/asyncselectionhandler_p.h b/src/core/asyncselectionhandler_p.h new file mode 100644 index 0000000..34b9f04 --- /dev/null +++ b/src/core/asyncselectionhandler_p.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" + +class QAbstractItemModel; +class QModelIndex; + +namespace Akonadi +{ +/** + * @internal + * + * A helper class to set a current index on a widget with + * delayed model loading. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AsyncSelectionHandler : public QObject +{ + Q_OBJECT + +public: + /** + */ + explicit AsyncSelectionHandler(QAbstractItemModel *model, QObject *parent = nullptr); + + ~AsyncSelectionHandler(); + + void waitForCollection(const Collection &collection); + void waitForItem(const Item &item); + +Q_SIGNALS: + void collectionAvailable(const QModelIndex &index); + void itemAvailable(const QModelIndex &index); + +private Q_SLOTS: + void rowsInserted(const QModelIndex &parent, int start, int end); + +private: + bool scanSubTree(const QModelIndex &index, bool searchForItem); + + QAbstractItemModel *const mModel; + Collection mCollection; + Item mItem; +}; + +} + diff --git a/src/core/attributefactory.cpp b/src/core/attributefactory.cpp new file mode 100644 index 0000000..5a0b2c0 --- /dev/null +++ b/src/core/attributefactory.cpp @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2007-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "attributefactory.h" + +#include "collectionquotaattribute.h" +#include "collectionrightsattribute_p.h" +#include "entityannotationsattribute.h" +#include "entitydeletedattribute.h" +#include "entitydisplayattribute.h" +#include "entityhiddenattribute.h" +#include "favoritecollectionattribute.h" +#include "indexpolicyattribute.h" +#include "persistentsearchattribute.h" +#include "tagattribute.h" + +#include + +#include + +using namespace Akonadi; + +namespace Akonadi +{ +namespace Internal +{ +/** + * @internal + */ +class DefaultAttribute : public Attribute +{ +public: + explicit DefaultAttribute(const QByteArray &type, const QByteArray &value = QByteArray()) + : mType(type) + , mValue(value) + { + } + + DefaultAttribute(const DefaultAttribute &) = delete; + DefaultAttribute &operator=(const DefaultAttribute &) = delete; + + QByteArray type() const override + { + return mType; + } + + Attribute *clone() const override + { + return new DefaultAttribute(mType, mValue); + } + + QByteArray serialized() const override + { + return mValue; + } + + void deserialize(const QByteArray &data) override + { + mValue = data; + } + +private: + QByteArray mType, mValue; +}; + +/** + * @internal + */ +class StaticAttributeFactory : public AttributeFactory +{ +public: + void init() + { + if (!initialized) { + initialized = true; + + // Register built-in attributes + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + AttributeFactory::registerAttribute(); + } + } + bool initialized = false; +}; + +Q_GLOBAL_STATIC(StaticAttributeFactory, s_attributeInstance) // NOLINT(readability-redundant-member-init) + +} // namespace Internal + +using Akonadi::Internal::s_attributeInstance; + +/** + * @internal + */ +class Q_DECL_HIDDEN AttributeFactory::Private +{ +public: + std::unordered_map> attributes; +}; + +AttributeFactory *AttributeFactory::self() +{ + s_attributeInstance->init(); + return s_attributeInstance; +} + +AttributeFactory::AttributeFactory() + : d(std::make_unique()) +{ +} + +AttributeFactory::~AttributeFactory() = default; + +void AttributeFactory::registerAttribute(std::unique_ptr attr) +{ + Q_ASSERT(attr); + Q_ASSERT(!attr->type().contains(' ') && !attr->type().contains('\'') && !attr->type().contains('"')); + auto it = d->attributes.find(attr->type()); + if (it != d->attributes.end()) { + d->attributes.erase(it); + } + d->attributes.emplace(attr->type(), std::move(attr)); +} + +Attribute *AttributeFactory::createAttribute(const QByteArray &type) +{ + auto attr = self()->d->attributes.find(type); + if (attr == self()->d->attributes.cend()) { + return new Internal::DefaultAttribute(type); + } + + return attr->second->clone(); +} + +} // namespace Akonadi diff --git a/src/core/attributefactory.h b/src/core/attributefactory.h new file mode 100644 index 0000000..321c869 --- /dev/null +++ b/src/core/attributefactory.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2007-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ +/** + * @short Provides the functionality of registering and creating arbitrary + * entity attributes. + * + * This class provides the functionality of registering and creating arbitrary Attributes for Entity + * and its subclasses (e.g. Item and Collection). + * + * @code + * + * // register the type first + * Akonadi::AttributeFactory::registerAttribute(); + * + * ... + * + * // use it anywhere else in the application + * SecrecyAttribute *attr = Akonadi::AttributeFactory::createAttribute( "secrecy" ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT AttributeFactory +{ +public: + /// @cond PRIVATE + ~AttributeFactory(); + /// @endcond + + /** + * Registers a custom attribute of type T. + * The same attribute cannot be registered more than once. + */ + template inline static void registerAttribute() + { + static_assert(std::is_default_constructible::value, "An Attribute must be default-constructible."); + AttributeFactory::self()->registerAttribute(std::unique_ptr{new T{}}); + } + + /** + * Creates an entity attribute object of the given type. + * If the type has not been registered, creates a DefaultAttribute. + * + * @param type The attribute type. + */ + static Attribute *createAttribute(const QByteArray &type); + +protected: + /// @cond PRIVATE + explicit AttributeFactory(); + +private: + Q_DISABLE_COPY(AttributeFactory) + static AttributeFactory *self(); + void registerAttribute(std::unique_ptr attribute); + + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/attribute.cpp b/src/core/attributes/attribute.cpp new file mode 100644 index 0000000..8f7cb10 --- /dev/null +++ b/src/core/attributes/attribute.cpp @@ -0,0 +1,11 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "attribute.h" + +using namespace Akonadi; + +Attribute::~Attribute() = default; diff --git a/src/core/attributes/attribute.h b/src/core/attributes/attribute.h new file mode 100644 index 0000000..b4233e5 --- /dev/null +++ b/src/core/attributes/attribute.h @@ -0,0 +1,167 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Provides interface for custom attributes for Entity. + * + * This class is an interface for custom attributes, that can be stored + * in an entity. Attributes should be meta data, e.g. ACLs, quotas etc. + * that are not part of the entities' data itself. + * + * Note that attributes are per user, i.e. when an attribute is added to + * an entity, it only applies to the current user. + * + * To provide custom attributes, you have to subclass from this interface + * and reimplement the pure virtual methods. + * + * @code + * + * class SecrecyAttribute : public Akonadi::Attribute + * { + * public: + * enum Secrecy + * { + * Public, + * Private, + * Confidential + * }; + * + * SecrecyAttribute( Secrecy secrecy = Public ) + * : mSecrecy( secrecy ) + * { + * } + * + * void setSecrecy( Secrecy secrecy ) + * { + * mSecrecy = secrecy; + * } + * + * Secrecy secrecy() const + * { + * return mSecrecy; + * } + * + * virtual QByteArray type() const + * { + * return "secrecy"; + * } + * + * virtual Attribute* clone() const + * { + * return new SecrecyAttribute( mSecrecy ); + * } + * + * virtual QByteArray serialized() const + * { + * switch ( mSecrecy ) { + * case Public: return "public"; break; + * case Private: return "private"; break; + * case Confidential: return "confidential"; break; + * } + * } + * + * virtual void deserialize( const QByteArray &data ) + * { + * if ( data == "public" ) + * mSecrecy = Public; + * else if ( data == "private" ) + * mSecrecy = Private; + * else if ( data == "confidential" ) + * mSecrecy = Confidential; + * } + * } + * + * @endcode + * + * Additionally, you need to register your attribute with Akonadi::AttributeFactory + * for automatic deserialization during retrieving of collections or items: + * + * @code + * AttributeFactory::registerAttribute(); + * @endcode + * + * Third party attributes need to be registered once by each application that uses + * them. So the above snippet needs to be in the resource that adds the attribute, + * and each application that uses the resource. This may be simplified in the future. + * + * The custom attributes can be used in the following way: + * + * @code + * + * Akonadi::Item item( "text/directory" ); + * SecrecyAttribute* attr = item.attribute( Item::AddIfMissing ); + * attr->setSecrecy( SecrecyAttribute::Confidential ); + * + * @endcode + * + * and + * + * @code + * + * Akonadi::Item item = ... + * + * if ( item.hasAttribute() ) { + * SecrecyAttribute *attr = item.attribute(); + * + * SecrecyAttribute::Secrecy secrecy = attr->secrecy(); + * ... + * } + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Attribute // clazy:exclude=copyable-polymorphic +{ +public: + /** + * Describes a list of attributes. + */ + using List = QList; + + /** + * Returns the type of the attribute. + */ + virtual QByteArray type() const = 0; + + /** + * Destroys this attribute. + */ + virtual ~Attribute(); + + /** + * Creates a copy of this attribute. + */ + virtual Attribute *clone() const = 0; + + /** + * Returns a QByteArray representation of the attribute which will be + * storaged. This can be raw binary data, no encoding needs to be applied. + */ + virtual QByteArray serialized() const = 0; + + /** + * Sets the data of this attribute, using the same encoding + * as returned by toByteArray(). + * + * @param data The encoded attribute data. + */ + virtual void deserialize(const QByteArray &data) = 0; + +protected: + explicit Attribute() = default; + Attribute(const Attribute &) = default; +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/collectioncolorattribute.cpp b/src/core/attributes/collectioncolorattribute.cpp new file mode 100644 index 0000000..3b27836 --- /dev/null +++ b/src/core/attributes/collectioncolorattribute.cpp @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2015 Sandro Knauß + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "collectioncolorattribute.h" + +#include +#include + +using namespace Akonadi; + +CollectionColorAttribute::CollectionColorAttribute(const QColor &color) + : mColor(color) +{ +} + +void CollectionColorAttribute::setColor(const QColor &color) +{ + mColor = color; +} + +QColor CollectionColorAttribute::color() const +{ + return mColor; +} + +QByteArray CollectionColorAttribute::type() const +{ + return QByteArrayLiteral("collectioncolor"); +} + +CollectionColorAttribute *CollectionColorAttribute::clone() const +{ + return new CollectionColorAttribute(mColor); +} + +QByteArray CollectionColorAttribute::serialized() const +{ + return mColor.isValid() ? mColor.name(QColor::HexArgb).toUtf8() : ""; +} + +void CollectionColorAttribute::deserialize(const QByteArray &data) +{ + mColor = QColor(QString::fromUtf8(data)); +} diff --git a/src/core/attributes/collectioncolorattribute.h b/src/core/attributes/collectioncolorattribute.h new file mode 100644 index 0000000..4f95d48 --- /dev/null +++ b/src/core/attributes/collectioncolorattribute.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2015 Sandro Knauß + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "akonadicore_export.h" + +#include + +#include + +namespace Akonadi +{ +/** + * @short Attribute that stores colors of a collection. + * + * Storing color in Akonadi makes it possible to sync them between client and server. + * + * @author Sandro Knauß + * @since 5.3 + */ + +class AKONADICORE_EXPORT CollectionColorAttribute : public Akonadi::Attribute +{ +public: + explicit CollectionColorAttribute() = default; + explicit CollectionColorAttribute(const QColor &color); + + ~CollectionColorAttribute() override = default; + + void setColor(const QColor &color); + QColor color() const; + + QByteArray type() const override; + CollectionColorAttribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + QColor mColor; +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/collectionidentificationattribute.cpp b/src/core/attributes/collectionidentificationattribute.cpp new file mode 100644 index 0000000..8870f7b --- /dev/null +++ b/src/core/attributes/collectionidentificationattribute.cpp @@ -0,0 +1,130 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionidentificationattribute.h" + +#include + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionIdentificationAttribute::Private +{ +public: + QByteArray mFolderNamespace; + QByteArray mIdentifier; + QByteArray mName; + QByteArray mOrganizationUnit; + QByteArray mMail; +}; + +CollectionIdentificationAttribute::CollectionIdentificationAttribute(const QByteArray &identifier, + const QByteArray &folderNamespace, + const QByteArray &name, + const QByteArray &organizationUnit, + const QByteArray &mail) + : d(std::make_unique()) +{ + d->mIdentifier = identifier; + d->mFolderNamespace = folderNamespace; + d->mName = name; + d->mOrganizationUnit = organizationUnit; + d->mMail = mail; +} + +CollectionIdentificationAttribute::~CollectionIdentificationAttribute() = default; + +void CollectionIdentificationAttribute::setIdentifier(const QByteArray &identifier) +{ + d->mIdentifier = identifier; +} + +QByteArray CollectionIdentificationAttribute::identifier() const +{ + return d->mIdentifier; +} + +void CollectionIdentificationAttribute::setMail(const QByteArray &mail) +{ + d->mMail = mail; +} + +QByteArray CollectionIdentificationAttribute::mail() const +{ + return d->mMail; +} + +void CollectionIdentificationAttribute::setOu(const QByteArray &ou) +{ + d->mOrganizationUnit = ou; +} + +QByteArray CollectionIdentificationAttribute::ou() const +{ + return d->mOrganizationUnit; +} + +void CollectionIdentificationAttribute::setName(const QByteArray &name) +{ + d->mName = name; +} + +QByteArray CollectionIdentificationAttribute::name() const +{ + return d->mName; +} + +void CollectionIdentificationAttribute::setCollectionNamespace(const QByteArray &ns) +{ + d->mFolderNamespace = ns; +} + +QByteArray CollectionIdentificationAttribute::collectionNamespace() const +{ + return d->mFolderNamespace; +} + +QByteArray CollectionIdentificationAttribute::type() const +{ + return QByteArrayLiteral("collectionidentification"); +} + +Akonadi::Attribute *CollectionIdentificationAttribute::clone() const +{ + return new CollectionIdentificationAttribute(d->mIdentifier, d->mFolderNamespace, d->mName, d->mOrganizationUnit, d->mMail); +} + +QByteArray CollectionIdentificationAttribute::serialized() const +{ + QList l; + l << Akonadi::ImapParser::quote(d->mIdentifier); + l << Akonadi::ImapParser::quote(d->mFolderNamespace); + l << Akonadi::ImapParser::quote(d->mName); + l << Akonadi::ImapParser::quote(d->mOrganizationUnit); + l << Akonadi::ImapParser::quote(d->mMail); + return '(' + Akonadi::ImapParser::join(l, " ") + ')'; +} + +void CollectionIdentificationAttribute::deserialize(const QByteArray &data) +{ + QList l; + Akonadi::ImapParser::parseParenthesizedList(data, l); + const int size = l.size(); + Q_ASSERT(size >= 2); + if (size < 2) { + return; + } + d->mIdentifier = l[0]; + d->mFolderNamespace = l[1]; + + if (size == 5) { + d->mName = l[2]; + d->mOrganizationUnit = l[3]; + d->mMail = l[4]; + } +} diff --git a/src/core/attributes/collectionidentificationattribute.h b/src/core/attributes/collectionidentificationattribute.h new file mode 100644 index 0000000..f1a9b94 --- /dev/null +++ b/src/core/attributes/collectionidentificationattribute.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "attribute.h" +#include + +#include + +namespace Akonadi +{ +/** + * @short Attribute that stores additional information on a collection that can be used for searching. + * + * Additional indexed properties that can be used for searching. + * + * @author Christian Mollekopf + * @since 4.15 + */ +class AKONADICORE_EXPORT CollectionIdentificationAttribute : public Akonadi::Attribute +{ +public: + explicit CollectionIdentificationAttribute(const QByteArray &identifier = QByteArray(), + const QByteArray &folderNamespace = QByteArray(), + const QByteArray &name = QByteArray(), + const QByteArray &organizationUnit = QByteArray(), + const QByteArray &mail = QByteArray()); + ~CollectionIdentificationAttribute() override; + + /** + * Sets an identifier for the collection. + */ + void setIdentifier(const QByteArray &identifier); + QByteArray identifier() const; + + void setMail(const QByteArray &); + QByteArray mail() const; + + void setOu(const QByteArray &); + QByteArray ou() const; + + void setName(const QByteArray &); + QByteArray name() const; + + /** + * Sets a namespace the collection is in. + * + * Initially used are: + * * "person" for a collection shared by a person. + * * "shared" for a collection shared by a person. + */ + void setCollectionNamespace(const QByteArray &ns); + QByteArray collectionNamespace() const; + QByteArray type() const override; + Attribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/collectionquotaattribute.cpp b/src/core/attributes/collectionquotaattribute.cpp new file mode 100644 index 0000000..29e7c19 --- /dev/null +++ b/src/core/attributes/collectionquotaattribute.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionquotaattribute.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionQuotaAttribute::Private +{ +public: + qint64 mCurrentValue = -1; + qint64 mMaximumValue = -1; +}; + +CollectionQuotaAttribute::CollectionQuotaAttribute() + : d(std::make_unique()) +{ +} + +CollectionQuotaAttribute::CollectionQuotaAttribute(qint64 currentValue, qint64 maxValue) + : d(std::make_unique()) +{ + d->mCurrentValue = currentValue; + d->mMaximumValue = maxValue; +} + +CollectionQuotaAttribute::~CollectionQuotaAttribute() = default; + +void CollectionQuotaAttribute::setCurrentValue(qint64 value) +{ + d->mCurrentValue = value; +} + +void CollectionQuotaAttribute::setMaximumValue(qint64 value) +{ + d->mMaximumValue = value; +} + +qint64 CollectionQuotaAttribute::currentValue() const +{ + return d->mCurrentValue; +} + +qint64 CollectionQuotaAttribute::maximumValue() const +{ + return d->mMaximumValue; +} + +QByteArray CollectionQuotaAttribute::type() const +{ + return QByteArrayLiteral("collectionquota"); +} + +Akonadi::Attribute *CollectionQuotaAttribute::clone() const +{ + return new CollectionQuotaAttribute(d->mCurrentValue, d->mMaximumValue); +} + +QByteArray CollectionQuotaAttribute::serialized() const +{ + return QByteArray::number(d->mCurrentValue) + ' ' + QByteArray::number(d->mMaximumValue); +} + +void CollectionQuotaAttribute::deserialize(const QByteArray &data) +{ + d->mCurrentValue = -1; + d->mMaximumValue = -1; + + const QList items = data.simplified().split(' '); + + if (items.isEmpty()) { + return; + } + + d->mCurrentValue = items[0].toLongLong(); + + if (items.size() < 2) { + return; + } + + d->mMaximumValue = items[1].toLongLong(); +} diff --git a/src/core/attributes/collectionquotaattribute.h b/src/core/attributes/collectionquotaattribute.h new file mode 100644 index 0000000..107481f --- /dev/null +++ b/src/core/attributes/collectionquotaattribute.h @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ +/** + * @short Attribute that provides quota information for a collection. + * + * This attribute class provides quota information (e.g. current fill value + * and maximum fill value) for an Akonadi collection. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * const Collection collection = collectionFetchJob->collections().at(0); + * if ( collection.hasAttribute() ) { + * const CollectionQuotaAttribute *attribute = collection.attribute(); + * qDebug() << "current value" << attribute->currentValue(); + * } + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADICORE_EXPORT CollectionQuotaAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new collection quota attribute. + */ + explicit CollectionQuotaAttribute(); + + /** + * Creates a new collection quota attribute with initial values. + * + * @param currentValue The current quota value in bytes. + * @param maxValue The maximum quota value in bytes. + */ + CollectionQuotaAttribute(qint64 currentValue, qint64 maxValue); + + /** + * Destroys the collection quota attribute. + */ + ~CollectionQuotaAttribute() override; + + /** + * Sets the current quota @p value for the collection. + * + * @param value The current quota value in bytes. + */ + void setCurrentValue(qint64 value); + + /** + * Sets the maximum quota @p value for the collection. + * + * @param value The maximum quota value in bytes. + */ + void setMaximumValue(qint64 value); + + /** + * Returns the current quota value in bytes. + */ + qint64 currentValue() const; + + /** + * Returns the maximum quota value in bytes. + */ + qint64 maximumValue() const; + + QByteArray type() const override; + Attribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/collectionrightsattribute.cpp b/src/core/attributes/collectionrightsattribute.cpp new file mode 100644 index 0000000..53a636f --- /dev/null +++ b/src/core/attributes/collectionrightsattribute.cpp @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionrightsattribute_p.h" + +using namespace Akonadi; + +static Collection::Rights dataToRights(const QByteArray &data) +{ + Collection::Rights rights = Collection::ReadOnly; + + if (data.isEmpty()) { + return Collection::ReadOnly; + } + + if (data.at(0) == 'a') { + return Collection::AllRights; + } + + for (int i = 0; i < data.count(); ++i) { + switch (data.at(i)) { + case 'w': + rights |= Collection::CanChangeItem; + break; + case 'c': + rights |= Collection::CanCreateItem; + break; + case 'd': + rights |= Collection::CanDeleteItem; + break; + case 'l': + rights |= Collection::CanLinkItem; + break; + case 'u': + rights |= Collection::CanUnlinkItem; + break; + case 'W': + rights |= Collection::CanChangeCollection; + break; + case 'C': + rights |= Collection::CanCreateCollection; + break; + case 'D': + rights |= Collection::CanDeleteCollection; + break; + } + } + + return rights; +} + +static QByteArray rightsToData(Collection::Rights &rights) +{ + if (rights == Collection::AllRights) { + return QByteArray("a"); + } + + QByteArray data; + if (rights & Collection::CanChangeItem) { + data.append('w'); + } + if (rights & Collection::CanCreateItem) { + data.append('c'); + } + if (rights & Collection::CanDeleteItem) { + data.append('d'); + } + if (rights & Collection::CanChangeCollection) { + data.append('W'); + } + if (rights & Collection::CanCreateCollection) { + data.append('C'); + } + if (rights & Collection::CanDeleteCollection) { + data.append('D'); + } + if (rights & Collection::CanLinkItem) { + data.append('l'); + } + if (rights & Collection::CanUnlinkItem) { + data.append('u'); + } + + return data; +} + +/** + * @internal + */ +class CollectionRightsAttribute::Private +{ +public: + QByteArray mData; +}; + +CollectionRightsAttribute::CollectionRightsAttribute() + : d(std::make_unique()) +{ +} + +CollectionRightsAttribute::~CollectionRightsAttribute() = default; + +void CollectionRightsAttribute::setRights(Collection::Rights rights) +{ + d->mData = rightsToData(rights); +} + +Collection::Rights CollectionRightsAttribute::rights() const +{ + return dataToRights(d->mData); +} + +CollectionRightsAttribute *CollectionRightsAttribute::clone() const +{ + auto attr = new CollectionRightsAttribute(); + attr->d->mData = d->mData; + + return attr; +} + +QByteArray CollectionRightsAttribute::type() const +{ + static const QByteArray s_accessRightsIdentifier("AccessRights"); + return s_accessRightsIdentifier; +} + +QByteArray CollectionRightsAttribute::serialized() const +{ + return d->mData; +} + +void CollectionRightsAttribute::deserialize(const QByteArray &data) +{ + d->mData = data; +} diff --git a/src/core/attributes/collectionrightsattribute_p.h b/src/core/attributes/collectionrightsattribute_p.h new file mode 100644 index 0000000..0cbbc62 --- /dev/null +++ b/src/core/attributes/collectionrightsattribute_p.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include "attribute.h" +#include "collection.h" + +#include + +namespace Akonadi +{ +/** + * @internal + * + * @short Attribute that stores the rights of a collection. + * + * Every collection can have rights set which describes whether + * the collection is readable or writable. That information is stored + * in this custom attribute. + * + * @note You shouldn't use this class directly but the convenience methods + * Collection::rights() and Collection::setRights() instead. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT CollectionRightsAttribute : public Attribute +{ +public: + /** + * Creates a new collection rights attribute. + */ + explicit CollectionRightsAttribute(); + + /** + * Destroys the collection rights attribute. + */ + ~CollectionRightsAttribute() override; + + /** + * Sets the @p rights of the collection. + */ + void setRights(Collection::Rights rights); + + /** + * Returns the rights of the collection. + */ + Collection::Rights rights() const; + + QByteArray type() const override; + + CollectionRightsAttribute *clone() const override; + + QByteArray serialized() const override; + + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/entityannotationsattribute.cpp b/src/core/attributes/entityannotationsattribute.cpp new file mode 100644 index 0000000..65af7f3 --- /dev/null +++ b/src/core/attributes/entityannotationsattribute.cpp @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2008 Omat Holding B.V. + * SPDX-FileCopyrightText: 2014 Christian Mollekopf + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "entityannotationsattribute.h" + +#include +#include + +using namespace Akonadi; + +EntityAnnotationsAttribute::EntityAnnotationsAttribute(const QMap &annotations) + : mAnnotations(annotations) +{ +} + +void EntityAnnotationsAttribute::setAnnotations(const QMap &annotations) +{ + mAnnotations = annotations; +} + +QMap EntityAnnotationsAttribute::annotations() const +{ + return mAnnotations; +} + +void EntityAnnotationsAttribute::insert(const QByteArray &key, const QString &value) +{ + mAnnotations.insert(key, value.toUtf8()); +} + +QString EntityAnnotationsAttribute::value(const QByteArray &key) const +{ + return QString::fromUtf8(mAnnotations.value(key).data()); +} + +bool EntityAnnotationsAttribute::contains(const QByteArray &key) const +{ + return mAnnotations.contains(key); +} + +QByteArray EntityAnnotationsAttribute::type() const +{ + static const QByteArray sType("entityannotations"); + return sType; +} + +Akonadi::Attribute *EntityAnnotationsAttribute::clone() const +{ + return new EntityAnnotationsAttribute(mAnnotations); +} + +QByteArray EntityAnnotationsAttribute::serialized() const +{ + QByteArray result = ""; + + for (auto it = mAnnotations.cbegin(), e = mAnnotations.cend(); it != e; ++it) { + result += it.key(); + result += ' '; + result += it.value(); + result += " % "; // We use this separator as '%' is not allowed in keys or values + } + result.chop(3); + + return result; +} + +void EntityAnnotationsAttribute::deserialize(const QByteArray &data) +{ + mAnnotations.clear(); + const QList lines = data.split('%'); + + for (int i = 0; i < lines.size(); ++i) { + QByteArray line = lines[i]; + if (i != 0 && line.startsWith(' ')) { + line = line.mid(1); + } + if (i != lines.size() - 1 && line.endsWith(' ')) { + line.chop(1); + } + if (line.trimmed().isEmpty()) { + continue; + } + const int wsIndex = line.indexOf(' '); + if (wsIndex > 0) { + const QByteArray key = line.mid(0, wsIndex); + const QByteArray value = line.mid(wsIndex + 1); + mAnnotations[key] = value; + } else { + mAnnotations.insert(line, QByteArray()); + } + } +} diff --git a/src/core/attributes/entityannotationsattribute.h b/src/core/attributes/entityannotationsattribute.h new file mode 100644 index 0000000..d1c91b5 --- /dev/null +++ b/src/core/attributes/entityannotationsattribute.h @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2008 Omat Holding B.V. + * SPDX-FileCopyrightText: 2014 Christian Mollekopf + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ +/** + * An attribute for annotations. + * + * The attribute is inspired by RFC5257(IMAP ANNOTATION) and RFC5464(IMAP METADATA), but serves + * the purpose of RFC5257. + * + * For a private note annotation the entry name is: + * /private/comment + * for a shared note: + * /shared/comment + * + * @since 4.13 + */ +class AKONADICORE_EXPORT EntityAnnotationsAttribute : public Akonadi::Attribute +{ +public: + explicit EntityAnnotationsAttribute() = default; + explicit EntityAnnotationsAttribute(const QMap &annotations); + + void setAnnotations(const QMap &annotations); + QMap annotations() const; + + void insert(const QByteArray &key, const QString &value); + QString value(const QByteArray &key) const; + bool contains(const QByteArray &key) const; + + QByteArray type() const override; + Attribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + QMap mAnnotations; +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/entitydeletedattribute.cpp b/src/core/attributes/entitydeletedattribute.cpp new file mode 100644 index 0000000..4fd4f5f --- /dev/null +++ b/src/core/attributes/entitydeletedattribute.cpp @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "entitydeletedattribute.h" + +#include "private/imapparser_p.h" + +#include "akonadicore_debug.h" + +#include +#include + +using namespace Akonadi; + +class EntityDeletedAttribute::EntityDeletedAttributePrivate +{ +public: + Collection restoreCollection; + QString restoreResource; +}; + +EntityDeletedAttribute::EntityDeletedAttribute() + : d(std::make_unique()) +{ +} + +EntityDeletedAttribute::~EntityDeletedAttribute() = default; + +void EntityDeletedAttribute::setRestoreCollection(const Akonadi::Collection &collection) +{ + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "invalid collection" << collection; + } + Q_ASSERT(collection.isValid()); + d->restoreCollection = collection; + if (collection.resource().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "no resource set"; + } + d->restoreResource = collection.resource(); +} + +Collection EntityDeletedAttribute::restoreCollection() const +{ + return d->restoreCollection; +} + +QString EntityDeletedAttribute::restoreResource() const +{ + return d->restoreResource; +} + +QByteArray Akonadi::EntityDeletedAttribute::type() const +{ + return QByteArrayLiteral("DELETED"); +} + +EntityDeletedAttribute *EntityDeletedAttribute::clone() const +{ + auto attr = new EntityDeletedAttribute(); + attr->d->restoreCollection = d->restoreCollection; + attr->d->restoreResource = d->restoreResource; + return attr; +} + +QByteArray EntityDeletedAttribute::serialized() const +{ + QList l; + l << ImapParser::quote(d->restoreResource.toUtf8()); + QList components; + components << QByteArray::number(d->restoreCollection.id()); + + l << '(' + ImapParser::join(components, " ") + ')'; + return '(' + ImapParser::join(l, " ") + ')'; +} + +void EntityDeletedAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + if (l.size() != 2) { + qCWarning(AKONADICORE_LOG) << "invalid size"; + return; + } + d->restoreResource = QString::fromUtf8(l[0]); + + if (!l[1].isEmpty()) { + QList componentData; + ImapParser::parseParenthesizedList(l[1], componentData); + if (componentData.size() != 1) { + return; + } + bool ok; + const int components = componentData.at(0).toInt(&ok); + if (!ok) { + return; + } + d->restoreCollection = Collection(components); + } +} diff --git a/src/core/attributes/entitydeletedattribute.h b/src/core/attributes/entitydeletedattribute.h new file mode 100644 index 0000000..44642d4 --- /dev/null +++ b/src/core/attributes/entitydeletedattribute.h @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" +#include "collection.h" + +namespace Akonadi +{ +/** + * @short An Attribute that marks that an entity was marked as deleted + * + * This class represents the attribute of all hidden items. The hidden + * items shouldn't be displayed in UI applications (unless in some kind + * of "debug" mode). + * + * Example: + * + * @code + * + * @endcode + * + * @author Christian Mollekopf + * @see Akonadi::Attribute + * @since 4.8 + */ +class AKONADICORE_EXPORT EntityDeletedAttribute : public Attribute +{ +public: + /** + * Creates a new entity deleted attribute. + */ + explicit EntityDeletedAttribute(); + + /** + * Destroys the entity deleted attribute. + */ + ~EntityDeletedAttribute() override; + /** + * Sets the collection used to restore items which have been moved to trash using a TrashJob + * If the Resource is set on the collection, the resource root will be used as fallback during the restore operation. + */ + void setRestoreCollection(const Collection &col); + + /** + * Returns the original collection of an item that has been moved to trash using a TrashJob + */ + Collection restoreCollection() const; + + /** + * Returns the resource of the restoreCollection + */ + QString restoreResource() const; + + /** + * Reimplemented from Attribute + */ + QByteArray type() const override; + + /** + * Reimplemented from Attribute + */ + EntityDeletedAttribute *clone() const override; + + /** + * Reimplemented from Attribute + */ + QByteArray serialized() const override; + + /** + * Reimplemented from Attribute + */ + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class EntityDeletedAttributePrivate; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/entitydisplayattribute.cpp b/src/core/attributes/entitydisplayattribute.cpp new file mode 100644 index 0000000..0713afb --- /dev/null +++ b/src/core/attributes/entitydisplayattribute.cpp @@ -0,0 +1,142 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitydisplayattribute.h" + +#include "private/imapparser_p.h" + +using namespace Akonadi; + +class Q_DECL_HIDDEN EntityDisplayAttribute::Private +{ +public: + QString name; + QString icon; + QString activeIcon; + QColor backgroundColor; +}; + +EntityDisplayAttribute::EntityDisplayAttribute() + : d(std::make_unique()) +{ +} + +EntityDisplayAttribute::~EntityDisplayAttribute() = default; + +QString EntityDisplayAttribute::displayName() const +{ + return d->name; +} + +void EntityDisplayAttribute::setDisplayName(const QString &name) +{ + d->name = name; +} + +QIcon EntityDisplayAttribute::icon() const +{ + return QIcon::fromTheme(d->icon); +} + +QString EntityDisplayAttribute::iconName() const +{ + return d->icon; +} + +void EntityDisplayAttribute::setIconName(const QString &icon) +{ + d->icon = icon; +} + +QByteArray Akonadi::EntityDisplayAttribute::type() const +{ + static const QByteArray sType("ENTITYDISPLAY"); + return sType; +} + +EntityDisplayAttribute *EntityDisplayAttribute::clone() const +{ + auto attr = new EntityDisplayAttribute(); + attr->d->name = d->name; + attr->d->icon = d->icon; + attr->d->activeIcon = d->activeIcon; + attr->d->backgroundColor = d->backgroundColor; + return attr; +} + +QByteArray EntityDisplayAttribute::serialized() const +{ + QList l; + l.reserve(4); + l << ImapParser::quote(d->name.toUtf8()); + l << ImapParser::quote(d->icon.toUtf8()); + l << ImapParser::quote(d->activeIcon.toUtf8()); + QList components; + if (d->backgroundColor.isValid()) { + components = QList() << QByteArray::number(d->backgroundColor.red()) << QByteArray::number(d->backgroundColor.green()) + << QByteArray::number(d->backgroundColor.blue()) << QByteArray::number(d->backgroundColor.alpha()); + } + l << '(' + ImapParser::join(components, " ") + ')'; + return '(' + ImapParser::join(l, " ") + ')'; +} + +void EntityDisplayAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + int size = l.size(); + Q_ASSERT(size >= 2); + d->name = QString::fromUtf8(l[0]); + d->icon = QString::fromUtf8(l[1]); + if (size >= 3) { + d->activeIcon = QString::fromUtf8(l[2]); + } + if (size >= 4) { + if (!l[3].isEmpty()) { + QList componentData; + ImapParser::parseParenthesizedList(l[3], componentData); + if (componentData.size() != 4) { + return; + } + QVector components; + components.reserve(4); + + bool ok; + for (int i = 0; i <= 3; ++i) { + components << componentData.at(i).toInt(&ok); + if (!ok) { + return; + } + } + d->backgroundColor = QColor(components.at(0), components.at(1), components.at(2), components.at(3)); + } + } +} + +void EntityDisplayAttribute::setActiveIconName(const QString &name) +{ + d->activeIcon = name; +} + +QIcon EntityDisplayAttribute::activeIcon() const +{ + return QIcon::fromTheme(d->activeIcon); +} + +QString EntityDisplayAttribute::activeIconName() const +{ + return d->activeIcon; +} + +QColor EntityDisplayAttribute::backgroundColor() const +{ + return d->backgroundColor; +} + +void EntityDisplayAttribute::setBackgroundColor(const QColor &color) +{ + d->backgroundColor = color; +} diff --git a/src/core/attributes/entitydisplayattribute.h b/src/core/attributes/entitydisplayattribute.h new file mode 100644 index 0000000..2c64370 --- /dev/null +++ b/src/core/attributes/entitydisplayattribute.h @@ -0,0 +1,112 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include +#include + +#include + +namespace Akonadi +{ +/** + * @short Attribute that stores the properties that are used to display an entity. + * + * Display properties of a collection or item, such as translated names and icons. + * + * @author Volker Krause + * @since 4.2 + */ +class AKONADICORE_EXPORT EntityDisplayAttribute : public Attribute +{ +public: + /** + * Creates a new entity display attribute. + */ + explicit EntityDisplayAttribute(); + + /** + * Destroys the entity display attribute. + */ + ~EntityDisplayAttribute() override; + + /** + * Sets the @p name that should be used for display. + */ + void setDisplayName(const QString &name); + + /** + * Returns the name that should be used for display. + * Users of this should fall back to Collection::name() if this is empty. + */ + QString displayName() const; + + /** + * Sets the icon @p name for the default icon. + */ + void setIconName(const QString &name); + + /** + * Returns the icon that should be used for this collection or item. + */ + QIcon icon() const; + + /** + * Returns the icon name of the icon returned by icon(). + */ + QString iconName() const; + + /** + * Sets the icon @p name for the active icon. + * @param name the icon name to use + * @since 4.4 + */ + void setActiveIconName(const QString &name); + + /** + * Returns the icon that should be used for this collection or item when active. + * @since 4.4 + */ + QIcon activeIcon() const; + + /** + * Returns the icon name of an active item. + * @since 4.4 + */ + QString activeIconName() const; + + /** + * Returns the backgroundColor or an invalid color if none is set. + * @since 4.4 + */ + QColor backgroundColor() const; + + /** + * Sets the backgroundColor to @p color. + * @param color the background color to use + * @since 4.4 + */ + void setBackgroundColor(const QColor &color); + + /* reimpl */ + QByteArray type() const override; + EntityDisplayAttribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/entityhiddenattribute.cpp b/src/core/attributes/entityhiddenattribute.cpp new file mode 100644 index 0000000..0825d46 --- /dev/null +++ b/src/core/attributes/entityhiddenattribute.cpp @@ -0,0 +1,42 @@ +/****************************************************************************** + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#include "entityhiddenattribute.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN EntityHiddenAttribute::Private +{ +}; + +EntityHiddenAttribute::EntityHiddenAttribute() = default; + +EntityHiddenAttribute::~EntityHiddenAttribute() = default; + +QByteArray Akonadi::EntityHiddenAttribute::type() const +{ + return QByteArrayLiteral("HIDDEN"); +} + +EntityHiddenAttribute *EntityHiddenAttribute::clone() const +{ + return new EntityHiddenAttribute(); +} + +QByteArray EntityHiddenAttribute::serialized() const +{ + return QByteArray(); +} + +void EntityHiddenAttribute::deserialize(const QByteArray &data) +{ + Q_ASSERT(data.isEmpty()); + Q_UNUSED(data) +} diff --git a/src/core/attributes/entityhiddenattribute.h b/src/core/attributes/entityhiddenattribute.h new file mode 100644 index 0000000..9836daf --- /dev/null +++ b/src/core/attributes/entityhiddenattribute.h @@ -0,0 +1,90 @@ +/****************************************************************************** + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ +/** + * @short An Attribute that marks that an entity should be hidden in the UI. + * + * This class represents the attribute of all hidden items. The hidden + * items shouldn't be displayed in UI applications (unless in some kind + * of "debug" mode). + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * ... + * // hide a collection by setting the hidden attribute + * Collection collection = collectionFetchJob->collections().at(0); + * collection.attribute( Collection::AddIfMissing ); + * new CollectionModifyJob( collection, this ); // save back to storage + * + * // check if the collection is hidden + * if ( collection.hasAttribute() ) + * qDebug() << "collection is hidden"; + * else + * qDebug() << "collection is visible"; + * + * @endcode + * + * @author Szymon Stefanek + * @see Akonadi::Attribute + * @since 4.4 + */ +class AKONADICORE_EXPORT EntityHiddenAttribute : public Attribute +{ +public: + /** + * Creates a new entity hidden attribute. + */ + explicit EntityHiddenAttribute(); + + /** + * Destroys the entity hidden attribute. + */ + ~EntityHiddenAttribute() override; + + /** + * Reimplemented from Attribute + */ + QByteArray type() const override; + + /** + * Reimplemented from Attribute + */ + EntityHiddenAttribute *clone() const override; + + /** + * Reimplemented from Attribute + */ + QByteArray serialized() const override; + + /** + * Reimplemented from Attribute + */ + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/favoritecollectionattribute.cpp b/src/core/attributes/favoritecollectionattribute.cpp new file mode 100644 index 0000000..9352a80 --- /dev/null +++ b/src/core/attributes/favoritecollectionattribute.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "favoritecollectionattribute.h" + +using namespace Akonadi; + +Attribute *FavoriteCollectionAttribute::clone() const +{ + return new FavoriteCollectionAttribute(); +} + +QByteArray FavoriteCollectionAttribute::type() const +{ + return QByteArrayLiteral("favorite"); +} + +void FavoriteCollectionAttribute::deserialize(const QByteArray & /*data*/) +{ + // unused +} + +QByteArray FavoriteCollectionAttribute::serialized() const +{ + return {}; +} diff --git a/src/core/attributes/favoritecollectionattribute.h b/src/core/attributes/favoritecollectionattribute.h new file mode 100644 index 0000000..11c21f9 --- /dev/null +++ b/src/core/attributes/favoritecollectionattribute.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +namespace Akonadi +{ +class AKONADICORE_EXPORT FavoriteCollectionAttribute : public Attribute +{ +public: + explicit FavoriteCollectionAttribute() = default; + + Attribute *clone() const override; + QByteArray type() const override; + + void deserialize(const QByteArray &data) override; + QByteArray serialized() const override; +}; + +} + diff --git a/src/core/attributes/indexpolicyattribute.cpp b/src/core/attributes/indexpolicyattribute.cpp new file mode 100644 index 0000000..6d78c2f --- /dev/null +++ b/src/core/attributes/indexpolicyattribute.cpp @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "indexpolicyattribute.h" + +#include "private/imapparser_p.h" + +using namespace Akonadi; + +class Q_DECL_HIDDEN IndexPolicyAttribute::Private +{ +public: + bool enable = true; +}; + +IndexPolicyAttribute::IndexPolicyAttribute() + : d(std::make_unique()) +{ +} + +IndexPolicyAttribute::~IndexPolicyAttribute() = default; + +bool IndexPolicyAttribute::indexingEnabled() const +{ + return d->enable; +} + +void IndexPolicyAttribute::setIndexingEnabled(bool enable) +{ + d->enable = enable; +} + +QByteArray IndexPolicyAttribute::type() const +{ + static const QByteArray sType("INDEXPOLICY"); + return sType; +} + +Attribute *IndexPolicyAttribute::clone() const +{ + auto attr = new IndexPolicyAttribute; + attr->setIndexingEnabled(indexingEnabled()); + return attr; +} + +QByteArray IndexPolicyAttribute::serialized() const +{ + QList l; + l.reserve(2); + l.append("ENABLE"); + l.append(d->enable ? "true" : "false"); + return "(" + ImapParser::join(l, " ") + ')'; // krazy:exclude=doublequote_chars +} + +void IndexPolicyAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + for (int i = 0; i < l.size() - 1; i += 2) { + const QByteArray &key = l.at(i); + if (key == "ENABLE") { + d->enable = l.at(i + 1) == "true"; + } + } +} diff --git a/src/core/attributes/indexpolicyattribute.h b/src/core/attributes/indexpolicyattribute.h new file mode 100644 index 0000000..f6a16ae --- /dev/null +++ b/src/core/attributes/indexpolicyattribute.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ +/** + * @short An attribute to specify how a collection should be indexed for searching. + * + * This attribute can be attached to any collection and should be honored by indexing + * agents. + * + * @since 4.6 + */ +class AKONADICORE_EXPORT IndexPolicyAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new index policy attribute. + */ + IndexPolicyAttribute(); + + /** + * Destroys the index policy attribute. + */ + ~IndexPolicyAttribute() override; + + /** + * Returns whether this collection is supposed to be indexed at all. + */ + Q_REQUIRED_RESULT bool indexingEnabled() const; + + /** + * Sets whether this collection should be indexed at all. + * @param enable @c true to enable indexing, @c false to exclude this collection from indexing + */ + void setIndexingEnabled(bool enable); + + /// @cond PRIVATE + QByteArray type() const override; + Attribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + /// @endcond + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/persistentsearchattribute.cpp b/src/core/attributes/persistentsearchattribute.cpp new file mode 100644 index 0000000..3382971 --- /dev/null +++ b/src/core/attributes/persistentsearchattribute.cpp @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "persistentsearchattribute.h" +#include "collection.h" + +#include "private/imapparser_p.h" + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN PersistentSearchAttribute::Private +{ +public: + QString queryString; + QVector queryCollections; + bool remote = false; + bool recursive = false; +}; + +PersistentSearchAttribute::PersistentSearchAttribute() + : d(std::make_unique()) +{ +} + +PersistentSearchAttribute::~PersistentSearchAttribute() = default; + +QString PersistentSearchAttribute::queryString() const +{ + return d->queryString; +} + +void PersistentSearchAttribute::setQueryString(const QString &query) +{ + d->queryString = query; +} + +QVector PersistentSearchAttribute::queryCollections() const +{ + return d->queryCollections; +} + +void PersistentSearchAttribute::setQueryCollections(const QVector &collections) +{ + d->queryCollections.clear(); + d->queryCollections.reserve(collections.count()); + for (const Collection &collection : collections) { + d->queryCollections << collection.id(); + } +} + +void PersistentSearchAttribute::setQueryCollections(const QVector &collectionsIds) +{ + d->queryCollections = collectionsIds; +} + +bool PersistentSearchAttribute::isRecursive() const +{ + return d->recursive; +} + +void PersistentSearchAttribute::setRecursive(bool recursive) +{ + d->recursive = recursive; +} + +bool PersistentSearchAttribute::isRemoteSearchEnabled() const +{ + return d->remote; +} + +void PersistentSearchAttribute::setRemoteSearchEnabled(bool enabled) +{ + d->remote = enabled; +} + +QByteArray PersistentSearchAttribute::type() const +{ + static const QByteArray sType("PERSISTENTSEARCH"); + return sType; +} + +Attribute *PersistentSearchAttribute::clone() const +{ + auto attr = new PersistentSearchAttribute; + attr->setQueryString(queryString()); + attr->setQueryCollections(queryCollections()); + attr->setRecursive(isRecursive()); + attr->setRemoteSearchEnabled(isRemoteSearchEnabled()); + return attr; +} + +QByteArray PersistentSearchAttribute::serialized() const +{ + QStringList cols; + cols.reserve(d->queryCollections.count()); + for (qint64 colId : std::as_const(d->queryCollections)) { + cols << QString::number(colId); + } + + QList l; + // ### eventually replace with the AKONADI_PARAM_PERSISTENTSEARCH_XXX constants + l.append("QUERYSTRING"); + l.append(ImapParser::quote(d->queryString.toUtf8())); + l.append("QUERYCOLLECTIONS"); + l.append("(" + cols.join(QLatin1Char(' ')).toLatin1() + ')'); + if (d->remote) { + l.append("REMOTE"); + } + if (d->recursive) { + l.append("RECURSIVE"); + } + return "(" + ImapParser::join(l, " ") + ')'; // krazy:exclude=doublequote_chars +} + +void PersistentSearchAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + const int listSize(l.size()); + for (int i = 0; i < listSize; ++i) { + const QByteArray &key = l.at(i); + if (key == QByteArrayLiteral("QUERYLANGUAGE")) { + // Skip the value + ++i; + } else if (key == QByteArrayLiteral("QUERYSTRING")) { + d->queryString = QString::fromUtf8(l.at(i + 1)); + ++i; + } else if (key == QByteArrayLiteral("QUERYCOLLECTIONS")) { + QList ids; + ImapParser::parseParenthesizedList(l.at(i + 1), ids); + d->queryCollections.clear(); + d->queryCollections.reserve(ids.count()); + for (const QByteArray &id : std::as_const(ids)) { + d->queryCollections << id.toLongLong(); + } + ++i; + } else if (key == QByteArrayLiteral("REMOTE")) { + d->remote = true; + } else if (key == QByteArrayLiteral("RECURSIVE")) { + d->recursive = true; + } + } +} diff --git a/src/core/attributes/persistentsearchattribute.h b/src/core/attributes/persistentsearchattribute.h new file mode 100644 index 0000000..5a9e31f --- /dev/null +++ b/src/core/attributes/persistentsearchattribute.h @@ -0,0 +1,166 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +namespace Akonadi +{ +class Collection; + +/** + * @short An attribute to store query properties of persistent search collections. + * + * This attribute is attached to persistent search collections automatically when + * creating a new persistent search with SearchCreateJob. + * Later on the search query can be changed by modifying this attribute of the + * persistent search collection with an CollectionModifyJob. + * + * Example: + * + * @code + * + * const QString name = "My search folder"; + * const QString query = "..."; + * + * Akonadi::SearchCreateJob *job = new Akonadi::SearchCreateJob( name, query ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * const Collection searchCollection = job->createdCollection(); + * ... + * + * // now let's change the query + * if ( searchCollection.hasAttribute() ) { + * Akonadi::PersistentSearchAttribute *attribute = searchCollection.attribute(); + * attribute->setQueryString( "... another query string ..." ); + * + * Akonadi::CollectionModifyJob *modifyJob = new Akonadi::CollectionModifyJob( searchCollection ); + * connect( modifyJob, SIGNAL(result(KJob*)), SLOT(modifyFinished(KJob*)) ); + * } + * ... + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.5 + */ +class AKONADICORE_EXPORT PersistentSearchAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new persistent search attribute. + */ + explicit PersistentSearchAttribute(); + + /** + * Destroys the persistent search attribute. + */ + ~PersistentSearchAttribute() override; + + /** + * Returns the query string used for this search. + */ + QString queryString() const; + + /** + * Sets the query string to be used for this search. + * @param query The query string. + */ + void setQueryString(const QString &query); + + /** + * Returns IDs of collections that will be queried + * @since 4.13 + */ + QVector queryCollections() const; + + /** + * Sets collections to be queried. + * @param collections List of collections to be queries + * @since 4.13 + */ + void setQueryCollections(const QVector &collections); + + /** + * Sets IDs of collections to be queries + * @param collectionsIds IDs of collections to query + * @since 4.13 + */ + void setQueryCollections(const QVector &collectionsIds); + + /** + * Sets whether resources should be queried too. + * + * When set to true, Akonadi will search local indexed items and will also + * query resources that support server-side search, to forward the query + * to remote storage (for example using SEARCH feature on IMAP servers) and + * merge their results with results from local index. + * + * This is useful especially when searching resources, that don't fetch full + * payload by default, for example the IMAP resource, which only fetches headers + * by default and the body is fetched on demand, which means that emails that + * were not yet fully fetched cannot be indexed in local index, and thus cannot + * be searched. With remote search, even those emails can be included in search + * results. + * + * @param enabled Whether remote search is enabled + * @since 4.13 + */ + void setRemoteSearchEnabled(bool enabled); + + /** + * Returns whether remote search is enabled. + * + * @since 4.13 + */ + bool isRemoteSearchEnabled() const; + + /** + * Sets whether the search should recurse into collections + * + * When set to true, all child collections of the specific collections will + * be search recursively. + * + * @param recursive Whether to search recursively + * @since 4.13 + */ + void setRecursive(bool recursive); + + /** + * Returns whether the search is recursive + * + * @since 4.13 + */ + bool isRecursive() const; + + /// @cond PRIVATE + QByteArray type() const override; + Attribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + /// @endcond + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/specialcollectionattribute.cpp b/src/core/attributes/specialcollectionattribute.cpp new file mode 100644 index 0000000..e512beb --- /dev/null +++ b/src/core/attributes/specialcollectionattribute.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "specialcollectionattribute.h" +#include "attributefactory.h" + +using namespace Akonadi; + +/** + @internal +*/ +class Q_DECL_HIDDEN SpecialCollectionAttribute::Private +{ +public: + QByteArray mType; +}; + +SpecialCollectionAttribute::SpecialCollectionAttribute(const QByteArray &type) + : d(std::make_unique()) +{ + d->mType = type; +} + +SpecialCollectionAttribute::~SpecialCollectionAttribute() = default; + +SpecialCollectionAttribute *SpecialCollectionAttribute::clone() const +{ + return new SpecialCollectionAttribute(d->mType); +} + +QByteArray SpecialCollectionAttribute::type() const +{ + static const QByteArray sType("SpecialCollectionAttribute"); + return sType; +} + +QByteArray SpecialCollectionAttribute::serialized() const +{ + return d->mType; +} + +void SpecialCollectionAttribute::deserialize(const QByteArray &data) +{ + d->mType = data; +} + +void SpecialCollectionAttribute::setCollectionType(const QByteArray &type) +{ + d->mType = type; +} + +QByteArray SpecialCollectionAttribute::collectionType() const +{ + return d->mType; +} + +// Register the attribute when the library is loaded. +namespace +{ +bool dummySpecialCollectionAttribute() +{ + using namespace Akonadi; + AttributeFactory::registerAttribute(); + return true; +} + +const bool registeredSpecialCollectionAttribute = dummySpecialCollectionAttribute(); + +} // namespace diff --git a/src/core/attributes/specialcollectionattribute.h b/src/core/attributes/specialcollectionattribute.h new file mode 100644 index 0000000..ccbee34 --- /dev/null +++ b/src/core/attributes/specialcollectionattribute.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +#include + +namespace Akonadi +{ +/** + * @short An Attribute that stores the special collection type of a collection. + * + * All collections registered with SpecialCollections must have this attribute set. + * + * @author Constantin Berzan + * @since 4.4 + */ +class AKONADICORE_EXPORT SpecialCollectionAttribute : public Akonadi::Attribute +{ +public: + /** + * Creates a new special collection attribute. + */ + explicit SpecialCollectionAttribute(const QByteArray &type = QByteArray()); + + /** + * Destroys the special collection attribute. + */ + ~SpecialCollectionAttribute() override; + + /** + * Sets the special collections @p type of the collection. + */ + void setCollectionType(const QByteArray &type); + + /** + * Returns the special collections type of the collection. + */ + QByteArray collectionType() const; + + /* reimpl */ + SpecialCollectionAttribute *clone() const override; + QByteArray type() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributes/tagattribute.cpp b/src/core/attributes/tagattribute.cpp new file mode 100644 index 0000000..a68daba --- /dev/null +++ b/src/core/attributes/tagattribute.cpp @@ -0,0 +1,201 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagattribute.h" + +#include "private/imapparser_p.h" + +using namespace Akonadi; + +class Q_DECL_HIDDEN TagAttribute::Private +{ +public: + QString name; + QString icon; + QColor backgroundColor; + QColor textColor; + QString font; + bool inToolbar = false; + QString shortcut; + int priority = -1; +}; + +TagAttribute::TagAttribute() + : d(std::make_unique()) +{ +} + +TagAttribute::~TagAttribute() = default; + +QString TagAttribute::displayName() const +{ + return d->name; +} + +void TagAttribute::setDisplayName(const QString &name) +{ + d->name = name; +} + +QString TagAttribute::iconName() const +{ + return d->icon; +} + +void TagAttribute::setIconName(const QString &icon) +{ + d->icon = icon; +} + +QByteArray Akonadi::TagAttribute::type() const +{ + static const QByteArray sType("TAG"); + return sType; +} + +TagAttribute *TagAttribute::clone() const +{ + auto attr = new TagAttribute(); + attr->d->name = d->name; + attr->d->icon = d->icon; + attr->d->backgroundColor = d->backgroundColor; + attr->d->textColor = d->textColor; + attr->d->font = d->font; + attr->d->inToolbar = d->inToolbar; + attr->d->shortcut = d->shortcut; + attr->d->priority = d->priority; + return attr; +} + +QByteArray TagAttribute::serialized() const +{ + QList l; + l.reserve(8); + l << ImapParser::quote(d->name.toUtf8()); + l << ImapParser::quote(d->icon.toUtf8()); + l << ImapParser::quote(d->font.toUtf8()); + l << ImapParser::quote(d->shortcut.toUtf8()); + l << ImapParser::quote(QString::number(d->inToolbar).toUtf8()); + { + QList components; + if (d->backgroundColor.isValid()) { + components = QList() << QByteArray::number(d->backgroundColor.red()) << QByteArray::number(d->backgroundColor.green()) + << QByteArray::number(d->backgroundColor.blue()) << QByteArray::number(d->backgroundColor.alpha()); + } + l << '(' + ImapParser::join(components, " ") + ')'; + } + { + QList components; + if (d->textColor.isValid()) { + components = QList() << QByteArray::number(d->textColor.red()) << QByteArray::number(d->textColor.green()) + << QByteArray::number(d->textColor.blue()) << QByteArray::number(d->textColor.alpha()); + } + l << '(' + ImapParser::join(components, " ") + ')'; + } + l << ImapParser::quote(QString::number(d->priority).toUtf8()); + return '(' + ImapParser::join(l, " ") + ')'; +} + +static QColor parseColor(const QByteArray &data) +{ + QList componentData; + ImapParser::parseParenthesizedList(data, componentData); + if (componentData.size() != 4) { + return QColor(); + } + QVector components; + components.reserve(4); + bool ok; + for (int i = 0; i <= 3; ++i) { + components << componentData.at(i).toInt(&ok); + if (!ok) { + return QColor(); + } + } + return QColor(components.at(0), components.at(1), components.at(2), components.at(3)); +} + +void TagAttribute::deserialize(const QByteArray &data) +{ + QList l; + ImapParser::parseParenthesizedList(data, l); + int size = l.size(); + Q_ASSERT(size >= 7); + d->name = QString::fromUtf8(l[0]); + d->icon = QString::fromUtf8(l[1]); + d->font = QString::fromUtf8(l[2]); + d->shortcut = QString::fromUtf8(l[3]); + d->inToolbar = QString::fromUtf8(l[4]).toInt(); + if (!l[5].isEmpty()) { + d->backgroundColor = parseColor(l[5]); + } + if (!l[6].isEmpty()) { + d->textColor = parseColor(l[6]); + } + if (l.size() >= 8) { + d->priority = QString::fromUtf8(l[7]).toInt(); + } +} + +QColor TagAttribute::backgroundColor() const +{ + return d->backgroundColor; +} + +void TagAttribute::setBackgroundColor(const QColor &color) +{ + d->backgroundColor = color; +} + +void TagAttribute::setTextColor(const QColor &color) +{ + d->textColor = color; +} + +QColor TagAttribute::textColor() const +{ + return d->textColor; +} + +void TagAttribute::setFont(const QString &font) +{ + d->font = font; +} + +QString TagAttribute::font() const +{ + return d->font; +} + +void TagAttribute::setInToolbar(bool inToolbar) +{ + d->inToolbar = inToolbar; +} + +bool TagAttribute::inToolbar() const +{ + return d->inToolbar; +} + +void TagAttribute::setShortcut(const QString &shortcut) +{ + d->shortcut = shortcut; +} + +QString TagAttribute::shortcut() const +{ + return d->shortcut; +} + +void TagAttribute::setPriority(int priority) +{ + d->priority = priority; +} + +int TagAttribute::priority() const +{ + return d->priority; +} diff --git a/src/core/attributes/tagattribute.h b/src/core/attributes/tagattribute.h new file mode 100644 index 0000000..796598f --- /dev/null +++ b/src/core/attributes/tagattribute.h @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include + +#include + +namespace Akonadi +{ +/** + * @short Attribute that stores the properties that are used to display a tag. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT TagAttribute : public Attribute +{ +public: + explicit TagAttribute(); + + ~TagAttribute() override; + + /** + * Sets the @p name that should be used for display. + */ + void setDisplayName(const QString &name); + + /** + * Returns the name that should be used for display. + * Users of this should fall back to Collection::name() if this is empty. + */ + QString displayName() const; + + /** + * Sets the icon @p name for the default icon. + */ + void setIconName(const QString &name); + + /** + * Returns the icon name of the icon returned by icon(). + */ + QString iconName() const; + + void setBackgroundColor(const QColor &color); + QColor backgroundColor() const; + void setTextColor(const QColor &color); + QColor textColor() const; + void setFont(const QString &fontKey); + QString font() const; + void setInToolbar(bool inToolbar); + bool inToolbar() const; + void setShortcut(const QString &shortcut); + QString shortcut() const; + + /** + * Sets the priority of the tag. + * The priority is primarily used for presentation, e.g. for sorting. + * If only one tag can be displayed for a given item, the one with the highest + * priority should be shown. + */ + void setPriority(int priority); + + /** + * Returns the priority of the tag. + * The default value is -1 + */ + int priority() const; + + /* reimpl */ + QByteArray type() const override; + TagAttribute *clone() const override; + QByteArray serialized() const override; + void deserialize(const QByteArray &data) override; + +private: + TagAttribute(const TagAttribute &other); + TagAttribute &operator=(const TagAttribute &other); + /// @cond PRIVATE + class Private; + const std::unique_ptr d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/attributestorage.cpp b/src/core/attributestorage.cpp new file mode 100644 index 0000000..bc5a28b --- /dev/null +++ b/src/core/attributestorage.cpp @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "attributestorage_p.h" + +using namespace Akonadi; + +AttributeStorage::AttributeStorage() +{ +} + +AttributeStorage::AttributeStorage(const AttributeStorage &other) + : mModifiedAttributes(other.mModifiedAttributes) + , mDeletedAttributes(other.mDeletedAttributes) +{ + for (Attribute *attr : std::as_const(other.mAttributes)) { + mAttributes.insert(attr->type(), attr->clone()); + } +} + +AttributeStorage &AttributeStorage::operator=(const AttributeStorage &other) +{ + AttributeStorage copy(other); + swap(copy); + return *this; +} + +void AttributeStorage::swap(AttributeStorage &other) noexcept +{ + using std::swap; + swap(other.mAttributes, mAttributes); + swap(other.mModifiedAttributes, mModifiedAttributes); + swap(other.mDeletedAttributes, mDeletedAttributes); +} + +AttributeStorage::~AttributeStorage() +{ + qDeleteAll(mAttributes); +} + +void AttributeStorage::addAttribute(Attribute *attr) +{ + Q_ASSERT(attr); + const QByteArray type = attr->type(); + Attribute *existing = mAttributes.value(type); + if (existing) { + if (attr == existing) { + return; + } + mAttributes.remove(type); + delete existing; + } + mAttributes.insert(type, attr); + markAttributeModified(type); +} + +void AttributeStorage::removeAttribute(const QByteArray &type) +{ + mModifiedAttributes.erase(type); + mDeletedAttributes.insert(type); + delete mAttributes.take(type); +} + +bool AttributeStorage::hasAttribute(const QByteArray &type) const +{ + return mAttributes.contains(type); +} + +Attribute::List AttributeStorage::attributes() const +{ + return mAttributes.values(); +} + +void AttributeStorage::clearAttributes() +{ + for (Attribute *attr : std::as_const(mAttributes)) { + mDeletedAttributes.insert(attr->type()); + delete attr; + } + mAttributes.clear(); + mModifiedAttributes.clear(); +} + +const Attribute *AttributeStorage::attribute(const QByteArray &type) const +{ + return mAttributes.value(type); +} + +Attribute *AttributeStorage::attribute(const QByteArray &type) +{ + Attribute *attr = mAttributes.value(type); + if (attr) { + markAttributeModified(type); + } + return attr; +} + +void AttributeStorage::markAttributeModified(const QByteArray &type) +{ + if (mAttributes.contains(type)) { + mDeletedAttributes.remove(type); + mModifiedAttributes.insert(type); + } +} + +void AttributeStorage::resetChangeLog() +{ + mModifiedAttributes.clear(); + mDeletedAttributes.clear(); +} + +QSet AttributeStorage::deletedAttributes() const +{ + return mDeletedAttributes; +} + +bool AttributeStorage::hasModifiedAttributes() const +{ + return !mModifiedAttributes.empty(); +} + +std::vector AttributeStorage::modifiedAttributes() const +{ + std::vector ret; + ret.reserve(mModifiedAttributes.size()); + for (const auto &type : mModifiedAttributes) { + Attribute *attr = mAttributes.value(type); + Q_ASSERT(attr); + ret.push_back(attr); + } + return ret; +} diff --git a/src/core/attributestorage_p.h b/src/core/attributestorage_p.h new file mode 100644 index 0000000..2c29fff --- /dev/null +++ b/src/core/attributestorage_p.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "attribute.h" +#include +#include +#include +#include + +namespace Akonadi +{ +/** + * The AttributeStorage class is used by Collection, Item, Tag... + * to store a set of attributes, remembering modifications. + * I.e. it knows which attributes have been added or removed + * compared to the initial set (e.g. fetched from server). + */ +class AttributeStorage +{ +public: + AttributeStorage(); + AttributeStorage(const AttributeStorage &other); + AttributeStorage &operator=(const AttributeStorage &other); + void swap(AttributeStorage &other) noexcept; + ~AttributeStorage(); + + void addAttribute(Attribute *attr); + void removeAttribute(const QByteArray &type); + bool hasAttribute(const QByteArray &type) const; + Attribute::List attributes() const; + void clearAttributes(); + const Attribute *attribute(const QByteArray &type) const; + Attribute *attribute(const QByteArray &type); + void markAttributeModified(const QByteArray &type); + void resetChangeLog(); + + QSet deletedAttributes() const; + bool hasModifiedAttributes() const; + std::vector modifiedAttributes() const; + +private: + QHash mAttributes; + std::set mModifiedAttributes; + QSet mDeletedAttributes; +}; + +} + diff --git a/src/core/braveheart.cpp b/src/core/braveheart.cpp new file mode 100644 index 0000000..a5c0bc7 --- /dev/null +++ b/src/core/braveheart.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#ifdef HAVE_MALLOC_TRIM + +#include +#include +#include +#include + +#include + +namespace Akonadi +{ +class Braveheart +{ +private: + static void sonOfScotland() + { + Q_ASSERT(qApp->thread() == QThread::currentThread()); + + if (!qApp->property("__Akonadi__Braveheart").isNull()) { + // One Scottish warrior is enough.... + return; + } + auto freedom = new QTimer(qApp); + QObject::connect(freedom, &QTimer::timeout, freedom, []() { + // They may take our lives, but they will never + // take our memory! + malloc_trim(50 * 1024 * 1024); + }); + // Fight for freedom every 15 minutes + freedom->start(15 * 60 * 1000); + qApp->setProperty("__Akonadi__Braveheart", true); + } + +public: + explicit Braveheart() + { + qAddPreRoutine([]() { + if (qApp->thread() != QThread::currentThread()) { + QTimer::singleShot(0, qApp, sonOfScotland); + } else { + sonOfScotland(); + } + }); + } +}; + +namespace +{ +Braveheart Wallace; // clazy:exclude=non-pod-global-static + +} + +} // namespace Akonadi + +#endif // HAVE_MALLOC_TRIM diff --git a/src/core/cachepolicy.cpp b/src/core/cachepolicy.cpp new file mode 100644 index 0000000..49978dc --- /dev/null +++ b/src/core/cachepolicy.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "cachepolicy.h" +#include "collection.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CachePolicy::Private : public QSharedData +{ +public: + QStringList localParts; + int timeout = -1; + int interval = -1; + bool inherit = true; + bool syncOnDemand = false; +}; + +CachePolicy::CachePolicy() +{ + static QSharedDataPointer sharedPrivate(new Private); + d = sharedPrivate; +} + +CachePolicy::CachePolicy(const CachePolicy &other) + : d(other.d) +{ +} + +CachePolicy::~CachePolicy() +{ +} + +CachePolicy &CachePolicy::operator=(const CachePolicy &other) +{ + d = other.d; + return *this; +} + +bool Akonadi::CachePolicy::operator==(const CachePolicy &other) const +{ + if (!d->inherit && !other.d->inherit) { + return d->localParts == other.d->localParts && d->timeout == other.d->timeout && d->interval == other.d->interval + && d->syncOnDemand == other.d->syncOnDemand; + } + return d->inherit == other.d->inherit; +} + +bool CachePolicy::inheritFromParent() const +{ + return d->inherit; +} + +void CachePolicy::setInheritFromParent(bool inherit) +{ + d->inherit = inherit; +} + +QStringList CachePolicy::localParts() const +{ + return d->localParts; +} + +void CachePolicy::setLocalParts(const QStringList &parts) +{ + d->localParts = parts; +} + +int CachePolicy::cacheTimeout() const +{ + return d->timeout; +} + +void CachePolicy::setCacheTimeout(int timeout) +{ + d->timeout = timeout; +} + +int CachePolicy::intervalCheckTime() const +{ + return d->interval; +} + +void CachePolicy::setIntervalCheckTime(int time) +{ + d->interval = time; +} + +bool CachePolicy::syncOnDemand() const +{ + return d->syncOnDemand; +} + +void CachePolicy::setSyncOnDemand(bool enable) +{ + d->syncOnDemand = enable; +} + +QDebug operator<<(QDebug d, const CachePolicy &c) +{ + return d << "CachePolicy: \n" + << " inherit:" << c.inheritFromParent() << '\n' + << " interval:" << c.intervalCheckTime() << '\n' + << " timeout:" << c.cacheTimeout() << '\n' + << " sync on demand:" << c.syncOnDemand() << '\n' + << " local parts:" << c.localParts(); +} diff --git a/src/core/cachepolicy.h b/src/core/cachepolicy.h new file mode 100644 index 0000000..7ba25e8 --- /dev/null +++ b/src/core/cachepolicy.h @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ +/** + * @short Represents the caching policy for a collection. + * + * There is one cache policy per collection. It can either specify that all + * properties of the policy of the parent collection will be inherited (the + * default) or specify the following values: + * + * - The item parts that should be permanently kept locally and are downloaded + * during a collection sync (e.g. full mail vs. just the headers). + * - A minimum time for which non-permanently cached item parts have to be kept + * (0 - infinity). + * - Whether or not a collection sync is triggered on demand, i.e. as soon + * as it is accessed by a client. + * - An optional time interval for regular collection sync (aka interval + * mail check). + * + * Syncing means fetching updates from the Akonadi database. The cache policy + * does not affect updates of the Akonadi database from the backend, since + * backend updates will normally immediately trigger the resource to update the + * Akonadi database. + * + * The cache policy applies only to reading from the collection. Writing to the + * collection is independent of cache policy - all updates are written to the + * backend as soon as the resource can schedule this. + * + * @code + * + * Akonadi::CachePolicy policy; + * policy.setCacheTimeout( 30 ); + * policy.setIntervalCheckTime( 20 ); + * + * Akonadi::Collection collection = ... + * collection.setCachePolicy( policy ); + * + * @endcode + * + * @todo Do we also need a size limit for the cache as well? + * @todo on a POP3 account, is should not be possible to change locally cached parts, find a solution for that + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CachePolicy +{ +public: + /** + * Creates an empty cache policy. + */ + CachePolicy(); + + /** + * Creates a cache policy from an @p other cache policy. + */ + CachePolicy(const CachePolicy &other); + + /** + * Destroys the cache policy. + */ + ~CachePolicy(); + + /** + * Returns whether it inherits cache policy from the parent collection. + */ + bool inheritFromParent() const; + + /** + * Sets whether the cache policy should be inherited from the parent collection. + */ + void setInheritFromParent(bool inherit); + + /** + * Returns the parts to permanently cache locally. + */ + Q_REQUIRED_RESULT QStringList localParts() const; + + /** + * Specifies the parts to permanently cache locally. + */ + void setLocalParts(const QStringList &parts); + + /** + * Returns the cache timeout for non-permanently cached parts in minutes; + * -1 means indefinitely. + */ + Q_REQUIRED_RESULT int cacheTimeout() const; + + /** + * Sets cache timeout for non-permanently cached parts. + * @param timeout Timeout in minutes, -1 for indefinitely. + */ + void setCacheTimeout(int timeout); + + /** + * Returns the interval check time in minutes, -1 for never. + */ + Q_REQUIRED_RESULT int intervalCheckTime() const; + + /** + * Sets interval check time. + * @param time Check time interval in minutes, -1 for never. + */ + void setIntervalCheckTime(int time); + + /** + * Returns whether the collection will be synced automatically when necessary, + * i.e. as soon as it is accessed by a client. + */ + Q_REQUIRED_RESULT bool syncOnDemand() const; + + /** + * Sets whether the collection shall be synced automatically when necessary, + * i.e. as soon as it is accessed by a client. + * @param enable If @c true the collection is synced. + */ + void setSyncOnDemand(bool enable); + + /** + * @internal. + * @param other other cache policy + */ + CachePolicy &operator=(const CachePolicy &other); + + /** + * @internal + * @param other other cache policy + */ + Q_REQUIRED_RESULT bool operator==(const CachePolicy &other) const; + +private: + /// @cond PRIVATE + class Private; + QSharedDataPointer d; + /// @endcond +}; + +} + +/** + * Allows a cache policy to be output for debugging purposes. + */ +AKONADICORE_EXPORT QDebug operator<<(QDebug, const Akonadi::CachePolicy &); + diff --git a/src/core/changemediator_p.cpp b/src/core/changemediator_p.cpp new file mode 100644 index 0000000..698741a --- /dev/null +++ b/src/core/changemediator_p.cpp @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2011 Tobias Koenig + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "changemediator_p.h" + +#include + +#include "collection.h" +#include "item.h" + +using namespace Akonadi; + +class GlobalChangeMediator : public ChangeMediator +{ + Q_OBJECT +}; + +Q_GLOBAL_STATIC(GlobalChangeMediator, s_globalChangeMediator) // NOLINT(readability-redundant-member-init) + +ChangeMediator *ChangeMediator::instance() +{ + if (s_globalChangeMediator.isDestroyed()) { + return nullptr; + } else { + return s_globalChangeMediator; + } +} + +ChangeMediator::ChangeMediator(QObject *parent) + : QObject(parent) +{ + if (auto app = QCoreApplication::instance(); app != nullptr) { + this->moveToThread(app->thread()); + } +} + +/* static */ +void ChangeMediator::registerMonitor(QObject *monitor) +{ + QMetaObject::invokeMethod(instance(), [monitor]() { + instance()->m_monitors.push_back(monitor); + }); +} + +/* static */ +void ChangeMediator::unregisterMonitor(QObject *monitor) +{ + QMetaObject::invokeMethod(instance(), [monitor]() { + instance()->m_monitors.removeAll(monitor); + }); +} + +/* static */ +void ChangeMediator::invalidateCollection(const Akonadi::Collection &collection) +{ + QMetaObject::invokeMethod(instance(), [colId = collection.id()]() { + for (auto monitor : std::as_const(instance()->m_monitors)) { + const bool ok = QMetaObject::invokeMethod(monitor, "invalidateCollectionCache", Q_ARG(qint64, colId)); + Q_ASSERT(ok); + Q_UNUSED(ok) + } + }); +} + +/* static */ +void ChangeMediator::invalidateItem(const Akonadi::Item &item) +{ + QMetaObject::invokeMethod(instance(), [itemId = item.id()]() { + for (auto monitor : std::as_const(instance()->m_monitors)) { + const bool ok = QMetaObject::invokeMethod(monitor, "invalidateItemCache", Q_ARG(qint64, itemId)); + Q_ASSERT(ok); + Q_UNUSED(ok) + } + }); +} + +/* static */ +void ChangeMediator::invalidateTag(const Tag &tag) +{ + QMetaObject::invokeMethod(instance(), [tagId = tag.id()]() { + for (auto monitor : std::as_const(instance()->m_monitors)) { + const bool ok = QMetaObject::invokeMethod(monitor, "invalidateTagCache", Q_ARG(qint64, tagId)); + Q_ASSERT(ok); + Q_UNUSED(ok) + } + }); +} + +#include "changemediator_p.moc" diff --git a/src/core/changemediator_p.h b/src/core/changemediator_p.h new file mode 100644 index 0000000..a3ac9d0 --- /dev/null +++ b/src/core/changemediator_p.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2011 Tobias Koenig + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace Akonadi +{ +class Job; +class JobPrivate; + +class Collection; +class Item; +class Tag; + +class ChangeMediator : public QObject +{ + Q_OBJECT +public: + static ChangeMediator *instance(); + + static void registerMonitor(QObject *monitor); + static void unregisterMonitor(QObject *monitor); + + static void invalidateCollection(const Akonadi::Collection &collection); + static void invalidateItem(const Akonadi::Item &item); + static void invalidateTag(const Akonadi::Tag &tag); + +protected: + explicit ChangeMediator(QObject *parent = nullptr); + Q_DISABLE_COPY_MOVE(ChangeMediator) + + QList m_monitors; +}; + +} + diff --git a/src/core/changenotification.cpp b/src/core/changenotification.cpp new file mode 100644 index 0000000..27bc226 --- /dev/null +++ b/src/core/changenotification.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "changenotification.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +namespace Akonadi +{ +class AKONADICORE_NO_EXPORT ChangeNotification::Private : public QSharedData +{ +public: + QDateTime timestamp; + QVector listeners; + Protocol::ChangeNotificationPtr notification; + ChangeNotification::Type type; +}; + +} // namespace Akonadi + +ChangeNotification::ChangeNotification() + : d(new Private) +{ +} + +ChangeNotification::ChangeNotification(const ChangeNotification &other) + : d(other.d) +{ +} + +ChangeNotification::~ChangeNotification() +{ +} + +ChangeNotification &ChangeNotification::operator=(const ChangeNotification &other) +{ + d = other.d; + return *this; +} + +bool ChangeNotification::isValid() const +{ + return d->timestamp.isValid(); +} + +void ChangeNotification::setType(ChangeNotification::Type type) +{ + d->type = type; +} + +ChangeNotification::Type ChangeNotification::type() const +{ + return d->type; +} + +void ChangeNotification::setListeners(const QVector &listeners) +{ + d->listeners = listeners; +} + +QVector ChangeNotification::listeners() const +{ + return d->listeners; +} + +void ChangeNotification::setTimestamp(const QDateTime ×tamp) +{ + d->timestamp = timestamp; +} + +QDateTime ChangeNotification::timestamp() const +{ + return d->timestamp; +} + +Protocol::ChangeNotificationPtr ChangeNotification::notification() const +{ + return d->notification; +} + +void ChangeNotification::setNotification(const Protocol::ChangeNotificationPtr &ntf) +{ + d->notification = ntf; +} diff --git a/src/core/changenotification.h b/src/core/changenotification.h new file mode 100644 index 0000000..72367d7 --- /dev/null +++ b/src/core/changenotification.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace Akonadi +{ +namespace Protocol +{ +class ChangeNotification; +using ChangeNotificationPtr = QSharedPointer; +} + +/** + * Emitted by Monitor::debugNotification() signal. + * + * This is purely for debugging purposes and should never be used in regular + * applications. + * + * @since 5.4 + */ +class AKONADICORE_EXPORT ChangeNotification +{ +public: + enum Type { + Items, + Collection, + Tag, + Relation, + Subscription, + }; + + explicit ChangeNotification(); + ChangeNotification(const ChangeNotification &other); + ~ChangeNotification(); + + ChangeNotification &operator=(const ChangeNotification &other); + + Q_REQUIRED_RESULT bool isValid() const; + + Q_REQUIRED_RESULT QDateTime timestamp() const; + void setTimestamp(const QDateTime ×tamp); + + Q_REQUIRED_RESULT QVector listeners() const; + void setListeners(const QVector &listeners); + + Q_REQUIRED_RESULT Type type() const; + void setType(Type type); + + Q_REQUIRED_RESULT Protocol::ChangeNotificationPtr notification() const; + void setNotification(const Protocol::ChangeNotificationPtr &ntf); + +private: + class Private; + QSharedDataPointer d; +}; + +} + diff --git a/src/core/changenotificationdependenciesfactory.cpp b/src/core/changenotificationdependenciesfactory.cpp new file mode 100644 index 0000000..8895e5f --- /dev/null +++ b/src/core/changenotificationdependenciesfactory.cpp @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "changemediator_p.h" +#include "changenotificationdependenciesfactory_p.h" +#include "connection_p.h" +#include "servermanager.h" +#include "session_p.h" +#include "sessionthread_p.h" + +#include + +using namespace Akonadi; + +Connection *ChangeNotificationDependenciesFactory::createNotificationConnection(Session *session, CommandBuffer *commandBuffer) +{ + if (!Akonadi::ServerManager::self()->isRunning()) { + return nullptr; + } + + auto connection = new Connection(Connection::NotificationConnection, session->sessionId(), commandBuffer); + addConnection(session, connection); + return connection; +} + +void ChangeNotificationDependenciesFactory::addConnection(Session *session, Connection *connection) +{ + session->d->sessionThread()->addConnection(connection); +} + +void ChangeNotificationDependenciesFactory::destroyNotificationConnection(Session *session, Connection *connection) +{ + session->d->sessionThread()->destroyConnection(connection); +} + +QObject *ChangeNotificationDependenciesFactory::createChangeMediator(QObject *parent) +{ + Q_UNUSED(parent) + return ChangeMediator::instance(); +} + +CollectionCache *ChangeNotificationDependenciesFactory::createCollectionCache(int maxCapacity, Session *session) +{ + return new CollectionCache(maxCapacity, session); +} + +ItemCache *ChangeNotificationDependenciesFactory::createItemCache(int maxCapacity, Session *session) +{ + return new ItemCache(maxCapacity, session); +} + +ItemListCache *ChangeNotificationDependenciesFactory::createItemListCache(int maxCapacity, Session *session) +{ + return new ItemListCache(maxCapacity, session); +} + +TagListCache *ChangeNotificationDependenciesFactory::createTagListCache(int maxCapacity, Session *session) +{ + return new TagListCache(maxCapacity, session); +} diff --git a/src/core/changenotificationdependenciesfactory_p.h b/src/core/changenotificationdependenciesfactory_p.h new file mode 100644 index 0000000..a2b8262 --- /dev/null +++ b/src/core/changenotificationdependenciesfactory_p.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2011 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entitycache_p.h" +#include "session.h" + +namespace Akonadi +{ +class Connection; +class CommandBuffer; + +/** + * This class exists so that we can create a fake notification source in + * unit tests. + */ +class AKONADI_TESTS_EXPORT ChangeNotificationDependenciesFactory +{ +public: + explicit ChangeNotificationDependenciesFactory() = default; + virtual ~ChangeNotificationDependenciesFactory() = default; + + virtual Connection *createNotificationConnection(Session *parent, CommandBuffer *commandBuffer); + virtual void destroyNotificationConnection(Session *parent, Connection *connection); + + virtual QObject *createChangeMediator(QObject *parent); + + virtual Akonadi::CollectionCache *createCollectionCache(int maxCapacity, Session *session); + virtual Akonadi::ItemCache *createItemCache(int maxCapacity, Session *session); + virtual Akonadi::ItemListCache *createItemListCache(int maxCapacity, Session *session); + virtual Akonadi::TagListCache *createTagListCache(int maxCapacity, Session *session); + +protected: + Q_DISABLE_COPY_MOVE(ChangeNotificationDependenciesFactory) + + void addConnection(Session *session, Connection *connection); +}; + +} + diff --git a/src/core/changerecorder.cpp b/src/core/changerecorder.cpp new file mode 100644 index 0000000..bacab1d --- /dev/null +++ b/src/core/changerecorder.cpp @@ -0,0 +1,115 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "changerecorder.h" +#include "changerecorder_p.h" + +#include + +using namespace Akonadi; + +ChangeRecorder::ChangeRecorder(QObject *parent) + : Monitor(new ChangeRecorderPrivate(nullptr, this), parent) +{ +} + +ChangeRecorder::ChangeRecorder(ChangeRecorderPrivate *privateclass, QObject *parent) + : Monitor(privateclass, parent) +{ +} + +ChangeRecorder::~ChangeRecorder() +{ +} + +void ChangeRecorder::setConfig(QSettings *settings) +{ + Q_D(ChangeRecorder); + if (settings) { + d->settings = settings; + Q_ASSERT(d->pendingNotifications.isEmpty()); + d->loadNotifications(); + } else if (d->settings) { + if (d->enableChangeRecording) { + d->saveNotifications(); + } + d->settings = settings; + } +} + +void ChangeRecorder::replayNext() +{ + Q_D(ChangeRecorder); + + if (!d->enableChangeRecording) { + return; + } + + if (!d->pendingNotifications.isEmpty()) { + const auto msg = d->pendingNotifications.head(); + if (d->ensureDataAvailable(msg)) { + d->emitNotification(msg); + } else if (d->translateAndCompress(d->pipeline, msg)) { + // The msg is now in both pipeline and pendingNotifications. + // When data is available, MonitorPrivate::flushPipeline will emitNotification. + // When changeProcessed is called, we'll finally remove it from pendingNotifications. + } else { + // In the case of a move where both source and destination are + // ignored, we ignore the message and process the next one. + d->dequeueNotification(); + replayNext(); + return; + } + } else { + // This is necessary when none of the notifications were accepted / processed + // above, and so there is no one to call changeProcessed() and the ChangeReplay task + // will be stuck forever in the ResourceScheduler. + Q_EMIT nothingToReplay(); + } +} + +bool ChangeRecorder::isEmpty() const +{ + Q_D(const ChangeRecorder); + return d->pendingNotifications.isEmpty(); +} + +void ChangeRecorder::changeProcessed() +{ + Q_D(ChangeRecorder); + + if (!d->enableChangeRecording) { + return; + } + + // changerecordertest.cpp calls changeProcessed after receiving nothingToReplay, + // so test for emptiness. Not sure real code does this though. + // Q_ASSERT( !d->pendingNotifications.isEmpty() ) + if (!d->pendingNotifications.isEmpty()) { + d->dequeueNotification(); + } +} + +void ChangeRecorder::setChangeRecordingEnabled(bool enable) +{ + Q_D(ChangeRecorder); + if (d->enableChangeRecording == enable) { + return; + } + d->enableChangeRecording = enable; + if (enable) { + d->m_needFullSave = true; + d->notificationsLoaded(); + } else { + d->dispatchNotifications(); + } +} + +QString Akonadi::ChangeRecorder::dumpNotificationListToString() const +{ + Q_D(const ChangeRecorder); + return d->dumpNotificationListToString(); +} diff --git a/src/core/changerecorder.h b/src/core/changerecorder.h new file mode 100644 index 0000000..95ddfb4 --- /dev/null +++ b/src/core/changerecorder.h @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "monitor.h" + +class QSettings; + +namespace Akonadi +{ +class ChangeRecorderPrivate; + +/** + * @short Records and replays change notification. + * + * This class is responsible for recording change notifications while + * an agent is not online and replaying the notifications when the agent + * is online again. Therefore the agent doesn't have to care about + * online/offline mode in its synchronization algorithm. + * + * Unlike Akonadi::Monitor this class only emits one change signal at a + * time. To receive the next one you need to explicitly call replayNext(). + * If a signal is emitted that has no receivers, it's automatically skipped, + * which means you only need to connect to signals you are actually interested + * in. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ChangeRecorder : public Monitor +{ + Q_OBJECT +public: + /** + * Creates a new change recorder. + */ + explicit ChangeRecorder(QObject *parent = nullptr); + + /** + * Destroys the change recorder. + * All not yet processed changes are written back to the config file. + */ + ~ChangeRecorder(); + + /** + * Sets the QSettings object used for persistent recorded changes. + */ + void setConfig(QSettings *settings); + + /** + * Returns whether there are recorded changes. + */ + Q_REQUIRED_RESULT bool isEmpty() const; + + /** + * Removes the previously emitted change from the records. + */ + void changeProcessed(); + + /** + * Enables change recording. If change recording is disabled, this class + * behaves exactly like Akonadi::Monitor. + * Change recording is enabled by default. + * @param enable @c false to disable change recording. @c true by default + */ + void setChangeRecordingEnabled(bool enable); + + /** + * Debugging: dump current list of notifications, as saved on disk. + */ + Q_REQUIRED_RESULT QString dumpNotificationListToString() const; + +public Q_SLOTS: + /** + * Replay the next change notification and erase the previous one from the record. + */ + void replayNext(); + +Q_SIGNALS: + /** + * Emitted when new changes are recorded. + */ + void changesAdded(); + + /** + * Emitted when replayNext() was called, but there was no valid change to replay. + * This can happen when all pending changes have been filtered out, for example. + * You only need to connect to this signal if you rely on one signal being emitted + * as a result of calling replayNext(). + */ + void nothingToReplay(); + +protected: + /// @cond PRIVATE + explicit ChangeRecorder(ChangeRecorderPrivate *d, QObject *parent = nullptr); + /// @endcond + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(ChangeRecorder) + /// @endcond +}; + +} + diff --git a/src/core/changerecorder_p.cpp b/src/core/changerecorder_p.cpp new file mode 100644 index 0000000..cd15996 --- /dev/null +++ b/src/core/changerecorder_p.cpp @@ -0,0 +1,223 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "changerecorder_p.h" +#include "akonadicore_debug.h" +#include "changerecorderjournal_p.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +ChangeRecorderPrivate::ChangeRecorderPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, ChangeRecorder *parent) + : MonitorPrivate(dependenciesFactory_, parent) +{ +} + +int ChangeRecorderPrivate::pipelineSize() const +{ + if (enableChangeRecording) { + return 0; // we fill the pipeline ourselves when using change recording + } + return MonitorPrivate::pipelineSize(); +} + +void ChangeRecorderPrivate::slotNotify(const Protocol::ChangeNotificationPtr &msg) +{ + Q_Q(ChangeRecorder); + const int oldChanges = pendingNotifications.size(); + // with change recording disabled this will automatically take care of dispatching notification messages and saving + MonitorPrivate::slotNotify(msg); + if (enableChangeRecording && pendingNotifications.size() != oldChanges) { + Q_EMIT q->changesAdded(); + } +} + +// The QSettings object isn't actually used anymore, except for migrating old data +// and it gives us the base of the filename to use. This is all historical. +QString ChangeRecorderPrivate::notificationsFileName() const +{ + return settings->fileName() + QStringLiteral("_changes.dat"); +} + +void ChangeRecorderPrivate::loadNotifications() +{ + pendingNotifications.clear(); + Q_ASSERT(pipeline.isEmpty()); + pipeline.clear(); + + const QString changesFileName = notificationsFileName(); + + /** + * In an older version we recorded changes inside the settings object, however + * for performance reasons we changed that to store them in a separated file. + * If this file doesn't exists, it means we run the new version the first time, + * so we have to read in the legacy list of changes first. + */ + if (!QFile::exists(changesFileName)) { + settings->beginGroup(QStringLiteral("ChangeRecorder")); + const int size = settings->beginReadArray(QStringLiteral("change")); + + for (int i = 0; i < size; ++i) { + settings->setArrayIndex(i); + auto msg = ChangeRecorderJournalReader::loadQSettingsNotification(settings); + if (msg->isValid()) { + pendingNotifications << msg; + } + } + + settings->endArray(); + + // save notifications to the new file... + saveNotifications(); + + // ...delete the legacy list... + settings->remove(QString()); + settings->endGroup(); + + // ...and continue as usually + } + + QFile file(changesFileName); + if (file.open(QIODevice::ReadOnly)) { + m_needFullSave = false; + pendingNotifications = ChangeRecorderJournalReader::loadFrom(&file, m_needFullSave); + } else { + m_needFullSave = true; + } + notificationsLoaded(); +} + +QString ChangeRecorderPrivate::dumpNotificationListToString() const +{ + if (!settings) { + return QStringLiteral("No settings set in ChangeRecorder yet."); + } + const QString changesFileName = notificationsFileName(); + QFile file(changesFileName); + + if (!file.open(QIODevice::ReadOnly)) { + return QLatin1String("Error reading ") + changesFileName; + } + + QString result; + bool dummy; + const auto notifications = ChangeRecorderJournalReader::loadFrom(&file, dummy); + for (const auto &n : notifications) { + result += Protocol::debugString(n) + QLatin1Char('\n'); + } + return result; +} + +void ChangeRecorderPrivate::writeStartOffset() const +{ + if (!settings) { + return; + } + + QFile file(notificationsFileName()); + if (!file.open(QIODevice::ReadWrite)) { + qCWarning(AKONADICORE_LOG) << "Could not update notifications in file" << file.fileName(); + return; + } + + // Skip "countAndVersion" + file.seek(8); + + // qCDebug(AKONADICORE_LOG) << "Writing start offset=" << m_startOffset; + + QDataStream stream(&file); + stream.setVersion(QDataStream::Qt_4_6); + stream << static_cast(m_startOffset); + + // Everything else stays unchanged +} + +void ChangeRecorderPrivate::saveNotifications() +{ + if (!settings) { + return; + } + + QFile file(notificationsFileName()); + QFileInfo info(file); + if (!QFile::exists(info.absolutePath())) { + QDir dir; + dir.mkpath(info.absolutePath()); + } + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(AKONADICORE_LOG) << "Could not save notifications to file" << file.fileName(); + return; + } + ChangeRecorderJournalWriter::saveTo(pendingNotifications, &file); + m_needFullSave = false; + m_startOffset = 0; +} + +void ChangeRecorderPrivate::notificationsEnqueued(int count) +{ + // Just to ensure the contract is kept, and these two methods are always properly called. + if (enableChangeRecording) { + m_lastKnownNotificationsCount += count; + if (m_lastKnownNotificationsCount != pendingNotifications.count()) { + qCWarning(AKONADICORE_LOG) << this << "The number of pending notifications changed without telling us! Expected" << m_lastKnownNotificationsCount + << "but got" << pendingNotifications.count() << "Caller just added" << count; + Q_ASSERT(pendingNotifications.count() == m_lastKnownNotificationsCount); + } + + saveNotifications(); + } +} + +void ChangeRecorderPrivate::dequeueNotification() +{ + if (pendingNotifications.isEmpty()) { + return; + } + + pendingNotifications.dequeue(); + if (enableChangeRecording) { + Q_ASSERT(pendingNotifications.count() == m_lastKnownNotificationsCount - 1); + --m_lastKnownNotificationsCount; + + if (m_needFullSave || pendingNotifications.isEmpty()) { + saveNotifications(); + } else { + ++m_startOffset; + writeStartOffset(); + } + } +} + +void ChangeRecorderPrivate::notificationsErased() +{ + if (enableChangeRecording) { + m_lastKnownNotificationsCount = pendingNotifications.count(); + m_needFullSave = true; + saveNotifications(); + } +} + +void ChangeRecorderPrivate::notificationsLoaded() +{ + m_lastKnownNotificationsCount = pendingNotifications.count(); + m_startOffset = 0; +} + +bool ChangeRecorderPrivate::emitNotification(const Protocol::ChangeNotificationPtr &msg) +{ + const bool someoneWasListening = MonitorPrivate::emitNotification(msg); + if (!someoneWasListening && enableChangeRecording) { + // If no signal was emitted (e.g. because no one was connected to it), no one is going to call changeProcessed, so we help ourselves. + dequeueNotification(); + QMetaObject::invokeMethod(q_ptr, "replayNext", Qt::QueuedConnection); + } + return someoneWasListening; +} diff --git a/src/core/changerecorder_p.h b/src/core/changerecorder_p.h new file mode 100644 index 0000000..d6dbcb3 --- /dev/null +++ b/src/core/changerecorder_p.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" +#include "changerecorder.h" +#include "monitor_p.h" + +class QDataStream; + +namespace Akonadi +{ +class ChangeRecorder; +class ChangeNotificationDependenciesFactory; + +class AKONADI_TESTS_EXPORT ChangeRecorderPrivate : public Akonadi::MonitorPrivate +{ +public: + ChangeRecorderPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, ChangeRecorder *parent); + + Q_DECLARE_PUBLIC(ChangeRecorder) + QSettings *settings = nullptr; + bool enableChangeRecording = true; + + int pipelineSize() const override; + void notificationsEnqueued(int count) override; + void notificationsErased() override; + + void slotNotify(const Protocol::ChangeNotificationPtr &msg) override; + bool emitNotification(const Protocol::ChangeNotificationPtr &msg) override; + + QString notificationsFileName() const; + + void loadNotifications(); + QString dumpNotificationListToString() const; + void saveNotifications(); + +private: + void dequeueNotification(); + void notificationsLoaded(); + void writeStartOffset() const; + + int m_lastKnownNotificationsCount = 0; // just for invariant checking + int m_startOffset = 0; // number of saved notifications to skip + bool m_needFullSave = true; +}; + +} // namespace Akonadi + diff --git a/src/core/changerecorderjournal.cpp b/src/core/changerecorderjournal.cpp new file mode 100644 index 0000000..1119e29 --- /dev/null +++ b/src/core/changerecorderjournal.cpp @@ -0,0 +1,1011 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "changerecorderjournal_p.h" + +#include +#include +#include +#include + +using namespace Akonadi; + +namespace +{ +constexpr quint64 s_currentVersion = Q_UINT64_C(0x000800000000); +constexpr quint64 s_versionMask = Q_UINT64_C(0xFFFF00000000); +constexpr quint64 s_sizeMask = Q_UINT64_C(0x0000FFFFFFFF); +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadQSettingsNotification(QSettings *settings) +{ + switch (static_cast(settings->value(QStringLiteral("type")).toInt())) { + case Item: + return loadQSettingsItemNotification(settings); + case Collection: + return loadQSettingsCollectionNotification(settings); + case Tag: + case Relation: + case InvalidType: + default: + qWarning() << "Unexpected notification type in legacy store"; + return {}; + } +} + +QQueue ChangeRecorderJournalReader::loadFrom(QFile *device, bool &needsFullSave) +{ + QDataStream stream(device); + stream.setVersion(QDataStream::Qt_4_6); + + QByteArray sessionId; + int type; + + QQueue list; + + quint64 sizeAndVersion; + stream >> sizeAndVersion; + + const quint64 size = sizeAndVersion & s_sizeMask; + const quint64 version = (sizeAndVersion & s_versionMask) >> 32; + + quint64 startOffset = 0; + if (version >= 1) { + stream >> startOffset; + } + + // If we skip the first N items, then we'll need to rewrite the file on saving. + // Also, if the file is old, it needs to be rewritten. + needsFullSave = startOffset > 0 || version == 0; + + for (quint64 i = 0; i < size && !stream.atEnd(); ++i) { + Protocol::ChangeNotificationPtr msg; + stream >> sessionId; + stream >> type; + + if (stream.status() != QDataStream::Ok) { + qCWarning(AKONADICORE_LOG) << "Error reading saved notifications! Aborting. Corrupt file:" << device->fileName(); + break; + } + + switch (static_cast(type)) { + case Item: + msg = loadItemNotification(stream, version); + break; + case Collection: + msg = loadCollectionNotification(stream, version); + break; + case Tag: + msg = loadTagNotification(stream, version); + break; + case Relation: + msg = loadRelationNotification(stream, version); + break; + default: + qCWarning(AKONADICORE_LOG) << "Unknown notification type"; + break; + } + + if (i < startOffset) { + continue; + } + + if (msg && msg->isValid()) { + msg->setSessionId(sessionId); + list << msg; + } + } + + return list; +} + +void ChangeRecorderJournalWriter::saveTo(const QQueue ¬ifications, QIODevice *device) +{ + // Version 0 of this file format was writing a quint64 count, followed by the notifications. + // Version 1 bundles a version number into that quint64, to be able to detect a version number at load time. + + const quint64 countAndVersion = static_cast(notifications.count()) | s_currentVersion; + + QDataStream stream(device); + stream.setVersion(QDataStream::Qt_4_6); + + stream << countAndVersion; + stream << quint64(0); // no start offset + + // qCDebug(AKONADICORE_LOG) << "Saving" << pendingNotifications.count() << "notifications (full save)"; + + for (int i = 0; i < notifications.count(); ++i) { + const Protocol::ChangeNotificationPtr &msg = notifications.at(i); + + // We deliberately don't use Factory::serialize(), because the internal + // serialization format could change at any point + + stream << msg->sessionId(); + stream << int(mapToLegacyType(msg->type())); + switch (msg->type()) { + case Protocol::Command::ItemChangeNotification: + saveItemNotification(stream, Protocol::cmdCast(msg)); + break; + case Protocol::Command::CollectionChangeNotification: + saveCollectionNotification(stream, Protocol::cmdCast(msg)); + break; + case Protocol::Command::TagChangeNotification: + saveTagNotification(stream, Protocol::cmdCast(msg)); + break; + case Protocol::Command::RelationChangeNotification: + saveRelationNotification(stream, Protocol::cmdCast(msg)); + break; + default: + qCWarning(AKONADICORE_LOG) << "Unexpected type?"; + return; + } + } +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadQSettingsItemNotification(QSettings *settings) +{ + auto msg = Protocol::ItemChangeNotificationPtr::create(); + msg->setSessionId(settings->value(QStringLiteral("sessionId")).toByteArray()); + msg->setOperation(mapItemOperation(static_cast(settings->value(QStringLiteral("op")).toInt()))); + Protocol::FetchItemsResponse item; + item.setId(settings->value(QStringLiteral("uid")).toLongLong()); + item.setRemoteId(settings->value(QStringLiteral("rid")).toString()); + item.setMimeType(settings->value(QStringLiteral("mimeType")).toString()); + msg->setItems({std::move(item)}); + msg->addMetadata("FETCH_ITEM"); + msg->setResource(settings->value(QStringLiteral("resource")).toByteArray()); + msg->setParentCollection(settings->value(QStringLiteral("parentCol")).toLongLong()); + msg->setParentDestCollection(settings->value(QStringLiteral("parentDestCol")).toLongLong()); + const QStringList list = settings->value(QStringLiteral("itemParts")).toStringList(); + QSet itemParts; + for (const QString &entry : list) { + itemParts.insert(entry.toLatin1()); + } + msg->setItemParts(itemParts); + return msg; +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadQSettingsCollectionNotification(QSettings *settings) +{ + auto msg = Protocol::CollectionChangeNotificationPtr::create(); + msg->setSessionId(settings->value(QStringLiteral("sessionId")).toByteArray()); + msg->setOperation(mapCollectionOperation(static_cast(settings->value(QStringLiteral("op")).toInt()))); + Protocol::FetchCollectionsResponse collection; + collection.setId(settings->value(QStringLiteral("uid")).toLongLong()); + collection.setRemoteId(settings->value(QStringLiteral("rid")).toString()); + msg->setCollection(std::move(collection)); + msg->addMetadata("FETCH_COLLECTION"); + msg->setResource(settings->value(QStringLiteral("resource")).toByteArray()); + msg->setParentCollection(settings->value(QStringLiteral("parentCol")).toLongLong()); + msg->setParentDestCollection(settings->value(QStringLiteral("parentDestCol")).toLongLong()); + const QStringList list = settings->value(QStringLiteral("itemParts")).toStringList(); + QSet changedParts; + for (const QString &entry : list) { + changedParts.insert(entry.toLatin1()); + } + msg->setChangedParts(changedParts); + return msg; +} + +QSet ChangeRecorderJournalReader::extractRelations(QSet &flags) +{ + QSet relations; + auto iter = flags.begin(); + while (iter != flags.end()) { + if (iter->startsWith("RELATION")) { + const QByteArrayList parts = iter->split(' '); + Q_ASSERT(parts.size() == 4); + Protocol::ItemChangeNotification::Relation relation; + relation.type = QString::fromLatin1(parts[1]); + relation.leftId = parts[2].toLongLong(); + relation.rightId = parts[3].toLongLong(); + relations.insert(relation); + iter = flags.erase(iter); + } else { + ++iter; + } + } + + return relations; +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadItemNotification(QDataStream &stream, quint64 version) +{ + QByteArray resource; + QByteArray destinationResource; + int operation; + int entityCnt; + qint64 uid; + qint64 parentCollection; + qint64 parentDestCollection; + QString remoteId; + QString mimeType; + QString remoteRevision; + QSet itemParts; + QSet addedFlags; + QSet removedFlags; + QSet addedTags; + QSet removedTags; + QVector items; + + auto msg = Protocol::ItemChangeNotificationPtr::create(); + + if (version == 1) { + stream >> operation; + stream >> uid; + stream >> remoteId; + stream >> resource; + stream >> parentCollection; + stream >> parentDestCollection; + stream >> mimeType; + stream >> itemParts; + + Protocol::FetchItemsResponse item; + item.setId(uid); + item.setRemoteId(remoteId); + item.setMimeType(mimeType); + items.push_back(std::move(item)); + msg->addMetadata("FETCH_ITEM"); + } else if (version >= 2) { + stream >> operation; + stream >> entityCnt; + if (version >= 7) { + QByteArray ba; + qint64 i64; + int i; + QDateTime dt; + QString str; + QVector bav; + QVector i64v; + QMap babaMap; + int cnt; + for (int j = 0; j < entityCnt; ++j) { + Protocol::FetchItemsResponse item; + stream >> i64; + item.setId(i64); + stream >> i; + item.setRevision(i); + stream >> i64; + item.setParentId(i64); + stream >> str; + item.setRemoteId(str); + stream >> str; + item.setRemoteRevision(str); + stream >> str; + item.setGid(str); + stream >> i64; + item.setSize(i64); + stream >> str; + item.setMimeType(str); + stream >> dt; + item.setMTime(dt); + stream >> bav; + item.setFlags(bav); + stream >> cnt; + QVector tags; + tags.reserve(cnt); + for (int k = 0; k < cnt; ++k) { + Protocol::FetchTagsResponse tag; + stream >> i64; + tag.setId(i64); + stream >> i64; + tag.setParentId(i64); + stream >> ba; + tag.setGid(ba); + stream >> ba; + tag.setType(ba); + stream >> ba; + tag.setRemoteId(ba); + stream >> babaMap; + tag.setAttributes(babaMap); + tags << tag; + } + item.setTags(tags); + stream >> i64v; + item.setVirtualReferences(i64v); + stream >> cnt; + QVector relations; + for (int k = 0; k < cnt; ++k) { + Protocol::FetchRelationsResponse relation; + stream >> i64; + relation.setLeft(i64); + stream >> ba; + relation.setLeftMimeType(ba); + stream >> i64; + relation.setRight(i64); + stream >> ba; + relation.setRightMimeType(ba); + stream >> ba; + relation.setType(ba); + stream >> ba; + relation.setRemoteId(ba); + relations << relation; + } + item.setRelations(relations); + stream >> cnt; + QVector ancestors; + for (int k = 0; k < cnt; ++k) { + Protocol::Ancestor ancestor; + stream >> i64; + ancestor.setId(i64); + stream >> str; + ancestor.setRemoteId(str); + stream >> str; + ancestor.setName(str); + stream >> babaMap; + ancestor.setAttributes(babaMap); + ancestors << ancestor; + } + item.setAncestors(ancestors); + stream >> cnt; + QVector parts; + for (int k = 0; k < cnt; ++k) { + Protocol::StreamPayloadResponse part; + stream >> ba; + part.setPayloadName(ba); + Protocol::PartMetaData metaData; + stream >> ba; + metaData.setName(ba); + stream >> i64; + metaData.setSize(i64); + stream >> i; + metaData.setVersion(i); + stream >> i; + metaData.setStorageType(static_cast(i)); + part.setMetaData(metaData); + stream >> ba; + part.setData(ba); + parts << part; + } + item.setParts(parts); + stream >> bav; + item.setCachedParts(bav); + items.push_back(std::move(item)); + } + } else { + for (int j = 0; j < entityCnt; ++j) { + stream >> uid; + stream >> remoteId; + stream >> remoteRevision; + stream >> mimeType; + if (stream.status() != QDataStream::Ok) { + qCWarning(AKONADICORE_LOG) << "Error reading saved notifications! Aborting"; + return msg; + } + Protocol::FetchItemsResponse item; + item.setId(uid); + item.setRemoteId(remoteId); + item.setRemoteRevision(remoteRevision); + item.setMimeType(mimeType); + items.push_back(std::move(item)); + } + msg->addMetadata("FETCH_ITEM"); + } + stream >> resource; + stream >> destinationResource; + stream >> parentCollection; + stream >> parentDestCollection; + stream >> itemParts; + stream >> addedFlags; + stream >> removedFlags; + if (version >= 3) { + stream >> addedTags; + stream >> removedTags; + } + if (version >= 8) { + bool boolean; + stream >> boolean; + msg->setMustRetrieve(boolean); + } + } else { + qCWarning(AKONADICORE_LOG) << "Error version is not correct here" << version; + return msg; + } + if (version >= 5) { + msg->setOperation(static_cast(operation)); + } else { + msg->setOperation(mapItemOperation(static_cast(operation))); + } + msg->setItems(items); + msg->setResource(resource); + msg->setDestinationResource(destinationResource); + msg->setParentCollection(parentCollection); + msg->setParentDestCollection(parentDestCollection); + msg->setItemParts(itemParts); + msg->setAddedRelations(extractRelations(addedFlags)); + msg->setAddedFlags(addedFlags); + msg->setRemovedRelations(extractRelations(removedFlags)); + msg->setRemovedFlags(removedFlags); + msg->setAddedTags(addedTags); + msg->setRemovedTags(removedTags); + return msg; +} + +QSet ChangeRecorderJournalWriter::encodeRelations(const QSet &relations) +{ + QSet rv; + for (const auto &rel : relations) { + rv.insert("RELATION " + rel.type.toLatin1() + ' ' + QByteArray::number(rel.leftId) + ' ' + QByteArray::number(rel.rightId)); + } + return rv; +} + +void ChangeRecorderJournalWriter::saveItemNotification(QDataStream &stream, const Protocol::ItemChangeNotification &msg) +{ + // Version 8 + + stream << int(msg.operation()); + const auto &items = msg.items(); + stream << items.count(); + for (const auto &item : items) { + stream << item.id() << item.revision() << item.parentId() << item.remoteId() << item.remoteRevision() << item.gid() << item.size() << item.mimeType() + << item.mTime() << item.flags(); + const auto tags = item.tags(); + stream << tags.count(); + for (const auto &tag : tags) { + stream << tag.id() << tag.parentId() << tag.gid() << tag.type() << tag.remoteId() << tag.attributes(); + } + stream << item.virtualReferences(); + const auto relations = item.relations(); + stream << relations.count(); + for (const auto &relation : relations) { + stream << relation.left() << relation.leftMimeType() << relation.right() << relation.rightMimeType() << relation.type() << relation.remoteId(); + } + const auto ancestors = item.ancestors(); + stream << ancestors.count(); + for (const auto &ancestor : ancestors) { + stream << ancestor.id() << ancestor.remoteId() << ancestor.name() << ancestor.attributes(); + } + const auto parts = item.parts(); + stream << parts.count(); + for (const auto &part : parts) { + const auto metaData = part.metaData(); + stream << part.payloadName() << metaData.name() << metaData.size() << metaData.version() << static_cast(metaData.storageType()) << part.data(); + } + stream << item.cachedParts(); + } + stream << msg.resource(); + stream << msg.destinationResource(); + stream << quint64(msg.parentCollection()); + stream << quint64(msg.parentDestCollection()); + stream << msg.itemParts(); + stream << msg.addedFlags() + encodeRelations(msg.addedRelations()); + stream << msg.removedFlags() + encodeRelations(msg.removedRelations()); + stream << msg.addedTags(); + stream << msg.removedTags(); + stream << msg.mustRetrieve(); +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadCollectionNotification(QDataStream &stream, quint64 version) +{ + QByteArray resource; + QByteArray destinationResource; + int operation; + int entityCnt; + quint64 uid; + quint64 parentCollection; + quint64 parentDestCollection; + QString remoteId; + QString remoteRevision; + QString dummyString; + QSet changedParts; + QSet dummyBa; + QSet dummyIv; + + auto msg = Protocol::CollectionChangeNotificationPtr::create(); + + if (version == 1) { + stream >> operation; + stream >> uid; + stream >> remoteId; + stream >> resource; + stream >> parentCollection; + stream >> parentDestCollection; + stream >> dummyString; + stream >> changedParts; + + Protocol::FetchCollectionsResponse collection; + collection.setId(uid); + collection.setRemoteId(remoteId); + msg->setCollection(std::move(collection)); + msg->addMetadata("FETCH_COLLECTION"); + } else if (version >= 2) { + stream >> operation; + stream >> entityCnt; + if (version >= 7) { + QString str; + QStringList stringList; + qint64 i64; + QVector vb; + QMap attrs; + bool b; + int i; + Tristate tristate; + Protocol::FetchCollectionsResponse collection; + stream >> uid; + collection.setId(uid); + stream >> uid; + collection.setParentId(uid); + stream >> str; + collection.setName(str); + stream >> stringList; + collection.setMimeTypes(stringList); + stream >> str; + collection.setRemoteId(str); + stream >> str; + collection.setRemoteRevision(str); + stream >> str; + collection.setResource(str); + + Protocol::FetchCollectionStatsResponse stats; + stream >> i64; + stats.setCount(i64); + stream >> i64; + stats.setUnseen(i64); + stream >> i64; + stats.setSize(i64); + collection.setStatistics(stats); + + stream >> str; + collection.setSearchQuery(str); + stream >> vb; + collection.setSearchCollections(vb); + stream >> entityCnt; + QVector ancestors; + for (int i = 0; i < entityCnt; ++i) { + Protocol::Ancestor ancestor; + stream >> i64; + ancestor.setId(i64); + stream >> str; + ancestor.setRemoteId(str); + stream >> str; + ancestor.setName(str); + stream >> attrs; + ancestor.setAttributes(attrs); + ancestors.push_back(ancestor); + + if (stream.status() != QDataStream::Ok) { + qCWarning(AKONADICORE_LOG) << "Erorr reading saved notifications! Aborting"; + return msg; + } + } + collection.setAncestors(ancestors); + + Protocol::CachePolicy cachePolicy; + stream >> b; + cachePolicy.setInherit(b); + stream >> i; + cachePolicy.setCheckInterval(i); + stream >> i; + cachePolicy.setCacheTimeout(i); + stream >> b; + cachePolicy.setSyncOnDemand(b); + stream >> stringList; + cachePolicy.setLocalParts(stringList); + collection.setCachePolicy(cachePolicy); + + stream >> attrs; + collection.setAttributes(attrs); + stream >> b; + collection.setEnabled(b); + stream >> reinterpret_cast(tristate); + collection.setDisplayPref(tristate); + stream >> reinterpret_cast(tristate); + collection.setSyncPref(tristate); + stream >> reinterpret_cast(tristate); + collection.setIndexPref(tristate); + stream >> b; // read the deprecated "isReferenced" value + stream >> b; + collection.setIsVirtual(b); + + msg->setCollection(std::move(collection)); + } else { + for (int j = 0; j < entityCnt; ++j) { + stream >> uid; + stream >> remoteId; + stream >> remoteRevision; + stream >> dummyString; + if (stream.status() != QDataStream::Ok) { + qCWarning(AKONADICORE_LOG) << "Error reading saved notifications! Aborting"; + return msg; + } + Protocol::FetchCollectionsResponse collection; + collection.setId(uid); + collection.setRemoteId(remoteId); + collection.setRemoteRevision(remoteRevision); + msg->setCollection(std::move(collection)); + msg->addMetadata("FETCH_COLLECTION"); + } + } + stream >> resource; + stream >> destinationResource; + stream >> parentCollection; + stream >> parentDestCollection; + stream >> changedParts; + stream >> dummyBa; + stream >> dummyBa; + if (version >= 3) { + stream >> dummyIv; + stream >> dummyIv; + } + } else { + qCWarning(AKONADICORE_LOG) << "Error version is not correct here" << version; + return msg; + } + + if (version >= 5) { + msg->setOperation(static_cast(operation)); + } else { + msg->setOperation(mapCollectionOperation(static_cast(operation))); + } + msg->setResource(resource); + msg->setDestinationResource(destinationResource); + msg->setParentCollection(parentCollection); + msg->setParentDestCollection(parentDestCollection); + msg->setChangedParts(changedParts); + return msg; +} + +void Akonadi::ChangeRecorderJournalWriter::saveCollectionNotification(QDataStream &stream, const Protocol::CollectionChangeNotification &msg) +{ + // Version 7 + + const auto &col = msg.collection(); + + stream << int(msg.operation()); + stream << int(1); + stream << col.id(); + stream << col.parentId(); + stream << col.name(); + stream << col.mimeTypes(); + stream << col.remoteId(); + stream << col.remoteRevision(); + stream << col.resource(); + const auto stats = col.statistics(); + stream << stats.count(); + stream << stats.unseen(); + stream << stats.size(); + stream << col.searchQuery(); + stream << col.searchCollections(); + const auto ancestors = col.ancestors(); + stream << ancestors.count(); + for (const auto &ancestor : ancestors) { + stream << ancestor.id() << ancestor.remoteId() << ancestor.name() << ancestor.attributes(); + } + const auto cachePolicy = col.cachePolicy(); + stream << cachePolicy.inherit(); + stream << cachePolicy.checkInterval(); + stream << cachePolicy.cacheTimeout(); + stream << cachePolicy.syncOnDemand(); + stream << cachePolicy.localParts(); + stream << col.attributes(); + stream << col.enabled(); + stream << static_cast(col.displayPref()); + stream << static_cast(col.syncPref()); + stream << static_cast(col.indexPref()); + stream << false; // write the deprecated "isReferenced" value + stream << col.isVirtual(); + + stream << msg.resource(); + stream << msg.destinationResource(); + stream << quint64(msg.parentCollection()); + stream << quint64(msg.parentDestCollection()); + stream << msg.changedParts(); + stream << QSet(); + stream << QSet(); + stream << QSet(); + stream << QSet(); +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadTagNotification(QDataStream &stream, quint64 version) +{ + QByteArray resource; + QByteArray dummyBa; + int operation; + int entityCnt; + quint64 uid; + quint64 dummyI; + QString remoteId; + QString dummyString; + QSet dummyBaV; + QSet dummyIv; + + auto msg = Protocol::TagChangeNotificationPtr::create(); + + if (version == 1) { + stream >> operation; + stream >> uid; + stream >> remoteId; + stream >> dummyBa; + stream >> dummyI; + stream >> dummyI; + stream >> dummyString; + stream >> dummyBaV; + + Protocol::FetchTagsResponse tag; + tag.setId(uid); + tag.setRemoteId(remoteId.toLatin1()); + msg->setTag(std::move(tag)); + msg->addMetadata("FETCH_TAG"); + } else if (version >= 2) { + stream >> operation; + stream >> entityCnt; + if (version >= 7) { + QByteArray ba; + QMap attrs; + + Protocol::FetchTagsResponse tag; + + stream >> uid; + tag.setId(uid); + stream >> ba; + tag.setParentId(uid); + stream >> attrs; + tag.setGid(ba); + stream >> ba; + tag.setType(ba); + stream >> uid; + tag.setRemoteId(ba); + stream >> ba; + tag.setAttributes(attrs); + msg->setTag(std::move(tag)); + + stream >> resource; + } else { + for (int j = 0; j < entityCnt; ++j) { + stream >> uid; + stream >> remoteId; + stream >> dummyString; + stream >> dummyString; + if (stream.status() != QDataStream::Ok) { + qCWarning(AKONADICORE_LOG) << "Error reading saved notifications! Aborting"; + return msg; + } + Protocol::FetchTagsResponse tag; + tag.setId(uid); + tag.setRemoteId(remoteId.toLatin1()); + msg->setTag(std::move(tag)); + msg->addMetadata("FETCH_TAG"); + } + stream >> resource; + stream >> dummyBa; + stream >> dummyI; + stream >> dummyI; + stream >> dummyBaV; + stream >> dummyBaV; + stream >> dummyBaV; + if (version >= 3) { + stream >> dummyIv; + stream >> dummyIv; + } + } + if (version >= 5) { + msg->setOperation(static_cast(operation)); + } else { + msg->setOperation(mapTagOperation(static_cast(operation))); + } + } + msg->setResource(resource); + return msg; +} + +void Akonadi::ChangeRecorderJournalWriter::saveTagNotification(QDataStream &stream, const Protocol::TagChangeNotification &msg) +{ + const auto &tag = msg.tag(); + stream << int(msg.operation()); + stream << int(1); + stream << tag.id(); + stream << tag.parentId(); + stream << tag.gid(); + stream << tag.type(); + stream << tag.remoteId(); + stream << tag.attributes(); + stream << msg.resource(); +} + +Protocol::ChangeNotificationPtr ChangeRecorderJournalReader::loadRelationNotification(QDataStream &stream, quint64 version) +{ + QByteArray dummyBa; + int operation; + int entityCnt; + quint64 dummyI; + QString dummyString; + QSet itemParts; + QSet dummyBaV; + QSet dummyIv; + + auto msg = Protocol::RelationChangeNotificationPtr::create(); + + if (version == 1) { + qCWarning(AKONADICORE_LOG) << "Invalid version of relation notification"; + return msg; + } else if (version >= 2) { + stream >> operation; + stream >> entityCnt; + if (version >= 7) { + Protocol::FetchRelationsResponse relation; + qint64 i64; + QByteArray ba; + stream >> i64; + relation.setLeft(i64); + stream >> ba; + relation.setLeftMimeType(ba); + stream >> i64; + relation.setRight(i64); + stream >> ba; + relation.setRightMimeType(ba); + stream >> ba; + relation.setRemoteId(ba); + stream >> ba; + relation.setType(ba); + + msg->setRelation(std::move(relation)); + + } else { + for (int j = 0; j < entityCnt; ++j) { + stream >> dummyI; + stream >> dummyString; + stream >> dummyString; + stream >> dummyString; + if (stream.status() != QDataStream::Ok) { + qCWarning(AKONADICORE_LOG) << "Error reading saved notifications! Aborting"; + return msg; + } + } + stream >> dummyBa; + if (version == 5) { + // there was a bug in version 5 serializer that serialized this + // field as qint64 (8 bytes) instead of empty QByteArray (which is + // 4 bytes) + stream >> dummyI; + } else { + stream >> dummyBa; + } + stream >> dummyI; + stream >> dummyI; + stream >> itemParts; + stream >> dummyBaV; + stream >> dummyBaV; + if (version >= 3) { + stream >> dummyIv; + stream >> dummyIv; + } + + Protocol::FetchRelationsResponse relation; + for (const QByteArray &part : std::as_const(itemParts)) { + const QByteArrayList p = part.split(' '); + if (p.size() < 2) { + continue; + } + if (p[0] == "LEFT") { + relation.setLeft(p[1].toLongLong()); + } else if (p[0] == "RIGHT") { + relation.setRight(p[1].toLongLong()); + } else if (p[0] == "RID") { + relation.setRemoteId(p[1]); + } else if (p[0] == "TYPE") { + relation.setType(p[1]); + } + } + msg->setRelation(std::move(relation)); + } + if (version >= 5) { + msg->setOperation(static_cast(operation)); + } else { + msg->setOperation(mapRelationOperation(static_cast(operation))); + } + } + + return msg; +} + +void Akonadi::ChangeRecorderJournalWriter::saveRelationNotification(QDataStream &stream, const Protocol::RelationChangeNotification &msg) +{ + const auto &rel = msg.relation(); + stream << int(msg.operation()); + stream << int(0); + stream << rel.left(); + stream << rel.leftMimeType(); + stream << rel.right(); + stream << rel.rightMimeType(); + stream << rel.remoteId(); + stream << rel.type(); +} + +Protocol::ItemChangeNotification::Operation ChangeRecorderJournalReader::mapItemOperation(LegacyOp op) +{ + switch (op) { + case Add: + return Protocol::ItemChangeNotification::Add; + case Modify: + return Protocol::ItemChangeNotification::Modify; + case Move: + return Protocol::ItemChangeNotification::Move; + case Remove: + return Protocol::ItemChangeNotification::Remove; + case Link: + return Protocol::ItemChangeNotification::Link; + case Unlink: + return Protocol::ItemChangeNotification::Unlink; + case ModifyFlags: + return Protocol::ItemChangeNotification::ModifyFlags; + case ModifyTags: + return Protocol::ItemChangeNotification::ModifyTags; + case ModifyRelations: + return Protocol::ItemChangeNotification::ModifyRelations; + default: + qWarning() << "Unexpected operation type in item notification"; + return Protocol::ItemChangeNotification::InvalidOp; + } +} + +Protocol::CollectionChangeNotification::Operation ChangeRecorderJournalReader::mapCollectionOperation(LegacyOp op) +{ + switch (op) { + case Add: + return Protocol::CollectionChangeNotification::Add; + case Modify: + return Protocol::CollectionChangeNotification::Modify; + case Move: + return Protocol::CollectionChangeNotification::Move; + case Remove: + return Protocol::CollectionChangeNotification::Remove; + case Subscribe: + return Protocol::CollectionChangeNotification::Subscribe; + case Unsubscribe: + return Protocol::CollectionChangeNotification::Unsubscribe; + default: + qCWarning(AKONADICORE_LOG) << "Unexpected operation type in collection notification"; + return Protocol::CollectionChangeNotification::InvalidOp; + } +} + +Protocol::TagChangeNotification::Operation ChangeRecorderJournalReader::mapTagOperation(LegacyOp op) +{ + switch (op) { + case Add: + return Protocol::TagChangeNotification::Add; + case Modify: + return Protocol::TagChangeNotification::Modify; + case Remove: + return Protocol::TagChangeNotification::Remove; + default: + qCWarning(AKONADICORE_LOG) << "Unexpected operation type in tag notification"; + return Protocol::TagChangeNotification::InvalidOp; + } +} + +Protocol::RelationChangeNotification::Operation ChangeRecorderJournalReader::mapRelationOperation(LegacyOp op) +{ + switch (op) { + case Add: + return Protocol::RelationChangeNotification::Add; + case Remove: + return Protocol::RelationChangeNotification::Remove; + default: + qCWarning(AKONADICORE_LOG) << "Unexpected operation type in relation notification"; + return Protocol::RelationChangeNotification::InvalidOp; + } +} + +ChangeRecorderJournalReader::LegacyType ChangeRecorderJournalWriter::mapToLegacyType(Protocol::Command::Type type) +{ + switch (type) { + case Protocol::Command::ItemChangeNotification: + return ChangeRecorderJournalReader::Item; + case Protocol::Command::CollectionChangeNotification: + return ChangeRecorderJournalReader::Collection; + case Protocol::Command::TagChangeNotification: + return ChangeRecorderJournalReader::Tag; + case Protocol::Command::RelationChangeNotification: + return ChangeRecorderJournalReader::Relation; + default: + qCWarning(AKONADICORE_LOG) << "Unexpected notification type"; + return ChangeRecorderJournalReader::InvalidType; + } +} diff --git a/src/core/changerecorderjournal_p.h b/src/core/changerecorderjournal_p.h new file mode 100644 index 0000000..65705a6 --- /dev/null +++ b/src/core/changerecorderjournal_p.h @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditests_export.h" +#include "private/protocol_p.h" + +class QSettings; +class QFile; + +namespace Akonadi +{ +class AKONADI_TESTS_EXPORT ChangeRecorderJournalReader +{ +public: + enum LegacyType { + InvalidType, + Item, + Collection, + Tag, + Relation, + }; + + // Ancient QSettings legacy store + static Protocol::ChangeNotificationPtr loadQSettingsNotification(QSettings *settings); + + static QQueue loadFrom(QFile *device, bool &needsFullSave); + +private: + enum LegacyOp { + InvalidOp, + Add, + Modify, + Move, + Remove, + Link, + Unlink, + Subscribe, + Unsubscribe, + ModifyFlags, + ModifyTags, + ModifyRelations, + }; + + static Protocol::ChangeNotificationPtr loadQSettingsItemNotification(QSettings *settings); + static Protocol::ChangeNotificationPtr loadQSettingsCollectionNotification(QSettings *settings); + + // More modern mechanisms + static Protocol::ChangeNotificationPtr loadItemNotification(QDataStream &stream, quint64 version); + static Protocol::ChangeNotificationPtr loadCollectionNotification(QDataStream &stream, quint64 version); + static Protocol::ChangeNotificationPtr loadTagNotification(QDataStream &stream, quint64 version); + static Protocol::ChangeNotificationPtr loadRelationNotification(QDataStream &stream, quint64 version); + + static Protocol::ItemChangeNotification::Operation mapItemOperation(LegacyOp op); + static Protocol::CollectionChangeNotification::Operation mapCollectionOperation(LegacyOp op); + static Protocol::TagChangeNotification::Operation mapTagOperation(LegacyOp op); + static Protocol::RelationChangeNotification::Operation mapRelationOperation(LegacyOp op); + + static QSet extractRelations(QSet &flags); +}; + +class AKONADI_TESTS_EXPORT ChangeRecorderJournalWriter +{ +public: + static void saveTo(const QQueue &changes, QIODevice *device); + +private: + static ChangeRecorderJournalReader::LegacyType mapToLegacyType(Protocol::Command::Type type); + + static void saveItemNotification(QDataStream &stream, const Protocol::ItemChangeNotification &ntf); + static void saveCollectionNotification(QDataStream &stream, const Protocol::CollectionChangeNotification &ntf); + static void saveTagNotification(QDataStream &stream, const Protocol::TagChangeNotification &ntf); + static void saveRelationNotification(QDataStream &stream, const Protocol::RelationChangeNotification &ntf); + + static QSet encodeRelations(const QSet &relations); +}; + +} // namespace + diff --git a/src/core/collection.cpp b/src/core/collection.cpp new file mode 100644 index 0000000..fca0f37 --- /dev/null +++ b/src/core/collection.cpp @@ -0,0 +1,427 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collection.h" +#include "collection_p.h" + +#include "attributefactory.h" +#include "cachepolicy.h" +#include "collectionrightsattribute_p.h" +#include "collectionstatistics.h" +#include "entitydisplayattribute.h" + +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +Q_GLOBAL_STATIC(Akonadi::Collection, s_defaultParentCollection) // NOLINT(readability-redundant-member-init) + +uint Akonadi::qHash(const Akonadi::Collection &collection) +{ + return ::qHash(collection.id()); +} + +/** + * Helper method for assignment operator and copy constructor. + */ +static void assignCollectionPrivate(QSharedDataPointer &one, const QSharedDataPointer &other) +{ + // We can't simply do one = other here, we have to use a temp. + // Otherwise ProtocolHelperTest::testParentCollectionAfterCollectionParsing() + // will break. + // + // The reason are assignments like + // col = col.parentCollection() + // + // Here, parentCollection() actually returns a reference to a pointer owned + // by col. So when col (or rather, it's private class) is deleted, the pointer + // to the parent collection and therefore the reference becomes invalid. + // + // With a single-line assignment here, the parent collection would be deleted + // before it is assigned, and therefore the resulting object would point to + // uninitalized memory. + const QSharedDataPointer temp = other; // NOLINT(performance-unnecessary-copy-initialization): see above + one = temp; +} + +class CollectionRoot : public Collection +{ +public: + CollectionRoot() + : Collection(0) + { + setContentMimeTypes({Collection::mimeType()}); + + // The root collection is read-only for the users + setRights(Collection::ReadOnly); + } +}; + +Q_GLOBAL_STATIC(CollectionRoot, s_root) // NOLINT(readability-redundant-member-init) + +Collection::Collection() + : d_ptr(new CollectionPrivate) +{ + static int lastId = -1; + d_ptr->mId = lastId--; +} + +Collection::Collection(Id id) + : d_ptr(new CollectionPrivate(id)) +{ +} + +Collection::Collection(const Collection &other) +{ + assignCollectionPrivate(d_ptr, other.d_ptr); +} + +Collection::Collection(Collection &&) noexcept = default; + +Collection::~Collection() = default; + +void Collection::setId(Collection::Id identifier) +{ + d_ptr->mId = identifier; +} + +Collection::Id Collection::id() const +{ + return d_ptr->mId; +} + +void Collection::setRemoteId(const QString &id) +{ + d_ptr->mRemoteId = id; +} + +QString Collection::remoteId() const +{ + return d_ptr->mRemoteId; +} + +void Collection::setRemoteRevision(const QString &revision) +{ + d_ptr->mRemoteRevision = revision; +} + +QString Collection::remoteRevision() const +{ + return d_ptr->mRemoteRevision; +} + +bool Collection::isValid() const +{ + return (d_ptr->mId >= 0); +} + +bool Collection::operator==(const Collection &other) const +{ + // Invalid collections are the same, no matter what their internal ID is + return (!isValid() && !other.isValid()) || (d_ptr->mId == other.d_ptr->mId); +} + +bool Akonadi::Collection::operator!=(const Collection &other) const +{ + return (isValid() || other.isValid()) && (d_ptr->mId != other.d_ptr->mId); +} + +Collection &Collection ::operator=(const Collection &other) +{ + if (this != &other) { + assignCollectionPrivate(d_ptr, other.d_ptr); + } + + return *this; +} + +bool Akonadi::Collection::operator<(const Collection &other) const +{ + return d_ptr->mId < other.d_ptr->mId; +} + +void Collection::addAttribute(Attribute *attr) +{ + d_ptr->mAttributeStorage.addAttribute(attr); +} + +void Collection::removeAttribute(const QByteArray &type) +{ + d_ptr->mAttributeStorage.removeAttribute(type); +} + +bool Collection::hasAttribute(const QByteArray &type) const +{ + return d_ptr->mAttributeStorage.hasAttribute(type); +} + +Attribute::List Collection::attributes() const +{ + return d_ptr->mAttributeStorage.attributes(); +} + +void Akonadi::Collection::clearAttributes() +{ + d_ptr->mAttributeStorage.clearAttributes(); +} + +Attribute *Collection::attribute(const QByteArray &type) +{ + markAttributeModified(type); + return d_ptr->mAttributeStorage.attribute(type); +} + +const Attribute *Collection::attribute(const QByteArray &type) const +{ + return d_ptr->mAttributeStorage.attribute(type); +} + +Collection &Collection::parentCollection() +{ + if (!d_ptr->mParent) { + d_ptr->mParent.reset(new Collection()); + } + return *d_ptr->mParent; +} + +Collection Collection::parentCollection() const +{ + if (!d_ptr->mParent) { + return *(s_defaultParentCollection); + } else { + return *d_ptr->mParent; + } +} + +void Collection::setParentCollection(const Collection &parent) +{ + d_ptr->mParent.reset(new Collection(parent)); +} + +QString Collection::name() const +{ + return d_ptr->name; +} + +QString Collection::displayName() const +{ + const auto *const attr = attribute(); + const QString displayName = attr ? attr->displayName() : QString(); + return !displayName.isEmpty() ? displayName : d_ptr->name; +} + +void Collection::setName(const QString &name) +{ + d_ptr->name = name; +} + +Collection::Rights Collection::rights() const +{ + if (const auto *const attr = attribute()) { + return attr->rights(); + } else { + return AllRights; + } +} + +void Collection::setRights(Rights rights) +{ + attribute(AddIfMissing)->setRights(rights); +} + +QStringList Collection::contentMimeTypes() const +{ + return d_ptr->contentTypes; +} + +void Collection::setContentMimeTypes(const QStringList &types) +{ + if (d_ptr->contentTypes != types) { + d_ptr->contentTypes = types; + d_ptr->contentTypesChanged = true; + } +} + +QUrl Collection::url(UrlType type) const +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("collection"), QString::number(id())); + if (type == UrlWithName) { + query.addQueryItem(QStringLiteral("name"), name()); + } + + QUrl url; + url.setScheme(QStringLiteral("akonadi")); + url.setQuery(query); + return url; +} + +Collection Collection::fromUrl(const QUrl &url) +{ + if (url.scheme() != QLatin1String("akonadi")) { + return Collection(); + } + + const QString colStr = QUrlQuery(url).queryItemValue(QStringLiteral("collection")); + bool ok = false; + Collection::Id colId = colStr.toLongLong(&ok); + if (!ok) { + return Collection(); + } + + if (colId == 0) { + return Collection::root(); + } + + return Collection(colId); +} + +Collection Collection::root() +{ + return *s_root; +} + +QString Collection::mimeType() +{ + return QStringLiteral("inode/directory"); +} + +QString Akonadi::Collection::virtualMimeType() +{ + return QStringLiteral("application/x-vnd.akonadi.collection.virtual"); +} + +QString Collection::resource() const +{ + return d_ptr->resource; +} + +void Collection::setResource(const QString &resource) +{ + d_ptr->resource = resource; +} + +QDebug operator<<(QDebug d, const Akonadi::Collection &collection) +{ + return d << "Collection ID:" << collection.id() << " remote ID:" << collection.remoteId() << '\n' + << " name:" << collection.name() << '\n' + << " url:" << collection.url() << '\n' + << " parent:" << collection.parentCollection().id() << collection.parentCollection().remoteId() << '\n' + << " resource:" << collection.resource() << '\n' + << " rights:" << collection.rights() << '\n' + << " contents mime type:" << collection.contentMimeTypes() << '\n' + << " isVirtual:" << collection.isVirtual() << '\n' + << " " << collection.cachePolicy() << '\n' + << " " << collection.statistics(); +} + +CollectionStatistics Collection::statistics() const +{ + return d_ptr->statistics; +} + +void Collection::setStatistics(const CollectionStatistics &statistics) +{ + d_ptr->statistics = statistics; +} + +CachePolicy Collection::cachePolicy() const +{ + return d_ptr->cachePolicy; +} + +void Collection::setCachePolicy(const CachePolicy &cachePolicy) +{ + d_ptr->cachePolicy = cachePolicy; + d_ptr->cachePolicyChanged = true; +} + +bool Collection::isVirtual() const +{ + return d_ptr->isVirtual; +} + +void Akonadi::Collection::setVirtual(bool isVirtual) +{ + d_ptr->isVirtual = isVirtual; +} + +void Collection::setEnabled(bool enabled) +{ + d_ptr->enabledChanged = true; + d_ptr->enabled = enabled; +} + +bool Collection::enabled() const +{ + return d_ptr->enabled; +} + +void Collection::setLocalListPreference(Collection::ListPurpose purpose, Collection::ListPreference preference) +{ + switch (purpose) { + case ListDisplay: + d_ptr->displayPreference = preference; + break; + case ListSync: + d_ptr->syncPreference = preference; + break; + case ListIndex: + d_ptr->indexPreference = preference; + break; + } + d_ptr->listPreferenceChanged = true; +} + +Collection::ListPreference Collection::localListPreference(Collection::ListPurpose purpose) const +{ + switch (purpose) { + case ListDisplay: + return d_ptr->displayPreference; + case ListSync: + return d_ptr->syncPreference; + case ListIndex: + return d_ptr->indexPreference; + } + return ListDefault; +} + +bool Collection::shouldList(Collection::ListPurpose purpose) const +{ + if (localListPreference(purpose) == ListDefault) { + return enabled(); + } + return (localListPreference(purpose) == ListEnabled); +} + +void Collection::setShouldList(ListPurpose purpose, bool list) +{ + if (localListPreference(purpose) == ListDefault) { + setEnabled(list); + } else { + setLocalListPreference(purpose, list ? ListEnabled : ListDisabled); + } +} + +void Collection::setKeepLocalChanges(const QSet &parts) +{ + d_ptr->keepLocalChanges = parts; +} + +QSet Collection::keepLocalChanges() const +{ + return d_ptr->keepLocalChanges; +} + +void Collection::markAttributeModified(const QByteArray &type) +{ + d_ptr->mAttributeStorage.markAttributeModified(type); +} diff --git a/src/core/collection.h b/src/core/collection.h new file mode 100644 index 0000000..d46f25d --- /dev/null +++ b/src/core/collection.h @@ -0,0 +1,588 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include +#include +#include + +class QUrl; + +namespace Akonadi +{ +class CachePolicy; +class CollectionPrivate; +class CollectionStatistics; + +/** + * @short Represents a collection of PIM items. + * + * This class represents a collection of PIM items, such as a folder on a mail- or + * groupware-server. + * + * Collections are hierarchical, i.e., they may have a parent collection. + * + * @code + * + * using namespace Akonadi; + * + * // fetching all collections recursive, starting at the root collection + * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), CollectionFetchJob::Recursive ); + * connect( job, SIGNAL(result(KJob*)), SLOT(fetchFinished(KJob*)) ); + * + * ... + * + * MyClass::fetchFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * CollectionFetchJob *fetchJob = qobject_cast( job ); + * + * const Collection::List collections = fetchJob->collections(); + * for ( const Collection &collection : collections ) { + * qDebug() << "Name:" << collection.name(); + * } + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Collection +{ +public: + /** + * Describes the unique id type. + */ + using Id = qint64; + + /** + * Describes a list of collections. + */ + using List = QVector; + + /** + * Describes rights of a collection. + */ + enum Right { + ReadOnly = 0x0, ///< Can only read items or subcollection of this collection + CanChangeItem = 0x1, ///< Can change items in this collection + CanCreateItem = 0x2, ///< Can create new items in this collection + CanDeleteItem = 0x4, ///< Can delete items in this collection + CanChangeCollection = 0x8, ///< Can change this collection + CanCreateCollection = 0x10, ///< Can create new subcollections in this collection + CanDeleteCollection = 0x20, ///< Can delete this collection + CanLinkItem = 0x40, ///< Can create links to existing items in this virtual collection @since 4.4 + CanUnlinkItem = 0x80, ///< Can remove links to items in this virtual collection @since 4.4 + AllRights = (CanChangeItem | CanCreateItem | CanDeleteItem | CanChangeCollection | CanCreateCollection + | CanDeleteCollection) ///< Has all rights on this storage collection + }; + Q_DECLARE_FLAGS(Rights, Right) + + /** + * Creates an invalid collection. + */ + Collection(); + + /** + * Create a new collection. + * + * @param id The unique identifier of the collection. + */ + explicit Collection(Id id); + + /** + * Destroys the collection. + */ + ~Collection(); + + /** + * Creates a collection from an @p other collection. + */ + Collection(const Collection &other); + + /** + * Move constructor. + */ + Collection(Collection &&other) noexcept; + + /** + * Creates a collection from the given @p url. + */ + static Collection fromUrl(const QUrl &url); + + /** + * Sets the unique @p identifier of the collection. + */ + void setId(Id identifier); + + /** + * Returns the unique identifier of the collection. + */ + Q_REQUIRED_RESULT Id id() const; + + /** + * Sets the remote @p id of the collection. + */ + void setRemoteId(const QString &id); + + /** + * Returns the remote id of the collection. + */ + Q_REQUIRED_RESULT QString remoteId() const; + + /** + * Sets the remote @p revision of the collection. + * @param revision the collections's remote revision + * The remote revision can be used by resources to store some + * revision information of the backend to detect changes there. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + void setRemoteRevision(const QString &revision); + + /** + * Returns the remote revision of the collection. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + Q_REQUIRED_RESULT QString remoteRevision() const; + + /** + * Returns whether the collection is valid. + */ + Q_REQUIRED_RESULT bool isValid() const; + + /** + * Returns whether this collections's id equals the + * id of the @p other collection. + */ + Q_REQUIRED_RESULT bool operator==(const Collection &other) const; + + /** + * Returns whether the collection's id does not equal the id + * of the @p other collection. + */ + Q_REQUIRED_RESULT bool operator!=(const Collection &other) const; + + /** + * Assigns the @p other to this collection and returns a reference to this + * collection. + * @param other the collection to assign + */ + Collection &operator=(const Collection &other); + + /** + * @internal For use with containers only. + * + * @since 4.8 + */ + Q_REQUIRED_RESULT bool operator<(const Collection &other) const; + + /** + * Returns the parent collection of this object. + * @note This will of course only return a useful value if it was explicitly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Q_REQUIRED_RESULT Collection parentCollection() const; + + /** + * Returns a reference to the parent collection of this object. + * @note This will of course only return a useful value if it was explicitly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Q_REQUIRED_RESULT Collection &parentCollection(); + + /** + * Set the parent collection of this object. + * @note Calling this method has no immediate effect for the object itself, + * such as being moved to another collection. + * It is mainly relevant to provide a context for RID-based operations + * inside resources. + * @param parent The parent collection. + * @since 4.4 + */ + void setParentCollection(const Collection &parent); + + /** + * Adds an attribute to the collection. + * + * If an attribute of the same type name already exists, it is deleted and + * replaced with the new one. + * + * @param attribute The new attribute. + * + * @note The collection takes the ownership of the attribute. + */ + void addAttribute(Attribute *attribute); + + /** + * Removes and deletes the attribute of the given type @p name. + */ + void removeAttribute(const QByteArray &name); + + /** + * Returns @c true if the collection has an attribute of the given type @p name, + * false otherwise. + */ + bool hasAttribute(const QByteArray &name) const; + + /** + * Returns a list of all attributes of the collection. + * + * @warning Do not modify the attributes returned from this method, + * the change will not be reflected when updating the Collection + * through CollectionModifyJob. + */ + Q_REQUIRED_RESULT Attribute::List attributes() const; + + /** + * Removes and deletes all attributes of the collection. + */ + void clearAttributes(); + + /** + * Returns the attribute of the given type @p name if available, 0 otherwise. + */ + Attribute *attribute(const QByteArray &name); + const Attribute *attribute(const QByteArray &name) const; + + /** + * Describes the options that can be passed to access attributes. + */ + enum CreateOption { + AddIfMissing, ///< Creates the attribute if it is missing + DontCreate ///< Default value + }; + + /** + * Returns the attribute of the requested type. + * If the collection has no attribute of that type yet, passing AddIfMissing + * as an argument will create and add it to the entity + * + * @param option The create options. + */ + template inline T *attribute(CreateOption option = DontCreate); + + /** + * Returns the attribute of the requested type or 0 if it is not available. + */ + template inline const T *attribute() const; + + /** + * Removes and deletes the attribute of the requested type. + */ + template inline void removeAttribute(); + + /** + * Returns whether the collection has an attribute of the requested type. + */ + template inline bool hasAttribute() const; + + /** + * Returns the i18n'ed name of the collection. + */ + Q_REQUIRED_RESULT QString name() const; + + /** + * Returns the display name (EntityDisplayAttribute::displayName()) if set, + * and Collection::name() otherwise. For human-readable strings this is preferred + * over Collection::name(). + * + * @since 4.11 + */ + Q_REQUIRED_RESULT QString displayName() const; + + /** + * Sets the i18n'ed name of the collection. + * + * @param name The new collection name. + */ + void setName(const QString &name); + + /** + * Returns the rights the user has on the collection. + */ + Q_REQUIRED_RESULT Rights rights() const; + + /** + * Sets the @p rights the user has on the collection. + */ + void setRights(Rights rights); + + /** + * Returns a list of possible content mimetypes, + * e.g. message/rfc822, x-akonadi/collection for a mail folder that + * supports sub-folders. + */ + Q_REQUIRED_RESULT QStringList contentMimeTypes() const; + + /** + * Sets the list of possible content mime @p types. + */ + void setContentMimeTypes(const QStringList &types); + + /** + * Returns the root collection. + */ + Q_REQUIRED_RESULT static Collection root(); + + /** + * Returns the mimetype used for collections. + */ + Q_REQUIRED_RESULT static QString mimeType(); + + /** + * Returns the mimetype used for virtual collections + * + * @since 4.11 + */ + Q_REQUIRED_RESULT static QString virtualMimeType(); + + /** + * Returns the identifier of the resource owning the collection. + */ + Q_REQUIRED_RESULT QString resource() const; + + /** + * Sets the @p identifier of the resource owning the collection. + */ + void setResource(const QString &identifier); + + /** + * Returns the cache policy of the collection. + */ + Q_REQUIRED_RESULT CachePolicy cachePolicy() const; + + /** + * Sets the cache @p policy of the collection. + */ + void setCachePolicy(const CachePolicy &policy); + + /** + * Returns the collection statistics of the collection. + */ + Q_REQUIRED_RESULT CollectionStatistics statistics() const; + + /** + * Sets the collection @p statistics for the collection. + */ + void setStatistics(const CollectionStatistics &statistics); + + /** + * Describes the type of url which is returned in url(). + * + * @since 4.7 + */ + enum UrlType { + UrlShort = 0, ///< A short url which contains the identifier only (equivalent to url()) + UrlWithName = 1 ///< A url with identifier and name + }; + + /** + * Returns the url of the collection. + * @param type the type of url + * @since 4.7 + */ + Q_REQUIRED_RESULT QUrl url(UrlType type = UrlShort) const; + + /** + * Returns whether the collection is virtual, for example a search collection. + * + * @since 4.6 + */ + Q_REQUIRED_RESULT bool isVirtual() const; + + /** + * Sets whether the collection is virtual or not. + * Virtual collections can't be converted to non-virtual and vice versa. + * @param isVirtual virtual collection if @c true, otherwise a normal collection + * @since 4.10 + */ + void setVirtual(bool isVirtual); + + /** + * Sets the collection's enabled state. + * + * Use this mechanism to set if a collection should be available + * to the user or not. + * + * This can be used in conjunction with the local list preference for finer grained control + * to define if a collection should be included depending on the purpose. + * + * For example: A collection is by default enabled, meaning it is displayed to the user, synchronized by the resource, + * and indexed by the indexer. A disabled collection on the other hand is not displayed, synchronized or indexed. + * The local list preference allows to locally override that default value for each purpose individually. + * + * The enabled state can be synchronized by backends. + * E.g. an imap resource may synchronize this with the subscription state. + * + * @since 4.14 + * @see setLocalListPreference, setShouldList + */ + void setEnabled(bool enabled); + + /** + * Returns the collection's enabled state. + * @since 4.14 + * @see localListPreference + */ + Q_REQUIRED_RESULT bool enabled() const; + + /** + * Describes the list preference value + * + * @since 4.14 + */ + enum ListPreference { + ListEnabled, ///< Enable collection for specified purpose + ListDisabled, ///< Disable collection for specified purpose + ListDefault ///< Fallback to enabled state + }; + + /** + * Describes the purpose of the listing + * + * @since 4.14 + */ + enum ListPurpose { + ListSync, ///< Listing for synchronization + ListDisplay, ///< Listing for display to the user + ListIndex ///< Listing for indexing the content + }; + + /** + * Sets the local list preference for the specified purpose. + * + * The local list preference overrides the enabled state unless set to ListDefault. + * In case of ListDefault the enabled state should be taken as fallback (shouldList() implements this logic). + * + * The default value is ListDefault. + * + * @since 4.14 + * @see shouldList, setEnabled + */ + void setLocalListPreference(ListPurpose purpose, ListPreference preference); + + /** + * Returns the local list preference for the specified purpose. + * @since 4.14 + * @see setLocalListPreference + */ + Q_REQUIRED_RESULT ListPreference localListPreference(ListPurpose purpose) const; + + /** + * Returns whether the collection should be listed or not for the specified purpose + * Takes enabled state and local preference into account. + * + * @since 4.14 + * @see setLocalListPreference, setEnabled + */ + Q_REQUIRED_RESULT bool shouldList(ListPurpose purpose) const; + + /** + * Sets whether the collection should be listed or not for the specified purpose. + * Takes enabled state and local preference into account. + * + * Use this instead of sestEnabled and setLocalListPreference to automatically set + * the right setting. + * + * @since 4.14 + * @see setLocalListPreference, setEnabled + */ + void setShouldList(ListPurpose purpose, bool shouldList); + + /** + * Set during sync to indicate that the provided parts are only default values; + * @since 4.15 + */ + void setKeepLocalChanges(const QSet &parts); + + /** + * Returns what parts are only default values. + */ + QSet keepLocalChanges() const; + +private: + friend class CollectionCreateJob; + friend class CollectionFetchJob; + friend class CollectionModifyJob; + friend class ProtocolHelper; + + void markAttributeModified(const QByteArray &type); + + /// @cond PRIVATE + QSharedDataPointer d_ptr; + friend class CollectionPrivate; + /// @endcond +}; + +AKONADICORE_EXPORT uint qHash(const Akonadi::Collection &collection); + +template inline T *Akonadi::Collection::attribute(Collection::CreateOption option) +{ + const QByteArray type = T().type(); + markAttributeModified(type); // do this first in case it detaches + if (hasAttribute(type)) { + if (T *attr = dynamic_cast(attribute(type))) { + return attr; + } + qWarning() << "Found attribute of unknown type" << type << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } else if (option == AddIfMissing) { + T *attr = new T(); + addAttribute(attr); + return attr; + } + + return nullptr; +} + +template inline const T *Akonadi::Collection::attribute() const +{ + const QByteArray type = T().type(); + if (hasAttribute(type)) { + if (const T *attr = dynamic_cast(attribute(type))) { + return attr; + } + qWarning() << "Found attribute of unknown type" << type << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + return nullptr; +} + +template inline void Akonadi::Collection::removeAttribute() +{ + removeAttribute(T().type()); +} + +template inline bool Akonadi::Collection::hasAttribute() const +{ + return hasAttribute(T().type()); +} + +} // namespace Akonadi + +/** + * Allows to output a collection for debugging purposes. + */ +AKONADICORE_EXPORT QDebug operator<<(QDebug d, const Akonadi::Collection &collection); + +Q_DECLARE_METATYPE(Akonadi::Collection) +Q_DECLARE_METATYPE(Akonadi::Collection::List) +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::Collection::Rights) +Q_DECLARE_TYPEINFO(Akonadi::Collection, Q_MOVABLE_TYPE); + diff --git a/src/core/collection_p.h b/src/core/collection_p.h new file mode 100644 index 0000000..f03b8fc --- /dev/null +++ b/src/core/collection_p.h @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "attributestorage_p.h" +#include "cachepolicy.h" +#include "collection.h" +#include "collectionstatistics.h" + +#include + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Akonadi::CollectionPrivate : public QSharedData +{ +public: + CollectionPrivate(Collection::Id id = -1) + : QSharedData() + , displayPreference(Collection::ListDefault) + , syncPreference(Collection::ListDefault) + , indexPreference(Collection::ListDefault) + , listPreferenceChanged(false) + , enabled(true) + , enabledChanged(false) + , contentTypesChanged(false) + , cachePolicyChanged(false) + , isVirtual(false) + , mId(id) + { + } + + CollectionPrivate(const CollectionPrivate &other) + : QSharedData(other) + { + mId = other.mId; + mRemoteId = other.mRemoteId; + mRemoteRevision = other.mRemoteRevision; + mAttributeStorage = other.mAttributeStorage; + if (other.mParent) { + mParent.reset(new Collection(*(other.mParent))); + } + name = other.name; + resource = other.resource; + statistics = other.statistics; + contentTypes = other.contentTypes; + cachePolicy = other.cachePolicy; + contentTypesChanged = other.contentTypesChanged; + cachePolicyChanged = other.cachePolicyChanged; + isVirtual = other.isVirtual; + enabled = other.enabled; + enabledChanged = other.enabledChanged; + displayPreference = other.displayPreference; + syncPreference = other.syncPreference; + indexPreference = other.indexPreference; + listPreferenceChanged = other.listPreferenceChanged; + keepLocalChanges = other.keepLocalChanges; + } + + void resetChangeLog() + { + contentTypesChanged = false; + cachePolicyChanged = false; + enabledChanged = false; + listPreferenceChanged = false; + mAttributeStorage.resetChangeLog(); + } + + static Collection newRoot() + { + Collection rootCollection(0); + rootCollection.setContentMimeTypes({Collection::mimeType()}); + return rootCollection; + } + + // Make use of the 4-bytes padding from QSharedData + Collection::ListPreference displayPreference : 2; + Collection::ListPreference syncPreference : 2; + Collection::ListPreference indexPreference : 2; + bool listPreferenceChanged : 1; + bool enabled : 1; + bool enabledChanged : 1; + bool contentTypesChanged : 1; + bool cachePolicyChanged : 1; + bool isVirtual : 1; + // 2 bytes padding here + + Collection::Id mId; + QString mRemoteId; + QString mRemoteRevision; + mutable QScopedPointer mParent; + AttributeStorage mAttributeStorage; + QString name; + QString resource; + CollectionStatistics statistics; + QStringList contentTypes; + static const Collection root; + CachePolicy cachePolicy; + QSet keepLocalChanges; +}; + diff --git a/src/core/collectionfetchscope.cpp b/src/core/collectionfetchscope.cpp new file mode 100644 index 0000000..e5bb5a7 --- /dev/null +++ b/src/core/collectionfetchscope.cpp @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2008 Kevin Krammer + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionfetchscope.h" + +#include +#include + +namespace Akonadi +{ +class CollectionFetchScopePrivate : public QSharedData +{ +public: + CollectionFetchScopePrivate() + : ancestorDepth(CollectionFetchScope::None) + , listFilter(CollectionFetchScope::Enabled) + { + } + + CollectionFetchScopePrivate(const CollectionFetchScopePrivate &other) + : QSharedData(other) + { + resource = other.resource; + contentMimeTypes = other.contentMimeTypes; + ancestorDepth = other.ancestorDepth; + statistics = other.statistics; + listFilter = other.listFilter; + attributes = other.attributes; + if (!ancestorFetchScope && other.ancestorFetchScope) { + ancestorFetchScope.reset(new CollectionFetchScope()); + *ancestorFetchScope = *other.ancestorFetchScope; + } else if (ancestorFetchScope && !other.ancestorFetchScope) { + ancestorFetchScope.reset(nullptr); + } + fetchIdOnly = other.fetchIdOnly; + mIgnoreRetrievalErrors = other.mIgnoreRetrievalErrors; + } + +public: + QString resource; + QStringList contentMimeTypes; + CollectionFetchScope::AncestorRetrieval ancestorDepth; + CollectionFetchScope::ListFilter listFilter; + QSet attributes; + QScopedPointer ancestorFetchScope; + bool statistics = false; + bool fetchIdOnly = true; + bool mIgnoreRetrievalErrors = false; +}; + +CollectionFetchScope::CollectionFetchScope() + : d(new CollectionFetchScopePrivate()) +{ +} + +CollectionFetchScope::CollectionFetchScope(const CollectionFetchScope &other) + : d(other.d) +{ +} + +CollectionFetchScope::~CollectionFetchScope() +{ +} + +CollectionFetchScope &CollectionFetchScope::operator=(const CollectionFetchScope &other) +{ + if (&other != this) { + d = other.d; + } + + return *this; +} + +bool CollectionFetchScope::isEmpty() const +{ + return d->resource.isEmpty() && d->contentMimeTypes.isEmpty() && !d->statistics && d->ancestorDepth == None && d->listFilter == Enabled; +} + +bool CollectionFetchScope::includeStatistics() const +{ + return d->statistics; +} + +void CollectionFetchScope::setIncludeStatistics(bool include) +{ + d->statistics = include; +} + +QString CollectionFetchScope::resource() const +{ + return d->resource; +} + +void CollectionFetchScope::setResource(const QString &resource) +{ + d->resource = resource; +} + +QStringList CollectionFetchScope::contentMimeTypes() const +{ + return d->contentMimeTypes; +} + +void CollectionFetchScope::setContentMimeTypes(const QStringList &mimeTypes) +{ + d->contentMimeTypes = mimeTypes; +} + +CollectionFetchScope::AncestorRetrieval CollectionFetchScope::ancestorRetrieval() const +{ + return d->ancestorDepth; +} + +void CollectionFetchScope::setAncestorRetrieval(AncestorRetrieval ancestorDepth) +{ + d->ancestorDepth = ancestorDepth; +} + +CollectionFetchScope::ListFilter CollectionFetchScope::listFilter() const +{ + return d->listFilter; +} + +void CollectionFetchScope::setListFilter(CollectionFetchScope::ListFilter listFilter) +{ + d->listFilter = listFilter; +} + +QSet CollectionFetchScope::attributes() const +{ + return d->attributes; +} + +void CollectionFetchScope::fetchAttribute(const QByteArray &type, bool fetch) +{ + d->fetchIdOnly = false; + if (fetch) { + d->attributes.insert(type); + } else { + d->attributes.remove(type); + } +} + +void CollectionFetchScope::setFetchIdOnly(bool fetchIdOnly) +{ + d->fetchIdOnly = fetchIdOnly; +} + +bool CollectionFetchScope::fetchIdOnly() const +{ + return d->fetchIdOnly; +} + +void CollectionFetchScope::setIgnoreRetrievalErrors(bool enable) +{ + d->mIgnoreRetrievalErrors = enable; +} + +bool CollectionFetchScope::ignoreRetrievalErrors() const +{ + return d->mIgnoreRetrievalErrors; +} + +void CollectionFetchScope::setAncestorFetchScope(const CollectionFetchScope &scope) +{ + *d->ancestorFetchScope = scope; +} + +CollectionFetchScope CollectionFetchScope::ancestorFetchScope() const +{ + if (!d->ancestorFetchScope) { + return CollectionFetchScope(); + } + return *d->ancestorFetchScope; +} + +CollectionFetchScope &CollectionFetchScope::ancestorFetchScope() +{ + if (!d->ancestorFetchScope) { + d->ancestorFetchScope.reset(new CollectionFetchScope()); + } + return *d->ancestorFetchScope; +} + +} // namespace Akonadi diff --git a/src/core/collectionfetchscope.h b/src/core/collectionfetchscope.h new file mode 100644 index 0000000..7786b73 --- /dev/null +++ b/src/core/collectionfetchscope.h @@ -0,0 +1,271 @@ +/* + SPDX-FileCopyrightText: 2008 Kevin Krammer + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +#include + +namespace Akonadi +{ +class CollectionFetchScopePrivate; + +/** + * @short Specifies which parts of a collection should be fetched from the Akonadi storage. + * + * When collections are fetched from the server either by using CollectionFetchJob + * explicitly or when it is being used internally by other classes, e.g. Akonadi::Monitor, + * the scope of the fetch operation can be tailored to the application's current needs. + * + * Note that CollectionFetchScope always includes fetching collection attributes. + * + * There are two supported ways of changing the currently active CollectionFetchScope + * of classes: + * - in-place: modify the CollectionFetchScope object the other class holds as a member + * - replace: replace the other class' member with a new scope object + * + * Example: modifying a CollectionFetchJob's scope @c in-place + * @code + * Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob( collection ); + * job->fetchScope().setIncludeUnsubscribed( true ); + * @endcode + * + * Example: @c replacing a CollectionFetchJob's scope + * @code + * Akonadi::CollectionFetchScope scope; + * scope.setIncludeUnsubscribed( true ); + * + * Akonadi::CollectionFetchJob *job = new Akonadi::CollectionFetchJob( collection ); + * job->setFetchScope( scope ); + * @endcode + * + * This class is implicitly shared. + * + * @author Volker Krause + * @since 4.4 + */ +class AKONADICORE_EXPORT CollectionFetchScope +{ +public: + /** + * Describes the ancestor retrieval depth. + */ + enum AncestorRetrieval { + None, ///< No ancestor retrieval at all (the default) + Parent, ///< Only retrieve the immediate parent collection + All ///< Retrieve all ancestors, up to Collection::root() + }; + + /** + * Creates an empty collection fetch scope. + * + * Using an empty scope will only fetch the very basic meta data of collections, + * e.g. local id, remote id and content mimetypes. + */ + CollectionFetchScope(); + + /** + * Creates a new collection fetch scope from an @p other. + */ + CollectionFetchScope(const CollectionFetchScope &other); + + /** + * Destroys the collection fetch scope. + */ + ~CollectionFetchScope(); + + /** + * Assigns the @p other to this scope and returns a reference to this scope. + */ + CollectionFetchScope &operator=(const CollectionFetchScope &other); + + /** + * Describes the list filter + * + * @since 4.14 + */ + enum ListFilter { + NoFilter, ///< No filtering, retrieve all collections + Display, ///< Only retrieve collections for display, taking the local preference and enabled into account. + Sync, ///< Only retrieve collections for synchronization, taking the local preference and enabled into account. + Index, ///< Only retrieve collections for indexing, taking the local preference and enabled into account. + Enabled ///< Only retrieve enabled collections, ignoring the local preference. + }; + + /** + * Sets a filter for the collections to be listed. + * + * Note that collections that do not match the filter are included if required to complete the tree. + * + * @since 4.14 + */ + void setListFilter(ListFilter); + + /** + * Returns the list filter. + * + * @see setListFilter() + * @since 4.14 + */ + Q_REQUIRED_RESULT ListFilter listFilter() const; + + /** + * Returns whether collection statistics should be included in the retrieved results. + * + * @see setIncludeStatistics() + */ + Q_REQUIRED_RESULT bool includeStatistics() const; + + /** + * Sets whether collection statistics should be included in the retrieved results. + * + * @param include @c true to include collection statistics, @c false otherwise (the default). + */ + void setIncludeStatistics(bool include); + + /** + * Returns the resource identifier that is used as filter. + * + * @see setResource() + */ + Q_REQUIRED_RESULT QString resource() const; + + /** + * Sets a resource filter, that is only collections owned by the specified resource are + * retrieved. + * + * @param resource The resource identifier. + */ + void setResource(const QString &resource); + + /** + * Sets a content mimetypes filter, that is only collections that contain at least one of the + * given mimetypes (or their parents) are retrieved. + * + * @param mimeTypes A list of mime types + */ + void setContentMimeTypes(const QStringList &mimeTypes); + + /** + * Returns the content mimetypes filter. + * + * @see setContentMimeTypes() + */ + Q_REQUIRED_RESULT QStringList contentMimeTypes() const; + + /** + * Sets how many levels of ancestor collections should be included in the retrieval. + * + * Only the ID and the remote ID of the ancestor collections are fetched. If + * you want more information about the ancestor collections, like their name, + * you will need to do an additional CollectionFetchJob for them. + * + * @param ancestorDepth The desired ancestor retrieval depth. + */ + void setAncestorRetrieval(AncestorRetrieval ancestorDepth); + + /** + * Returns the ancestor retrieval depth. + * + * @see setAncestorRetrieval() + */ + Q_REQUIRED_RESULT AncestorRetrieval ancestorRetrieval() const; + + /** + * Sets the fetch scope for ancestor retrieval. + * + * @see setAncestorRetrieval() + */ + void setAncestorFetchScope(const CollectionFetchScope &scope); + + /** + * Returns the fetch scope for ancestor retrieval. + */ + Q_REQUIRED_RESULT CollectionFetchScope ancestorFetchScope() const; + + /** + * Returns the fetch scope for ancestor retrieval. + */ + CollectionFetchScope &ancestorFetchScope(); + + /** + * Returns all explicitly fetched attributes. + * + * @see fetchAttribute() + */ + Q_REQUIRED_RESULT QSet attributes() const; + + /** + * Sets whether the attribute of the given @p type should be fetched. + * + * @param type The attribute type to fetch. + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + void fetchAttribute(const QByteArray &type, bool fetch = true); + + /** + * Sets whether the attribute of the requested type should be fetched. + * + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + template inline void fetchAttribute(bool fetch = true) + { + T dummy; + fetchAttribute(dummy.type(), fetch); + } + + /** + * Sets whether only the id or the complete tag should be fetched. + * + * The default is @c false. + * + * @since 4.15 + */ + void setFetchIdOnly(bool fetchIdOnly); + + /** + * Sets whether only the id of the tags should be retieved or the complete tag. + * + * @see tagFetchScope() + * @since 4.15 + */ + Q_REQUIRED_RESULT bool fetchIdOnly() const; + + /** + * Ignore retrieval errors while fetching collections, and always deliver what is available. + * + * This flag is useful to fetch a list of collections, where some might no longer be available. + * + * @since KF5 + */ + void setIgnoreRetrievalErrors(bool enabled); + + /** + * Returns whether retrieval errors should be ignored. + * + * @see setIgnoreRetrievalErrors() + * @since KF5 + */ + Q_REQUIRED_RESULT bool ignoreRetrievalErrors() const; + + /** + * Returns @c true if there is nothing to fetch. + */ + Q_REQUIRED_RESULT bool isEmpty() const; + +private: + /// @cond PRIVATE + QSharedDataPointer d; + /// @endcond +}; + +} + diff --git a/src/core/collectionpathresolver.cpp b/src/core/collectionpathresolver.cpp new file mode 100644 index 0000000..39249ae --- /dev/null +++ b/src/core/collectionpathresolver.cpp @@ -0,0 +1,220 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionpathresolver.h" + +#include "collectionfetchjob.h" +#include "job_p.h" + +#include "akonadicore_debug.h" + +#include + +#include + +using namespace Akonadi; + +/// @cond PRIVATE + +class Akonadi::CollectionPathResolverPrivate : public JobPrivate +{ +public: + explicit CollectionPathResolverPrivate(CollectionPathResolver *parent) + : JobPrivate(parent) + , mColId(-1) + { + } + + void init(const QString &path, const Collection &rootCollection) + { + Q_Q(CollectionPathResolver); + + mPathToId = true; + mPath = path; + if (mPath.startsWith(q->pathDelimiter())) { + mPath = mPath.right(mPath.length() - q->pathDelimiter().length()); + } + if (mPath.endsWith(q->pathDelimiter())) { + mPath = mPath.left(mPath.length() - q->pathDelimiter().length()); + } + + mPathParts = splitPath(mPath); + mCurrentNode = rootCollection; + } + + void jobResult(KJob *job); + + QStringList splitPath(const QString &path) + { + if (path.isEmpty()) { // path is normalized, so non-empty means at least one hit + return QStringList(); + } + + QStringList rv; + int begin = 0; + const int pathSize(path.size()); + for (int i = 0; i < pathSize; ++i) { + if (path[i] == QLatin1Char('/')) { + QString pathElement = path.mid(begin, i - begin); + pathElement.replace(QLatin1String("\\/"), QLatin1String("/")); + rv.append(pathElement); + begin = i + 1; + } + if (i < path.size() - 2 && path[i] == QLatin1Char('\\') && path[i + 1] == QLatin1Char('/')) { + ++i; + } + } + QString pathElement = path.mid(begin); + pathElement.replace(QLatin1String("\\/"), QLatin1String("/")); + rv.append(pathElement); + return rv; + } + + Q_DECLARE_PUBLIC(CollectionPathResolver) + + Collection mCurrentNode; + QStringList mPathParts; + QString mPath; + Collection::Id mColId; + bool mPathToId = false; +}; + +void CollectionPathResolverPrivate::jobResult(KJob *job) +{ + if (job->error()) { + return; + } + + Q_Q(CollectionPathResolver); + + auto list = static_cast(job); + CollectionFetchJob *nextJob = nullptr; + const Collection::List cols = list->collections(); + if (cols.isEmpty()) { + mColId = -1; + q->setError(CollectionPathResolver::Unknown); + q->setErrorText(i18n("No such collection.")); + q->emitResult(); + return; + } + + if (mPathToId) { + const QString currentPart = mPathParts.takeFirst(); + bool found = false; + for (const Collection &c : cols) { + if (c.name() == currentPart) { + mCurrentNode = c; + found = true; + break; + } + } + if (!found) { + qCWarning(AKONADICORE_LOG) << "No such collection" << currentPart << "with parent" << mCurrentNode.id(); + mColId = -1; + q->setError(CollectionPathResolver::Unknown); + q->setErrorText(i18n("No such collection.")); + q->emitResult(); + return; + } + if (mPathParts.isEmpty()) { + mColId = mCurrentNode.id(); + q->emitResult(); + return; + } + nextJob = new CollectionFetchJob(mCurrentNode, CollectionFetchJob::FirstLevel, q); + } else { + Collection col = list->collections().at(0); + mCurrentNode = col.parentCollection(); + mPathParts.prepend(col.name()); + if (mCurrentNode == Collection::root()) { + q->emitResult(); + return; + } + nextJob = new CollectionFetchJob(mCurrentNode, CollectionFetchJob::Base, q); + } + q->connect(nextJob, &CollectionFetchJob::result, q, [this](KJob *job) { + jobResult(job); + }); +} + +CollectionPathResolver::CollectionPathResolver(const QString &path, QObject *parent) + : Job(new CollectionPathResolverPrivate(this), parent) +{ + Q_D(CollectionPathResolver); + d->init(path, Collection::root()); +} + +CollectionPathResolver::CollectionPathResolver(const QString &path, const Collection &parentCollection, QObject *parent) + : Job(new CollectionPathResolverPrivate(this), parent) +{ + Q_D(CollectionPathResolver); + d->init(path, parentCollection); +} + +CollectionPathResolver::CollectionPathResolver(const Collection &collection, QObject *parent) + : Job(new CollectionPathResolverPrivate(this), parent) +{ + Q_D(CollectionPathResolver); + + d->mPathToId = false; + d->mColId = collection.id(); + d->mCurrentNode = collection; +} + +CollectionPathResolver::~CollectionPathResolver() +{ +} + +Collection::Id CollectionPathResolver::collection() const +{ + Q_D(const CollectionPathResolver); + + return d->mColId; +} + +QString CollectionPathResolver::path() const +{ + Q_D(const CollectionPathResolver); + + if (d->mPathToId) { + return d->mPath; + } + return d->mPathParts.join(pathDelimiter()); +} + +QString CollectionPathResolver::pathDelimiter() +{ + return QStringLiteral("/"); +} + +void CollectionPathResolver::doStart() +{ + Q_D(CollectionPathResolver); + + CollectionFetchJob *job = nullptr; + if (d->mPathToId) { + if (d->mPath.isEmpty()) { + d->mColId = Collection::root().id(); + emitResult(); + return; + } + job = new CollectionFetchJob(d->mCurrentNode, CollectionFetchJob::FirstLevel, this); + } else { + if (d->mColId == 0) { + d->mColId = Collection::root().id(); + emitResult(); + return; + } + job = new CollectionFetchJob(d->mCurrentNode, CollectionFetchJob::Base, this); + } + connect(job, &CollectionFetchJob::result, this, [d](KJob *job) { + d->jobResult(job); + }); +} + +/// @endcond + +#include "moc_collectionpathresolver.cpp" diff --git a/src/core/collectionpathresolver.h b/src/core/collectionpathresolver.h new file mode 100644 index 0000000..d5af23d --- /dev/null +++ b/src/core/collectionpathresolver.h @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ +class CollectionPathResolverPrivate; + +/** + * @internal + * + * Converts between collection id and collection path. + * + * While it is generally recommended to use collection ids, it can + * be necessary in some cases (eg. a command line client) to use the + * collection path instead. Use this class to get a collection id + * from a collection path. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionPathResolver : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection path resolver to convert a path into a id. + * + * Equivalent to calling CollectionPathResolver(path, Collection:root(), parent) + * + * @param path The collection path. + * @param parent The parent object. + */ + explicit CollectionPathResolver(const QString &path, QObject *parent = nullptr); + + /** + * Create a new collection path resolver to convert a path into an id. + * + * The @p path is resolved relatively to @p parentCollection. This can be + * useful for resource, which now the root collection. + * + * @param path The collection path. + * @param parentCollection Collection relatively to which the path will be resolved. + * @param parent The parent object. + * + * @since 4.14 + */ + explicit CollectionPathResolver(const QString &path, const Collection &parentCollection, QObject *parent = nullptr); + + /** + * Creates a new collection path resolver to determine the path of + * the given collection. + * + * @param collection The collection. + * @param parent The parent object. + */ + explicit CollectionPathResolver(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the collection path resolver. + */ + ~CollectionPathResolver() override; + + /** + * Returns the collection id. Only valid after the job succeeded. + */ + Q_REQUIRED_RESULT Collection::Id collection() const; + + /** + * Returns the collection path. Only valid after the job succeeded. + */ + Q_REQUIRED_RESULT QString path() const; + + /** + * Returns the path delimiter for collections. + */ + Q_REQUIRED_RESULT static QString pathDelimiter(); + +protected: + void doStart() override; + +private: + Q_DECLARE_PRIVATE(CollectionPathResolver) +}; + +} + diff --git a/src/core/collectionstatistics.cpp b/src/core/collectionstatistics.cpp new file mode 100644 index 0000000..845efc3 --- /dev/null +++ b/src/core/collectionstatistics.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionstatistics.h" + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionStatistics::Private : public QSharedData +{ +public: + qint64 count = -1; + qint64 unreadCount = -1; + qint64 size = -1; +}; + +CollectionStatistics::CollectionStatistics() + : d(new Private) +{ +} + +CollectionStatistics::CollectionStatistics(const CollectionStatistics &other) + : d(other.d) +{ +} + +CollectionStatistics::~CollectionStatistics() = default; + +qint64 CollectionStatistics::count() const +{ + return d->count; +} + +void CollectionStatistics::setCount(qint64 count) +{ + d->count = count; +} + +qint64 CollectionStatistics::unreadCount() const +{ + return d->unreadCount; +} + +void CollectionStatistics::setUnreadCount(qint64 count) +{ + d->unreadCount = count; +} + +qint64 CollectionStatistics::size() const +{ + return d->size; +} + +void CollectionStatistics::setSize(qint64 size) +{ + d->size = size; +} + +CollectionStatistics &CollectionStatistics::operator=(const CollectionStatistics &other) +{ + d = other.d; + return *this; +} + +QDebug operator<<(QDebug d, const CollectionStatistics &s) +{ + return d << "CollectionStatistics:\n" + << " count:" << s.count() << '\n' + << " unread count:" << s.unreadCount() << '\n' + << " size:" << s.size(); +} diff --git a/src/core/collectionstatistics.h b/src/core/collectionstatistics.h new file mode 100644 index 0000000..1ec5e74 --- /dev/null +++ b/src/core/collectionstatistics.h @@ -0,0 +1,146 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ +/** + * @short Provides statistics information of a Collection. + * + * This class contains information such as total number of items, + * number of new and unread items, etc. + * + * This information might be expensive to obtain and is thus + * not included when fetching collections with a CollectionFetchJob. + * It can be retrieved separately using CollectionStatisticsJob. + * + * Example: + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionStatisticsJob *job = new Akonadi::CollectionStatisticsJob( collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * CollectionStatisticsJob *statisticsJob = qobject_cast( job ); + * + * const Akonadi::CollectionStatistics statistics = statisticsJob->statistics(); + * qDebug() << "Unread items:" << statistics.unreadCount(); + * } + * + * @endcode + * + * This class is implicitly shared. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionStatistics +{ +public: + /** + * Creates a new collection statistics object. + */ + CollectionStatistics(); + + /** + * Creates a collection statistics object from an @p other one. + */ + CollectionStatistics(const CollectionStatistics &other); + + /** + * Destroys the collection statistics object. + */ + ~CollectionStatistics(); + + /** + * Returns the number of items in this collection or @c -1 if + * this information is not available. + * + * @see setCount() + * @see unreadCount() + */ + Q_REQUIRED_RESULT qint64 count() const; + + /** + * Sets the number of items in this collection. + * + * @param count The number of items. + * @see count() + */ + void setCount(qint64 count); + + /** + * Returns the number of unread items in this collection or @c -1 if + * this information is not available. + * + * @see setUnreadCount() + * @see count() + */ + Q_REQUIRED_RESULT qint64 unreadCount() const; + + /** + * Sets the number of unread items in this collection. + * + * @param count The number of unread messages. + * @see unreadCount() + */ + void setUnreadCount(qint64 count); + + /** + * Returns the total size of the items in this collection or @c -1 if + * this information is not available. + * + * @see setSize() + * @since 4.3 + */ + Q_REQUIRED_RESULT qint64 size() const; + + /** + * Sets the total size of the items in this collection. + * + * @param size The total size of the items + * @see size() + * @since 4.3 + */ + void setSize(qint64 size); + + /** + * Assigns @p other to this statistics object and returns a reference to this one. + */ + CollectionStatistics &operator=(const CollectionStatistics &other); + +private: + /// @cond PRIVATE + class Private; + QSharedDataPointer d; + /// @endcond +}; + +} + +/** + * Allows to output the collection statistics for debugging purposes. + */ +AKONADICORE_EXPORT QDebug operator<<(QDebug d, const Akonadi::CollectionStatistics &); + +Q_DECLARE_METATYPE(Akonadi::CollectionStatistics) + diff --git a/src/core/collectionsync.cpp b/src/core/collectionsync.cpp new file mode 100644 index 0000000..ec2a823 --- /dev/null +++ b/src/core/collectionsync.cpp @@ -0,0 +1,859 @@ +/* + SPDX-FileCopyrightText: 2007, 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "collection.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "collectionmovejob.h" +#include "collectionsync_p.h" + +#include "cachepolicy.h" + +#include +#include +#include + +#include + +using namespace Akonadi; + +static const char CONTENTMIMETYPES[] = "CONTENTMIMETYPES"; + +static const char ROOTPARENTRID[] = "AKONADI_ROOT_COLLECTION"; + +class RemoteId +{ +public: + explicit RemoteId() + { + } + + explicit inline RemoteId(const QStringList &ridChain) + : ridChain(ridChain) + { + } + + explicit inline RemoteId(const QString &rid) + { + ridChain.append(rid); + } + + inline bool isAbsolute() const + { + return ridChain.last() == QString::fromLatin1(ROOTPARENTRID); + } + + inline bool isEmpty() const + { + return ridChain.isEmpty(); + } + + inline bool operator==(const RemoteId &other) const + { + return ridChain == other.ridChain; + } + + QStringList ridChain; + + static RemoteId rootRid; +}; + +RemoteId RemoteId::rootRid = RemoteId(QStringList() << QString::fromLatin1(ROOTPARENTRID)); + +Q_DECLARE_METATYPE(RemoteId) + +uint qHash(const RemoteId &rid) +{ + uint hash = 0; + for (QStringList::ConstIterator iter = rid.ridChain.constBegin(), end = rid.ridChain.constEnd(); iter != end; ++iter) { + hash += qHash(*iter); + } + return hash; +} + +inline bool operator<(const RemoteId &r1, const RemoteId &r2) +{ + if (r1.ridChain.length() == r2.ridChain.length()) { + auto it1 = r1.ridChain.constBegin(); + auto end1 = r1.ridChain.constEnd(); + auto it2 = r2.ridChain.constBegin(); + while (it1 != end1) { + if ((*it1) == (*it2)) { + ++it1; + ++it2; + continue; + } + return (*it1) < (*it2); + } + } else { + return r1.ridChain.length() < r2.ridChain.length(); + } + return false; +} + +QDebug operator<<(QDebug s, const RemoteId &rid) +{ + s.nospace() << "RemoteId(" << rid.ridChain << ")"; + return s; +} + +/** + * @internal + */ +class CollectionSync::Private +{ +public: + explicit Private(CollectionSync *parent) + : q(parent) + , pendingJobs(0) + , progress(0) + , currentTransaction(nullptr) + , incremental(false) + , streaming(false) + , hierarchicalRIDs(false) + , localListDone(false) + , deliveryDone(false) + , akonadiRootCollection(Collection::root()) + , resultEmitted(false) + { + } + + ~Private() + { + } + + RemoteId remoteIdForCollection(const Collection &collection) const + { + if (collection == Collection::root()) { + return RemoteId::rootRid; + } + + if (!hierarchicalRIDs) { + return RemoteId(collection.remoteId()); + } + + RemoteId rid; + Collection parent = collection; + while (parent.isValid() || !parent.remoteId().isEmpty()) { + QString prid = parent.remoteId(); + if (prid.isEmpty() && parent.isValid()) { + prid = uidRidMap.value(parent.id()); + } + if (prid.isEmpty()) { + break; + } + rid.ridChain.append(prid); + parent = parent.parentCollection(); + if (parent == akonadiRootCollection) { + rid.ridChain.append(QString::fromLatin1(ROOTPARENTRID)); + break; + } + } + return rid; + } + + void addRemoteColection(const Collection &collection, bool removed = false) + { + QHash &map = (removed ? removedRemoteCollections : remoteCollections); + const Collection parentCollection = collection.parentCollection(); + if (parentCollection.remoteId() == akonadiRootCollection.remoteId() || parentCollection.id() == akonadiRootCollection.id()) { + Collection c2(collection); + c2.setParentCollection(akonadiRootCollection); + map[RemoteId::rootRid].append(c2); + } else { + Q_ASSERT(!parentCollection.remoteId().isEmpty()); + map[remoteIdForCollection(parentCollection)].append(collection); + } + } + + /* Compares collections by remoteId and falls back to name comparison in case + * local collection does not have remoteId (which can happen in some cases) + */ + bool matchLocalAndRemoteCollection(const Collection &local, const Collection &remote) + { + if (!local.remoteId().isEmpty()) { + return local.remoteId() == remote.remoteId(); + } else { + return local.name() == remote.name(); + } + } + + void localCollectionsReceived(const Akonadi::Collection::List &localCols) + { + for (const Akonadi::Collection &collection : localCols) { + const RemoteId parentRid = remoteIdForCollection(collection.parentCollection()); + localCollections[parentRid] += collection; + } + } + + void processCollections(const RemoteId &parentRid) + { + Collection::List remoteChildren = remoteCollections.value(parentRid); + Collection::List removedChildren = removedRemoteCollections.value(parentRid); + Collection::List localChildren = localCollections.value(parentRid); + + // Iterate over the list of local children of localParent + for (auto localIter = localChildren.begin(), localEnd = localChildren.end(); localIter != localEnd;) { + const Collection localCollection = *localIter; + bool matched = false; + uidRidMap.insert(localIter->id(), localIter->remoteId()); + + // Try to map removed remote collections (from incremental sync) to local collections + for (auto removedIter = removedChildren.begin(), removedEnd = removedChildren.end(); removedIter != removedEnd;) { + Collection removedCollection = *removedIter; + + if (matchLocalAndRemoteCollection(localCollection, removedCollection)) { + matched = true; + if (!localCollection.remoteId().isEmpty()) { + localCollectionsToRemove.append(localCollection); + } + // Remove the matched removed collection from the list so that + // we don't have to iterate over it again next time. + removedIter = removedChildren.erase(removedIter); + removedEnd = removedChildren.end(); + break; + } else { + // Keep looking + ++removedIter; + } + } + + if (matched) { + // Remove the matched local collection from the list, because we + // have already put it into localCollectionsToRemove + localIter = localChildren.erase(localIter); + localEnd = localChildren.end(); + continue; + } + + // Try to find a matching collection in the list of remote children + for (auto remoteIter = remoteChildren.begin(), remoteEnd = remoteChildren.end(); !matched && remoteIter != remoteEnd;) { + Collection remoteCollection = *remoteIter; + + // Yay, we found a match! + if (matchLocalAndRemoteCollection(localCollection, remoteCollection)) { + matched = true; + + // "Virtual" flag cannot be updated: we need to recreate + // the collection from scratch. + if (localCollection.isVirtual() != remoteCollection.isVirtual()) { + // Mark the local collection and all its children for deletion and re-creation + QList> parents = {{localCollection, remoteCollection}}; + while (!parents.empty()) { + auto parent = parents.takeFirst(); + qCDebug(AKONADICORE_LOG) << "Local collection " << parent.first.name() << " will be recreated"; + localCollectionsToRemove.push_back(parent.first); + remoteCollectionsToCreate.push_back(parent.second); + for (auto it = localChildren.begin(), end = localChildren.end(); it != end;) { + if (it->parentCollection() == parent.first) { + Collection remoteParent; + auto remoteIt = std::find_if( + remoteChildren.begin(), + remoteChildren.end(), + std::bind(&CollectionSync::Private::matchLocalAndRemoteCollection, this, parent.first, std::placeholders::_1)); + if (remoteIt != remoteChildren.end()) { + remoteParent = *remoteIt; + remoteEnd = remoteChildren.erase(remoteIt); + } + parents.push_back({*it, remoteParent}); + it = localChildren.erase(it); + localEnd = end = localChildren.end(); + } else { + ++it; + } + } + } + } else if (collectionNeedsUpdate(localCollection, remoteCollection)) { + // We need to store both local and remote collections, so that + // we can copy over attributes to be preserved + remoteCollectionsToUpdate.append(qMakePair(localCollection, remoteCollection)); + } else { + // Collections are the same, no need to update anything + } + + // Remove the matched remote collection from the list so that + // in the end we are left with list of collections that don't + // exist locally (i.e. new collections) + remoteIter = remoteChildren.erase(remoteIter); + remoteEnd = remoteChildren.end(); + break; + } else { + // Keep looking + ++remoteIter; + } + } + + if (matched) { + // Remove the matched local collection from the list so that + // in the end we are left with list of collections that don't + // exist remotely (i.e. removed collections) + localIter = localChildren.erase(localIter); + localEnd = localChildren.end(); + } else { + ++localIter; + } + } + + if (!removedChildren.isEmpty()) { + removedRemoteCollections[parentRid] = removedChildren; + } else { + removedRemoteCollections.remove(parentRid); + } + + if (!remoteChildren.isEmpty()) { + remoteCollections[parentRid] = remoteChildren; + } else { + remoteCollections.remove(parentRid); + } + + if (!localChildren.isEmpty()) { + localCollections[parentRid] = localChildren; + } else { + localCollections.remove(parentRid); + } + } + + void processLocalCollections(const RemoteId &parentRid, const Collection &parentCollection) + { + const Collection::List originalChildren = localCollections.value(parentRid); + processCollections(parentRid); + + const Collection::List remoteChildren = remoteCollections.take(parentRid); + const Collection::List localChildren = localCollections.take(parentRid); + + // At this point remoteChildren contains collections that don't exist locally yet + if (!remoteChildren.isEmpty()) { + for (Collection c : remoteChildren) { + c.setParentCollection(parentCollection); + remoteCollectionsToCreate.append(c); + } + } + // At this point localChildren contains collections that don't exist remotely anymore + if (!localChildren.isEmpty() && !incremental) { + for (const auto &c : localChildren) { + if (!c.remoteId().isEmpty()) { + localCollectionsToRemove.push_back(c); + } + } + } + + // Recurse into children + for (const Collection &c : originalChildren) { + processLocalCollections(remoteIdForCollection(c), c); + } + } + + void localCollectionFetchResult(KJob *job) + { + if (job->error()) { + return; // handled by the base class + } + + processLocalCollections(RemoteId::rootRid, akonadiRootCollection); + localListDone = true; + execute(); + } + + bool ignoreAttributeChanges(const Akonadi::Collection &col, const QByteArray &attribute) const + { + return (keepLocalChanges.contains(attribute) || col.keepLocalChanges().contains(attribute)); + } + + /** + Checks if the given localCollection and remoteCollection are different + */ + bool collectionNeedsUpdate(const Collection &localCollection, const Collection &remoteCollection) const + { + if (!ignoreAttributeChanges(remoteCollection, CONTENTMIMETYPES)) { + if (localCollection.contentMimeTypes().size() != remoteCollection.contentMimeTypes().size()) { + return true; + } else { + for (int i = 0, total = remoteCollection.contentMimeTypes().size(); i < total; ++i) { + const QString &m = remoteCollection.contentMimeTypes().at(i); + if (!localCollection.contentMimeTypes().contains(m)) { + return true; + } + } + } + } + + if (localCollection.parentCollection().remoteId() != remoteCollection.parentCollection().remoteId()) { + return true; + } + if (localCollection.name() != remoteCollection.name()) { + return true; + } + if (localCollection.remoteId() != remoteCollection.remoteId()) { + return true; + } + if (localCollection.remoteRevision() != remoteCollection.remoteRevision()) { + return true; + } + if (!(localCollection.cachePolicy() == remoteCollection.cachePolicy())) { + return true; + } + if (localCollection.enabled() != remoteCollection.enabled()) { + return true; + } + + // CollectionModifyJob adds the remote attributes to the local collection + const Akonadi::Attribute::List lstAttr = remoteCollection.attributes(); + for (const Attribute *attr : lstAttr) { + const Attribute *localAttr = localCollection.attribute(attr->type()); + if (localAttr && ignoreAttributeChanges(remoteCollection, attr->type())) { + continue; + } + // The attribute must both exist and have equal contents + if (!localAttr || localAttr->serialized() != attr->serialized()) { + return true; + } + } + + return false; + } + + void createLocalCollections() + { + if (remoteCollectionsToCreate.isEmpty()) { + updateLocalCollections(); + return; + } + + for (auto iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end;) { + const Collection col = *iter; + const Collection parentCollection = col.parentCollection(); + // The parent already exists locally + if (parentCollection == akonadiRootCollection || parentCollection.id() > 0) { + ++pendingJobs; + auto create = new CollectionCreateJob(col, currentTransaction); + QObject::connect(create, &KJob::result, q, [this](KJob *job) { + createLocalCollectionResult(job); + }); + + // Commit transaction after every 100 collections are created, + // otherwise it overloads database journal and things get veeery slow + if (pendingJobs % 100 == 0) { + currentTransaction->commit(); + createTransaction(); + } + + iter = remoteCollectionsToCreate.erase(iter); + end = remoteCollectionsToCreate.end(); + } else { + // Skip the collection, we'll try again once we create all the other + // collection we already have a parent for + ++iter; + } + } + } + + void createLocalCollectionResult(KJob *job) + { + --pendingJobs; + if (job->error()) { + return; // handled by the base class + } + + q->setProcessedAmount(KJob::Bytes, ++progress); + + const Collection newLocal = static_cast(job)->collection(); + uidRidMap.insert(newLocal.id(), newLocal.remoteId()); + const RemoteId newLocalRID = remoteIdForCollection(newLocal); + + // See if there are any pending collections that this collection is parent of and + // update them if so + for (auto iter = remoteCollectionsToCreate.begin(), end = remoteCollectionsToCreate.end(); iter != end; ++iter) { + const Collection parentCollection = iter->parentCollection(); + if (parentCollection != akonadiRootCollection && parentCollection.id() <= 0) { + const RemoteId remoteRID = remoteIdForCollection(*iter); + if (remoteRID.isAbsolute()) { + if (newLocalRID == remoteIdForCollection(*iter)) { + iter->setParentCollection(newLocal); + } + } else if (!hierarchicalRIDs) { + if (remoteRID.ridChain.startsWith(parentCollection.remoteId())) { + iter->setParentCollection(newLocal); + } + } + } + } + + // Enqueue all pending remote collections that are children of the just-created + // collection + Collection::List collectionsToCreate = remoteCollections.take(newLocalRID); + if (collectionsToCreate.isEmpty() && !hierarchicalRIDs) { + collectionsToCreate = remoteCollections.take(RemoteId(newLocal.remoteId())); + } + for (Collection col : std::as_const(collectionsToCreate)) { + col.setParentCollection(newLocal); + remoteCollectionsToCreate.append(col); + } + + // If there are still any collections to create left, try if we just created + // a parent for any of them + if (!remoteCollectionsToCreate.isEmpty()) { + createLocalCollections(); + } else if (pendingJobs == 0) { + Q_ASSERT(remoteCollectionsToCreate.isEmpty()); + if (!remoteCollections.isEmpty()) { + currentTransaction->rollback(); + q->setError(Unknown); + q->setErrorText(i18n("Found unresolved orphan collections")); + qCWarning(AKONADICORE_LOG) << "found unresolved orphan collection"; + emitResult(); + return; + } + + currentTransaction->commit(); + createTransaction(); + + // Otherwise move to next task: updating existing collections + updateLocalCollections(); + } + /* + * else if (!remoteCollections.isEmpty()) { + currentTransaction->rollback(); + q->setError(Unknown); + q->setErrorText(i18n("Incomplete collection tree")); + emitResult(); + return; + } + */ + } + + /** + Performs a local update for the given node pair. + */ + void updateLocalCollections() + { + if (remoteCollectionsToUpdate.isEmpty()) { + deleteLocalCollections(); + return; + } + + using CollectionPair = QPair; + for (const CollectionPair &pair : std::as_const(remoteCollectionsToUpdate)) { + const Collection local = pair.first; + const Collection remote = pair.second; + Collection upd(remote); + + Q_ASSERT(!upd.remoteId().isEmpty()); + Q_ASSERT(currentTransaction); + upd.setId(local.id()); + if (ignoreAttributeChanges(remote, CONTENTMIMETYPES)) { + upd.setContentMimeTypes(local.contentMimeTypes()); + } + Q_FOREACH (Attribute *remoteAttr, upd.attributes()) { + if (ignoreAttributeChanges(remote, remoteAttr->type()) && local.hasAttribute(remoteAttr->type())) { + // We don't want to overwrite the attribute changes with the defaults provided by the resource. + const Attribute *localAttr = local.attribute(remoteAttr->type()); + upd.removeAttribute(localAttr->type()); + upd.addAttribute(localAttr->clone()); + } + } + + // ### HACK to work around the implicit move attempts of CollectionModifyJob + // which we do explicitly below + Collection c(upd); + c.setParentCollection(local.parentCollection()); + ++pendingJobs; + auto mod = new CollectionModifyJob(c, currentTransaction); + QObject::connect(mod, &KJob::result, q, [this](KJob *job) { + updateLocalCollectionResult(job); + }); + + // detecting moves is only possible with global RIDs + if (!hierarchicalRIDs) { + if (remote.parentCollection().isValid() && remote.parentCollection().id() != local.parentCollection().id()) { + ++pendingJobs; + auto move = new CollectionMoveJob(upd, remote.parentCollection(), currentTransaction); + QObject::connect(move, &KJob::result, q, [this](KJob *job) { + updateLocalCollectionResult(job); + }); + } + } + } + } + + void updateLocalCollectionResult(KJob *job) + { + --pendingJobs; + if (job->error()) { + return; // handled by the base class + } + if (qobject_cast(job)) { + q->setProcessedAmount(KJob::Bytes, ++progress); + } + + // All updates are done, time to move on to next task: deletion + if (pendingJobs == 0) { + currentTransaction->commit(); + createTransaction(); + + deleteLocalCollections(); + } + } + + void deleteLocalCollections() + { + if (localCollectionsToRemove.isEmpty()) { + done(); + return; + } + + for (const Collection &col : std::as_const(localCollectionsToRemove)) { + Q_ASSERT(!col.remoteId().isEmpty()); // empty RID -> stuff we haven't even written to the remote side yet + + ++pendingJobs; + Q_ASSERT(currentTransaction); + auto job = new CollectionDeleteJob(col, currentTransaction); + connect(job, &KJob::result, q, [this](KJob *job) { + deleteLocalCollectionsResult(job); + }); + + // It can happen that the groupware servers report us deleted collections + // twice, in this case this collection delete job will fail on the second try. + // To avoid a rollback of the complete transaction we gracefully allow the job + // to fail :) + currentTransaction->setIgnoreJobFailure(job); + } + } + + void deleteLocalCollectionsResult(KJob * /*unused*/) + { + --pendingJobs; + q->setProcessedAmount(KJob::Bytes, ++progress); + + if (pendingJobs == 0) { + currentTransaction->commit(); + currentTransaction = nullptr; + + done(); + } + } + + void done() + { + if (currentTransaction) { + // This can trigger a direct call of transactionSequenceResult + currentTransaction->commit(); + currentTransaction = nullptr; + } + + if (!remoteCollections.isEmpty()) { + q->setError(Unknown); + q->setErrorText(i18n("Found unresolved orphan collections")); + } + emitResult(); + } + + void emitResult() + { + // Prevent double result emission + Q_ASSERT(!resultEmitted); + if (!resultEmitted) { + if (q->hasSubjobs()) { + // If there are subjobs, pick one, wait for it to finish, then + // try again. This way we make sure we don't emit result() signal + // while there is still a Transaction job running + KJob *subjob = q->subjobs().at(0); + connect( + subjob, + &KJob::result, + q, + [this](KJob * /*unused*/) { + emitResult(); + }, + Qt::QueuedConnection); + } else { + resultEmitted = true; + q->emitResult(); + } + } + } + + void createTransaction() + { + currentTransaction = new TransactionSequence(q); + currentTransaction->setAutomaticCommittingEnabled(false); + q->connect(currentTransaction, &TransactionSequence::finished, q, [this](KJob *job) { + transactionSequenceResult(job); + }); + } + + /** After the transaction has finished report we're done as well. */ + void transactionSequenceResult(KJob *job) + { + if (job->error()) { + return; // handled by the base class + } + + // If this was the last transaction, then finish, otherwise there's + // a new transaction in the queue already + if (job == currentTransaction) { + currentTransaction = nullptr; + } + } + + /** + Process what's currently available. + */ + void execute() + { + qCDebug(AKONADICORE_LOG) << "localListDone: " << localListDone << " deliveryDone: " << deliveryDone; + if (!localListDone && !deliveryDone) { + return; + } + + if (!localListDone && deliveryDone) { + Job *parent = (currentTransaction ? static_cast(currentTransaction) : static_cast(q)); + auto job = new CollectionFetchJob(akonadiRootCollection, CollectionFetchJob::Recursive, parent); + job->fetchScope().setResource(resourceId); + job->fetchScope().setListFilter(CollectionFetchScope::NoFilter); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { + localCollectionsReceived(cols); + }); + q->connect(job, &KJob::result, q, [this](KJob *job) { + localCollectionFetchResult(job); + }); + return; + } + + // If a transaction is not started yet, it means we just finished local listing + if (!currentTransaction) { + // There's nothing to do after local listing -> we are done! + if (remoteCollectionsToCreate.isEmpty() && remoteCollectionsToUpdate.isEmpty() && localCollectionsToRemove.isEmpty()) { + qCDebug(AKONADICORE_LOG) << "Nothing to do"; + emitResult(); + return; + } + // Ok, there's some work to do, so create a transaction we can use + createTransaction(); + } + + createLocalCollections(); + } + + CollectionSync *const q; + + QString resourceId; + + int pendingJobs; + int progress; + + TransactionSequence *currentTransaction; + + bool incremental; + bool streaming; + bool hierarchicalRIDs; + + bool localListDone; + bool deliveryDone; + + // List of parts where local changes should not be overwritten + QSet keepLocalChanges; + + QHash removedRemoteCollections; + QHash remoteCollections; + QHash localCollections; + + Collection::List localCollectionsToRemove; + Collection::List remoteCollectionsToCreate; + QList> remoteCollectionsToUpdate; + QHash uidRidMap; + + // HACK: To workaround Collection copy constructor being very expensive, we + // store the Collection::root() collection in a variable here for faster + // access + Collection akonadiRootCollection; + + bool resultEmitted; +}; + +CollectionSync::CollectionSync(const QString &resourceId, QObject *parent) + : Job(parent) + , d(new Private(this)) +{ + d->resourceId = resourceId; + setTotalAmount(KJob::Bytes, 0); +} + +CollectionSync::~CollectionSync() +{ + delete d; +} + +void CollectionSync::setRemoteCollections(const Collection::List &remoteCollections) +{ + setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + remoteCollections.count()); + for (const Collection &c : remoteCollections) { + d->addRemoteColection(c); + } + + if (!d->streaming) { + d->deliveryDone = true; + } + d->execute(); +} + +void CollectionSync::setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections) +{ + setTotalAmount(KJob::Bytes, totalAmount(KJob::Bytes) + changedCollections.count()); + d->incremental = true; + for (const Collection &c : changedCollections) { + d->addRemoteColection(c); + } + for (const Collection &c : removedCollections) { + d->addRemoteColection(c, true); + } + + if (!d->streaming) { + d->deliveryDone = true; + } + d->execute(); +} + +void CollectionSync::doStart() +{ +} + +void CollectionSync::setStreamingEnabled(bool streaming) +{ + d->streaming = streaming; +} + +void CollectionSync::retrievalDone() +{ + d->deliveryDone = true; + d->execute(); +} + +void CollectionSync::setHierarchicalRemoteIds(bool hierarchical) +{ + d->hierarchicalRIDs = hierarchical; +} + +void CollectionSync::rollback() +{ + if (d->currentTransaction) { + d->currentTransaction->rollback(); + } else { + setError(UserCanceled); + emitResult(); + } +} + +void CollectionSync::setKeepLocalChanges(const QSet &parts) +{ + d->keepLocalChanges = parts; +} + +#include "moc_collectionsync_p.cpp" diff --git a/src/core/collectionsync_p.h b/src/core/collectionsync_p.h new file mode 100644 index 0000000..bba0534 --- /dev/null +++ b/src/core/collectionsync_p.h @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2007, 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "transactionsequence.h" + +namespace Akonadi +{ +/** + @internal + + Syncs remote and local collections. + + Basic terminology: + - "local": The current state in the Akonadi server + - "remote": The state in the backend, which is also the state the Akonadi + server is supposed to have afterwards. + + There are three options to influence the way syncing is done: + - Streaming vs. complete delivery: If streaming is enabled remote collections + do not need to be delivered in a single batch but can be delivered in multiple + chunks. This improves performance but requires an explicit notification + when delivery has been completed. + - Incremental vs. non-incremental: In the incremental case only remote changes + since the last sync have to be delivered, in the non-incremental mode the full + remote state has to be provided. The first is obviously the preferred way, + but requires support by the backend. + - Hierarchical vs. global RIDs: The first requires RIDs to be unique per parent + collection, the second one requires globally unique RIDs (per resource). Those + have different advantages and disadvantages, esp. regarding moving. Which one + to chose mostly depends on what the backend provides in this regard. + +*/ +class AKONADICORE_EXPORT CollectionSync : public Job +{ + Q_OBJECT + +public: + /** + Creates a new collection synchronzier. + @param resourceId The identifier of the resource we are syncing. + @param parent The parent object. + */ + explicit CollectionSync(const QString &resourceId, QObject *parent = nullptr); + + /** + Destroys this job. + */ + ~CollectionSync() override; + + /** + Sets the result of a full remote collection listing. + @param remoteCollections A list of collections. + Important: All of these need a unique remote identifier and parent remote + identifier. + */ + void setRemoteCollections(const Collection::List &remoteCollections); + + /** + Sets the result of an incremental remote collection listing. + @param changedCollections A list of remotely added or changed collections. + @param removedCollections A list of remotely deleted collections. + */ + void setRemoteCollections(const Collection::List &changedCollections, const Collection::List &removedCollections); + + /** + Enables streaming, that is not all collections are delivered at once. + Use setRemoteCollections() multiple times when streaming is enabled and call + retrievalDone() when all collections have been retrieved. + Must be called before the first call to setRemoteCollections(). + @param streaming enables streaming if set as @c true + */ + void setStreamingEnabled(bool streaming); + + /** + Indicate that all collections have been retrieved in streaming mode. + */ + void retrievalDone(); + + /** + Indicate whether the resource supplies collections with hierarchical or + global remote identifiers. @c false by default. + Must be called before the first call to setRemoteCollections(). + @param hierarchical @c true if collection remote IDs are relative to their parents' remote IDs + */ + void setHierarchicalRemoteIds(bool hierarchical); + + /** + Do a rollback operation if needed. In read only cases this is a noop. + */ + void rollback(); + + /** + * Allows to specify parts of the collection that should not be changed if locally available. + * + * This is useful for resources to provide default values during the collection sync, while + * preserving more up-to date values if available. + * + * Use CONTENTMIMETYPES as identifier to not overwrite the content mimetypes. + * + * @since 4.14 + */ + void setKeepLocalChanges(const QSet &parts); + +protected: + void doStart() override; + +private: + class Private; + Private *const d; +}; + +} + diff --git a/src/core/collectionutils.h b/src/core/collectionutils.h new file mode 100644 index 0000000..658beb7 --- /dev/null +++ b/src/core/collectionutils.h @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "collectionstatistics.h" +#include "entitydisplayattribute.h" +#include "item.h" + +namespace Akonadi +{ +/** + * @internal + */ +namespace CollectionUtils +{ +Q_REQUIRED_RESULT inline bool isVirtualParent(const Collection &collection) +{ + return (collection.parentCollection() == Collection::root() && collection.isVirtual()); +} + +Q_REQUIRED_RESULT inline bool isReadOnly(const Collection &collection) +{ + return !(collection.rights() & Collection::CanCreateItem); +} + +Q_REQUIRED_RESULT inline bool isRoot(const Collection &collection) +{ + return (collection == Collection::root()); +} + +Q_REQUIRED_RESULT inline bool isResource(const Collection &collection) +{ + return (collection.parentCollection() == Collection::root()); +} + +Q_REQUIRED_RESULT inline bool isStructural(const Collection &collection) +{ + return collection.contentMimeTypes().isEmpty(); +} + +Q_REQUIRED_RESULT inline bool isFolder(const Collection &collection) +{ + return (!isRoot(collection) && !isResource(collection) && !isStructural(collection) && collection.resource() != QLatin1String("akonadi_search_resource")); +} + +Q_REQUIRED_RESULT inline bool isUnifiedMailbox(const Collection &collection) +{ + return collection.resource() == QLatin1String("akonadi_unifiedmailbox_agent"); +} + +Q_REQUIRED_RESULT inline QString defaultIconName(const Collection &col) +{ + if (CollectionUtils::isVirtualParent(col)) { + return QStringLiteral("edit-find"); + } + if (col.isVirtual()) { + return QStringLiteral("document-preview"); + } + if (CollectionUtils::isResource(col)) { + return QStringLiteral("network-server"); + } + if (CollectionUtils::isStructural(col)) { + return QStringLiteral("folder-grey"); + } + if (CollectionUtils::isReadOnly(col)) { + return QStringLiteral("folder-grey"); + } + + const QStringList content = col.contentMimeTypes(); + if ((content.size() == 1) || (content.size() == 2 && content.contains(Collection::mimeType()))) { + if (content.contains(QLatin1String("text/x-vcard")) || content.contains(QLatin1String("text/directory")) + || content.contains(QLatin1String("text/vcard"))) { + return QStringLiteral("x-office-address-book"); + } + // TODO: add all other content types and/or fix their mimetypes + if (content.contains(QLatin1String("akonadi/event")) || content.contains(QLatin1String("text/ical"))) { + return QStringLiteral("view-pim-calendar"); + } + if (content.contains(QLatin1String("akonadi/task"))) { + return QStringLiteral("view-pim-tasks"); + } + } else if (content.isEmpty()) { + return QStringLiteral("folder-grey"); + } + return QStringLiteral("folder"); +} +Q_REQUIRED_RESULT inline QString displayIconName(const Collection &col) +{ + QString iconName = defaultIconName(col); + if (col.hasAttribute() && !col.attribute()->iconName().isEmpty()) { + if (!col.attribute()->activeIconName().isEmpty() && col.statistics().unreadCount() > 0) { + iconName = col.attribute()->activeIconName(); + } else { + iconName = col.attribute()->iconName(); + } + } + return iconName; +} +Q_REQUIRED_RESULT inline bool hasValidHierarchicalRID(const Collection &col) +{ + if (col == Collection::root()) { + return true; + } + if (col.remoteId().isEmpty()) { + return false; + } + return hasValidHierarchicalRID(col.parentCollection()); +} +Q_REQUIRED_RESULT inline bool hasValidHierarchicalRID(const Item &item) +{ + return !item.remoteId().isEmpty() && hasValidHierarchicalRID(item.parentCollection()); +} +} + +} + diff --git a/src/core/commandbuffer_p.h b/src/core/commandbuffer_p.h new file mode 100644 index 0000000..f5d326e --- /dev/null +++ b/src/core/commandbuffer_p.h @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +class QObject; + +#include +#include + +#include + +#include "akonadicore_debug.h" + +namespace Akonadi +{ +class CommandBufferLocker; +class CommandBufferNotifyBlocker; + +class CommandBuffer +{ + friend class CommandBufferLocker; + friend class CommandBufferNotifyBlocker; + +public: + struct Command { + qint64 tag; + Protocol::CommandPtr command; + }; + + CommandBuffer(QObject *parent, const char *notifySlot) + : mParent(parent) + , mNotifySlot(notifySlot) + { + } + + void enqueue(qint64 tag, const Protocol::CommandPtr &command) + { + mCommands.enqueue({tag, command}); + if (mNotify) { + const bool ok = QMetaObject::invokeMethod(mParent, mNotifySlot.constData(), Qt::QueuedConnection); + Q_ASSERT(ok); + Q_UNUSED(ok) + } + } + + inline Command dequeue() + { + return mCommands.dequeue(); + } + + inline bool isEmpty() const + { + return mCommands.isEmpty(); + } + + inline int size() const + { + return mCommands.size(); + } + +private: + Q_DISABLE_COPY_MOVE(CommandBuffer) + + QObject *mParent = nullptr; + QByteArray mNotifySlot; + + QQueue mCommands; + QMutex mLock; + + bool mNotify = true; +}; + +class CommandBufferLocker +{ +public: + explicit CommandBufferLocker(CommandBuffer *buffer) + : mBuffer(buffer) + { + relock(); + } + + ~CommandBufferLocker() + { + unlock(); + } + + inline void unlock() + { + if (mLocked) { + mBuffer->mLock.unlock(); + mLocked = false; + } + } + + inline void relock() + { + if (!mLocked) { + mBuffer->mLock.lock(); + mLocked = true; + } + } + +private: + Q_DISABLE_COPY_MOVE(CommandBufferLocker) + + CommandBuffer *mBuffer = nullptr; + bool mLocked = false; +}; + +class CommandBufferNotifyBlocker +{ +public: + explicit CommandBufferNotifyBlocker(CommandBuffer *buffer) + : mBuffer(buffer) + { + mBuffer->mNotify = false; + } + + ~CommandBufferNotifyBlocker() + { + unblock(); + } + + void unblock() + { + mBuffer->mNotify = true; + } + +private: + Q_DISABLE_COPY_MOVE(CommandBufferNotifyBlocker) + + CommandBuffer *mBuffer; +}; + +} // namespace + diff --git a/src/core/config.cpp b/src/core/config.cpp new file mode 100644 index 0000000..f0f0a26 --- /dev/null +++ b/src/core/config.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "config_p.h" +#include "private/instance_p.h" + +#include +#include + +using namespace Akonadi; + +Q_GLOBAL_STATIC(Config, sConfig) // NOLINT(readability-redundant-member-init) + +namespace +{ +QString getConfigName() +{ + if (Instance::hasIdentifier()) { + return QStringLiteral("akonadi_%1rc").arg(Instance::identifier()); + } else { + return QStringLiteral("akonadirc"); + } +} + +static constexpr char group_PayloadCompression[] = "PayloadCompression"; + +// Payload compression +static constexpr char key_PC_Enabled[] = "enabled"; + +} // namespace + +Config::Config() +{ + auto config = KSharedConfig::openConfig(getConfigName()); + + { + const auto group = config->group(group_PayloadCompression); + payloadCompression.enabled = group.readEntry(key_PC_Enabled, payloadCompression.enabled); + } +} + +const Config &Config::get() +{ + return *sConfig; +} diff --git a/src/core/config_p.h b/src/core/config_p.h new file mode 100644 index 0000000..fc124c7 --- /dev/null +++ b/src/core/config_p.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +class Config +{ +public: + explicit Config(); + ~Config() = default; + + static const Config &get(); + + struct PayloadCompression { + /** + * Whether or not the payload compression feature should be enabled. + * Default is false (currently). + * + * This only disables only compressing the payload. If the feature is disabled, + * Akonadi can still decompress payloads that have been compressed previously. + */ + bool enabled = false; + }; + + /** + * Configures behavior of the payload compression feature. + */ + PayloadCompression payloadCompression = {}; +}; + +} // namespace Akonadi + diff --git a/src/core/conflicthandler.cpp b/src/core/conflicthandler.cpp new file mode 100644 index 0000000..c36586c --- /dev/null +++ b/src/core/conflicthandler.cpp @@ -0,0 +1,129 @@ +/* + SPDX-FileCopyrightText: 2010 KDAB + SPDX-FileContributor: Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "conflicthandler_p.h" + +#include "itemcreatejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "session.h" +#include + +using namespace Akonadi; + +ConflictHandler::ConflictHandler(ConflictType type, QObject *parent) + : QObject(parent) + , mConflictType(type) + , mSession(new Session("conflict handling session", this)) +{ +} + +void ConflictHandler::setConflictingItems(const Akonadi::Item &changedItem, const Akonadi::Item &conflictingItem) +{ + mChangedItem = changedItem; + mConflictingItem = conflictingItem; +} + +void ConflictHandler::start() +{ + if (mConflictType == LocalLocalConflict || mConflictType == LocalRemoteConflict) { + auto job = new ItemFetchJob(mConflictingItem, mSession); + job->fetchScope().fetchFullPayload(); + job->fetchScope().setAncestorRetrieval(ItemFetchScope::Parent); + connect(job, &ItemFetchJob::result, this, &ConflictHandler::slotOtherItemFetched); + } else { + resolve(); + } +} + +void ConflictHandler::slotOtherItemFetched(KJob *job) +{ + if (job->error()) { + Q_EMIT error(job->errorText()); // TODO: extend error message + return; + } + + auto fetchJob = qobject_cast(job); + if (fetchJob->items().isEmpty()) { + Q_EMIT error(i18n("Did not find other item for conflict handling")); + return; + } + + mConflictingItem = fetchJob->items().at(0); + QMetaObject::invokeMethod(this, &ConflictHandler::resolve, Qt::QueuedConnection); +} + +void ConflictHandler::resolve() +{ +#pragma message("warning KF5 Port me!") +#if 0 + ConflictResolveDialog dlg; + dlg.setConflictingItems(mChangedItem, mConflictingItem); + dlg.exec(); + + const ResolveStrategy strategy = dlg.resolveStrategy(); + switch (strategy) { + case UseLocalItem: + useLocalItem(); + break; + case UseOtherItem: + useOtherItem(); + break; + case UseBothItems: + useBothItems(); + break; + } +#endif +} + +void ConflictHandler::useLocalItem() +{ + // We have to overwrite the other item inside the Akonadi storage with the local + // item. To make this happen, we have to set the revision of the local item to + // the one of the other item to let the Akonadi server accept it. + + Item newItem(mChangedItem); + newItem.setRevision(mConflictingItem.revision()); + + auto job = new ItemModifyJob(newItem, mSession); + connect(job, &ItemModifyJob::result, this, &ConflictHandler::slotUseLocalItemFinished); +} + +void ConflictHandler::slotUseLocalItemFinished(KJob *job) +{ + if (job->error()) { + Q_EMIT error(job->errorText()); // TODO: extend error message + } else { + Q_EMIT conflictResolved(); + } +} + +void ConflictHandler::useOtherItem() +{ + // We can just ignore the local item here and leave everything as it is. + Q_EMIT conflictResolved(); +} + +void ConflictHandler::useBothItems() +{ + // We have to create a new item for the local item under the collection that has + // been retrieved when we fetched the other item. + auto job = new ItemCreateJob(mChangedItem, mConflictingItem.parentCollection(), mSession); + connect(job, &ItemCreateJob::result, this, &ConflictHandler::slotUseBothItemsFinished); +} + +void ConflictHandler::slotUseBothItemsFinished(KJob *job) +{ + if (job->error()) { + Q_EMIT error(job->errorText()); // TODO: extend error message + } else { + Q_EMIT conflictResolved(); + } +} + +#include "moc_conflicthandler_p.cpp" diff --git a/src/core/conflicthandler_p.h b/src/core/conflicthandler_p.h new file mode 100644 index 0000000..2987620 --- /dev/null +++ b/src/core/conflicthandler_p.h @@ -0,0 +1,106 @@ +/* + SPDX-FileCopyrightText: 2010 KDAB + SPDX-FileContributor: Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "item.h" + +class KJob; + +namespace Akonadi +{ +class Session; + +/** + * @short A class to handle conflicts in Akonadi + * + * @author Tobias Koenig + */ +class ConflictHandler : public QObject +{ + Q_OBJECT + +public: + /** + * Describes the type of conflict that should be resolved by + * the conflict handler. + */ + enum ConflictType { + LocalLocalConflict, ///< Changes of two Akonadi client applications conflict. + LocalRemoteConflict, ///< Changes of an Akonadi client application and a resource conflict. + BackendConflict ///< Changes of a resource and the backend data conflict. + }; + + /** + * Describes the strategy that should be used for resolving the conflict. + */ + enum ResolveStrategy { + UseLocalItem, ///< The local item overwrites the other item inside the Akonadi storage. + UseOtherItem, ///< The local item is dropped and the other item from the Akonadi storage is used. + UseBothItems ///< Both items are kept in the Akonadi storage. + }; + + /** + * Creates a new conflict handler. + * + * @param type The type of the conflict that should be resolved. + * @param parent The parent object. + */ + explicit ConflictHandler(ConflictType type, QObject *parent = nullptr); + + /** + * Sets the items that causes the conflict. + * + * @param changedItem The item that has been changed, it needs the complete payload set. + * @param conflictingItem The item from the Akonadi storage that is conflicting. + * This needs only the id set, the payload will be refetched automatically. + */ + void setConflictingItems(const Akonadi::Item &changedItem, const Akonadi::Item &conflictingItem); + +public Q_SLOTS: + /** + * Starts the conflict handling. + */ + void start(); + +Q_SIGNALS: + /** + * This signal is emitted whenever the conflict has been resolved + * automatically or by the user. + */ + void conflictResolved(); + + /** + * This signal is emitted whenever an error occurred during the conflict + * handling. + * + * @param message A user visible string that describes the error. + */ + void error(const QString &message); + +private Q_SLOTS: + void slotOtherItemFetched(KJob *); + void slotUseLocalItemFinished(KJob *); + void slotUseBothItemsFinished(KJob *); + void resolve(); + +private: + void useLocalItem(); + void useOtherItem(); + void useBothItems(); + + const ConflictType mConflictType; + Akonadi::Item mChangedItem; + Akonadi::Item mConflictingItem; + + Session *mSession = nullptr; +}; + +} + diff --git a/src/core/connection.cpp b/src/core/connection.cpp new file mode 100644 index 0000000..02d135b --- /dev/null +++ b/src/core/connection.cpp @@ -0,0 +1,324 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "akonadicore_debug.h" +#include "commandbuffer_p.h" +#include "connection_p.h" +#include "servermanager_p.h" +#include "session_p.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +Connection::Connection(ConnectionType connType, const QByteArray &sessionId, CommandBuffer *commandBuffer, QObject *parent) + : QObject(parent) + , mConnectionType(connType) + , mSessionId(sessionId) + , mCommandBuffer(commandBuffer) +{ + qRegisterMetaType(); + qRegisterMetaType(); + + const QByteArray sessionLogFile = qgetenv("AKONADI_SESSION_LOGFILE"); + if (!sessionLogFile.isEmpty()) { + mLogFile = new QFile(QStringLiteral("%1.%2.%3.%4-%5") + .arg(QString::fromLatin1(sessionLogFile)) + .arg(QApplication::applicationPid()) + .arg(QString::number(reinterpret_cast(this), 16), + QString::fromLatin1(mSessionId.replace('/', '_')), + connType == CommandConnection ? QStringLiteral("Cmd") : QStringLiteral("Ntf"))); + if (!mLogFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(AKONADICORE_LOG) << "Failed to open Akonadi Session log file" << mLogFile->fileName(); + delete mLogFile; + mLogFile = nullptr; + } + } +} + +Connection::~Connection() +{ + delete mLogFile; + if (mSocket) { + mSocket->disconnect(); + mSocket->disconnectFromServer(); + mSocket->close(); + mSocket.reset(); + } +} + +void Connection::reconnect() +{ + const bool ok = QMetaObject::invokeMethod(this, &Connection::doReconnect, Qt::QueuedConnection); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +QString Connection::defaultAddressForTypeAndMethod(ConnectionType type, const QString &method) +{ + if (method == QLatin1String("UnixPath")) { + const QString defaultSocketDir = StandardDirs::saveDir("data"); + if (type == CommandConnection) { + return defaultSocketDir % QStringLiteral("akonadiserver-cmd.socket"); + } else if (type == NotificationConnection) { + return defaultSocketDir % QStringLiteral("akonadiserver-ntf.socket"); + } + } else if (method == QLatin1String("NamedPipe")) { + QString suffix; + if (Instance::hasIdentifier()) { + suffix += QStringLiteral("%1-").arg(Instance::identifier()); + } + suffix += QString::fromUtf8(QUrl::toPercentEncoding(qApp->applicationDirPath())); + if (type == CommandConnection) { + return QStringLiteral("Akonadi-Cmd-") % suffix; + } else if (type == NotificationConnection) { + return QStringLiteral("Akonadi-Ntf-") % suffix; + } + } + + Q_UNREACHABLE(); +} + +void Connection::doReconnect() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mSocket && (mSocket->state() == QLocalSocket::ConnectedState || mSocket->state() == QLocalSocket::ConnectingState)) { + // nothing to do, we are still/already connected + return; + } + + if (ServerManager::self()->state() != ServerManager::Running) { + return; + } + + // try to figure out where to connect to + QString serverAddress; + + // env var has precedence + const QByteArray serverAddressEnvVar = qgetenv("AKONADI_SERVER_ADDRESS"); + if (!serverAddressEnvVar.isEmpty()) { + const int pos = serverAddressEnvVar.indexOf(':'); + const QByteArray protocol = serverAddressEnvVar.left(pos); + QMap options; + const QStringList lst = QString::fromLatin1(serverAddressEnvVar.mid(pos + 1)).split(QLatin1Char(',')); + for (const QString &entry : lst) { + const QStringList pair = entry.split(QLatin1Char('=')); + if (pair.size() != 2) { + continue; + } + options.insert(pair.first(), pair.last()); + } + + if (protocol == "unix") { + serverAddress = options.value(QStringLiteral("path")); + } else if (protocol == "pipe") { + serverAddress = options.value(QStringLiteral("name")); + } + } + + // try config file next, fall back to defaults if that fails as well + if (serverAddress.isEmpty()) { + const QString connectionConfigFile = StandardDirs::connectionConfigFile(); + const QFileInfo fileInfo(connectionConfigFile); + if (!fileInfo.exists()) { + qCWarning(AKONADICORE_LOG) << "Akonadi Client Session: connection config file '" + "akonadi/akonadiconnectionrc' can not be found!"; + } + + QSettings connectionSettings(connectionConfigFile, QSettings::IniFormat); + + QString connectionType; + if (mConnectionType == CommandConnection) { + connectionType = QStringLiteral("Data"); + } else if (mConnectionType == NotificationConnection) { + connectionType = QStringLiteral("Notifications"); + } + + connectionSettings.beginGroup(connectionType); + const auto method = connectionSettings.value(QStringLiteral("Method"), QStringLiteral("UnixPath")).toString(); + serverAddress = connectionSettings.value(method, defaultAddressForTypeAndMethod(mConnectionType, method)).toString(); + } + + mSocket.reset(new QLocalSocket(this)); + connect(mSocket.data(), + &QLocalSocket::errorOccurred, + this, + [this](QLocalSocket::LocalSocketError /*unused*/) { + qCWarning(AKONADICORE_LOG) << mSocket->errorString() << mSocket->serverName(); + Q_EMIT socketError(mSocket->errorString()); + Q_EMIT socketDisconnected(); + }); + connect(mSocket.data(), &QLocalSocket::disconnected, this, &Connection::socketDisconnected); + // note: we temporarily disconnect from readyRead-signal inside handleIncomingData() + connect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData); + + // actually do connect + qCDebug(AKONADICORE_LOG) << "connectToServer" << serverAddress; + mSocket->connectToServer(serverAddress); + if (!mSocket->waitForConnected()) { + qCWarning(AKONADICORE_LOG) << "Failed to connect to server!"; + Q_EMIT socketError(tr("Failed to connect to server!")); + mSocket.reset(); + return; + } + + QTimer::singleShot(0, this, &Connection::handleIncomingData); + + Q_EMIT reconnected(); +} + +void Connection::forceReconnect() +{ + const bool ok = QMetaObject::invokeMethod(this, &Connection::doForceReconnect, Qt::QueuedConnection); + + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void Connection::doForceReconnect() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mSocket) { + disconnect(mSocket.get(), &QLocalSocket::disconnected, this, &Connection::socketDisconnected); + mSocket->disconnectFromServer(); + mSocket.reset(); + } +} + +void Connection::closeConnection() +{ + const bool ok = QMetaObject::invokeMethod(this, &Connection::doCloseConnection, Qt::QueuedConnection); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void Connection::doCloseConnection() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mSocket) { + mSocket->close(); + mSocket.reset(); + } +} + +QLocalSocket *Connection::socket() const +{ + return mSocket.data(); +} + +void Connection::handleIncomingData() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (!mSocket) { // not connected yet + return; + } + + while (mSocket->bytesAvailable() >= int(sizeof(qint64))) { + Protocol::DataStream stream(mSocket.data()); + qint64 tag; + stream >> tag; + + // temporarily disconnect from readyRead-signal to avoid re-entering this function when we + // call waitForData() deep inside Protocol::deserialize + disconnect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData); + + Protocol::CommandPtr cmd; + try { + cmd = Protocol::deserialize(mSocket.data()); + } catch (const Akonadi::ProtocolException &e) { + qCWarning(AKONADICORE_LOG) << "Protocol exception:" << e.what(); + // cmd's type will be Invalid by default, so fall-through + } + + // reconnect to the signal again + connect(mSocket.data(), &QLocalSocket::readyRead, this, &Connection::handleIncomingData); + + if (!cmd || (cmd->type() == Protocol::Command::Invalid)) { + qCWarning(AKONADICORE_LOG) << "Invalid command, the world is going to end!"; + mSocket->close(); + reconnect(); + return; + } + + if (mLogFile) { + mLogFile->write("S: "); + mLogFile->write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz ")).toUtf8()); + mLogFile->write(QByteArray::number(tag)); + mLogFile->write(" "); + mLogFile->write(Protocol::debugString(cmd).toUtf8()); + mLogFile->write("\n\n"); + mLogFile->flush(); + } + + if (cmd->type() == Protocol::Command::Hello) { + Q_ASSERT(cmd->isResponse()); + } + + { + CommandBufferLocker locker(mCommandBuffer); + mCommandBuffer->enqueue(tag, cmd); + } + } +} + +void Connection::sendCommand(qint64 tag, const Protocol::CommandPtr &cmd) +{ + const bool ok = QMetaObject::invokeMethod(this, "doSendCommand", Qt::QueuedConnection, Q_ARG(qint64, tag), Q_ARG(Akonadi::Protocol::CommandPtr, cmd)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void Connection::doSendCommand(qint64 tag, const Protocol::CommandPtr &cmd) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mLogFile) { + mLogFile->write("C: "); + mLogFile->write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz ")).toUtf8()); + mLogFile->write(QByteArray::number(tag)); + mLogFile->write(" "); + mLogFile->write(Protocol::debugString(cmd).toUtf8()); + mLogFile->write("\n\n"); + mLogFile->flush(); + } + + if (mSocket && mSocket->isOpen()) { + Protocol::DataStream stream(mSocket.data()); + try { + stream << tag; + Protocol::serialize(stream, cmd); + stream.flush(); + } catch (const Akonadi::ProtocolException &e) { + qCWarning(AKONADICORE_LOG) << "Protocol Exception:" << QString::fromUtf8(e.what()); + mSocket->close(); + reconnect(); + return; + } + if (!mSocket->waitForBytesWritten()) { + qCWarning(AKONADICORE_LOG) << "Socket write timeout"; + mSocket->close(); + reconnect(); + return; + } + } else { + // TODO: Queue the commands and resend on reconnect? + } +} diff --git a/src/core/connection_p.h b/src/core/connection_p.h new file mode 100644 index 0000000..b98c623 --- /dev/null +++ b/src/core/connection_p.h @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include +#include + +#include "private/protocol_p.h" + +#include "akonadicore_export.h" + +class QFile; + +namespace Akonadi +{ +class SessionThread; +class SessionPrivate; +class CommandBuffer; + +class AKONADICORE_EXPORT Connection : public QObject +{ + Q_OBJECT + +public: + enum ConnectionType { + CommandConnection, + NotificationConnection, + }; + Q_ENUM(ConnectionType) + + explicit Connection(ConnectionType connType, const QByteArray &sessionId, CommandBuffer *commandBuffer, QObject *parent = nullptr); + ~Connection(); + + void setSession(SessionPrivate *session); + + QLocalSocket *socket() const; + + Q_INVOKABLE void reconnect(); + void forceReconnect(); + void closeConnection(); + void sendCommand(qint64 tag, const Protocol::CommandPtr &command); + + void handleIncomingData(); + +Q_SIGNALS: + void connected(); + void reconnected(); + void commandReceived(qint64 tag, const Akonadi::Protocol::CommandPtr &command); + void socketDisconnected(); + void socketError(const QString &message); + +private Q_SLOTS: + void doReconnect(); + void doForceReconnect(); + void doCloseConnection(); + void doSendCommand(qint64 tag, const Akonadi::Protocol::CommandPtr &command); + +private: + QString defaultAddressForTypeAndMethod(ConnectionType type, const QString &method); + bool handleCommand(qint64 tag, const Protocol::CommandPtr &cmd); + + ConnectionType mConnectionType; + QScopedPointer mSocket; + QFile *mLogFile = nullptr; + QByteArray mSessionId; + CommandBuffer *mCommandBuffer; + + friend class Akonadi::SessionThread; +}; + +} + diff --git a/src/core/control.cpp b/src/core/control.cpp new file mode 100644 index 0000000..e2cb9d7 --- /dev/null +++ b/src/core/control.cpp @@ -0,0 +1,154 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "control.h" +#include "akonadicore_debug.h" +#include "servermanager.h" + +#include +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +namespace Internal +{ +class StaticControl : public Control +{ + Q_OBJECT +}; + +} + +Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) // NOLINT(readability-redundant-member-init) + +/** + * @internal + */ +class Q_DECL_HIDDEN Control::Private +{ +public: + explicit Private(Control *parent) + : mParent(parent) + { + } + + void cleanup() + { + } + + bool exec(); + void serverStateChanged(ServerManager::State state); + + QPointer mParent; + QEventLoop *mEventLoop = nullptr; + bool mSuccess = false; + + bool mStarting = false; + bool mStopping = false; +}; + +bool Control::Private::exec() +{ + qCDebug(AKONADICORE_LOG) << "Starting/Stopping Akonadi (using an event loop)."; + mEventLoop = new QEventLoop(mParent); + mEventLoop->exec(); + mEventLoop->deleteLater(); + mEventLoop = nullptr; + + if (!mSuccess) { + qCWarning(AKONADICORE_LOG) << "Could not start/stop Akonadi!"; + } + + mStarting = false; + mStopping = false; + + const bool rv = mSuccess; + mSuccess = false; + return rv; +} + +void Control::Private::serverStateChanged(ServerManager::State state) +{ + qCDebug(AKONADICORE_LOG) << "Server state changed to" << state; + if (mEventLoop && mEventLoop->isRunning()) { + // ignore transient states going into the right direction + if ((mStarting && (state == ServerManager::Starting || state == ServerManager::Upgrading)) || (mStopping && state == ServerManager::Stopping)) { + return; + } + mEventLoop->quit(); + mSuccess = (mStarting && state == ServerManager::Running) || (mStopping && state == ServerManager::NotRunning); + } +} + +Control::Control() + : d(new Private(this)) +{ + connect(ServerManager::self(), &ServerManager::stateChanged, this, [this](Akonadi::ServerManager::State state) { + d->serverStateChanged(state); + }); + // mProgressIndicator is a widget, so it better be deleted before the QApplication is deleted + // Otherwise we get a crash in QCursor code with Qt-4.5 + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + d->cleanup(); + }); + } +} + +Control::~Control() +{ + delete d; +} + +bool Control::start() +{ + if (ServerManager::state() == ServerManager::Stopping) { + qCDebug(AKONADICORE_LOG) << "Server is currently being stopped, wont try to start it now"; + return false; + } + if (ServerManager::isRunning() || s_instance->d->mEventLoop) { + qCDebug(AKONADICORE_LOG) << "Server is already running"; + return true; + } + s_instance->d->mStarting = true; + if (!ServerManager::start()) { + qCDebug(AKONADICORE_LOG) << "ServerManager::start failed -> return false"; + return false; + } + return s_instance->d->exec(); +} + +bool Control::stop() +{ + if (ServerManager::state() == ServerManager::Starting) { + return false; + } + if (!ServerManager::isRunning() || s_instance->d->mEventLoop) { + return true; + } + s_instance->d->mStopping = true; + if (!ServerManager::stop()) { + return false; + } + return s_instance->d->exec(); +} + +bool Control::restart() +{ + if (ServerManager::isRunning()) { + if (!stop()) { + return false; + } + } + return start(); +} + +} // namespace Akonadi + +#include "control.moc" diff --git a/src/core/control.h b/src/core/control.h new file mode 100644 index 0000000..7840110 --- /dev/null +++ b/src/core/control.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Provides methods to control the Akonadi server process. + * + * This class provides synchronous methods (ie. use a sub-eventloop) + * to control the Akonadi service. For asynchronous methods see + * Akonadi::ServerManager. + * + * The most important method in here is widgetNeedsAkonadi(). It is + * recommended to call it with every top-level widget of your application + * as argument, assuming your application relies on Akonadi being operational + * of course. + * + * While the Akonadi server automatically started by Akonadi::Session + * on first use, it might be necessary for some use-cases to guarantee + * a running Akonadi service at some point. This can be done using + * start(). + * + * Example: + * + * @code + * + * if ( !Akonadi::Control::start() ) { + * qDebug() << "Unable to start Akonadi server, exit application"; + * return 1; + * } else { + * ... + * } + * + * @endcode + * + * @author Volker Krause + * + * @see Akonadi::ServerManager + */ +class AKONADICORE_EXPORT Control : public QObject +{ + Q_OBJECT + +public: + /** + * Destroys the control object. + */ + ~Control(); + + /** + * Starts the Akonadi server synchronously if it is not already running. + * @return @c true if the server was started successfully or was already + * running, @c false otherwise + */ + static bool start(); + + /** + * Stops the Akonadi server synchronously if it is currently running. + * @return @c true if the server was shutdown successfully or was + * not running at all, @c false otherwise. + * @since 4.2 + */ + static bool stop(); + + /** + * Restarts the Akonadi server synchronously. + * @return @c true if the restart was successful, @c false otherwise, + * the server state is undefined in this case. + * @since 4.2 + */ + static bool restart(); + +protected: + /** + * Creates the control object. + */ + Control(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/differencesalgorithminterface.h b/src/core/differencesalgorithminterface.h new file mode 100644 index 0000000..8d504c5 --- /dev/null +++ b/src/core/differencesalgorithminterface.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2010 KDAB + SPDX-FileContributor: Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +namespace Akonadi +{ +class AbstractDifferencesReporter; +class Item; + +/** + * @short An interface to find out differences between two Akonadi objects. + * + * @author Tobias Koenig + * @since 4.6 + */ +class DifferencesAlgorithmInterface +{ +public: + /** + * Destroys the differences algorithm interface. + */ + virtual ~DifferencesAlgorithmInterface() + { + } + + /** + * Calculates the differences between two Akonadi objects and reports + * them to a reporter object. + * + * @param reporter The reporter object that will be used for reporting the differences. + * @param leftItem The left-hand side item that will be compared. + * @param rightItem The right-hand side item that will be compared. + */ + virtual void compare(AbstractDifferencesReporter *reporter, const Akonadi::Item &leftItem, const Akonadi::Item &rightItem) = 0; +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::DifferencesAlgorithmInterface, "org.freedesktop.Akonadi.DifferencesAlgorithmInterface/1.0") + diff --git a/src/core/entitycache.cpp b/src/core/entitycache.cpp new file mode 100644 index 0000000..1bb039d --- /dev/null +++ b/src/core/entitycache.cpp @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitycache_p.h" + +using namespace Akonadi; + +EntityCacheBase::EntityCacheBase(Session *_session, QObject *parent) + : QObject(parent) + , session(_session) +{ +} + +void EntityCacheBase::setSession(Session *_session) +{ + session = _session; +} + +#include "moc_entitycache_p.cpp" diff --git a/src/core/entitycache_p.h b/src/core/entitycache_p.h new file mode 100644 index 0000000..f2252e8 --- /dev/null +++ b/src/core/entitycache_p.h @@ -0,0 +1,520 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collection.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "session.h" +#include "tag.h" +#include "tagfetchjob.h" +#include "tagfetchscope.h" + +#include "akonaditests_export.h" + +#include +#include +#include +#include + +class KJob; + +Q_DECLARE_METATYPE(QList) + +namespace Akonadi +{ +/** + @internal + QObject part of EntityCache. +*/ +class AKONADI_TESTS_EXPORT EntityCacheBase : public QObject +{ + Q_OBJECT +public: + explicit EntityCacheBase(Session *session, QObject *parent = nullptr); + + void setSession(Session *session); + +protected: + Session *session = nullptr; + +Q_SIGNALS: + void dataAvailable(); + +private Q_SLOTS: + virtual void processResult(KJob *job) = 0; +}; + +template struct EntityCacheNode { + EntityCacheNode() + : pending(false) + , invalid(false) + { + } + EntityCacheNode(typename T::Id id) + : entity(T(id)) + , pending(true) + , invalid(false) + { + } + T entity; + bool pending; + bool invalid; +}; + +/** + * @internal + * A in-memory FIFO cache for a small amount of Item or Collection objects. + */ +template class EntityCache : public EntityCacheBase +{ +public: + using FetchScope = FetchScope_; + explicit EntityCache(int maxCapacity, Session *session = nullptr, QObject *parent = nullptr) + : EntityCacheBase(session, parent) + , mCapacity(maxCapacity) + { + } + + ~EntityCache() override + { + qDeleteAll(mCache); + } + + /** Object is available in the cache and can be retrieved. */ + bool isCached(typename T::Id id) const + { + EntityCacheNode *node = cacheNodeForId(id); + return node && !node->pending; + } + + /** Object has been requested but is not yet loaded into the cache or is already available. */ + bool isRequested(typename T::Id id) const + { + return cacheNodeForId(id); + } + + /** Returns the cached object if available, an empty instance otherwise. */ + virtual T retrieve(typename T::Id id) const + { + EntityCacheNode *node = cacheNodeForId(id); + if (node && !node->pending && !node->invalid) { + return node->entity; + } + return T(); + } + + /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ + void invalidate(typename T::Id id) + { + EntityCacheNode *node = cacheNodeForId(id); + if (node) { + node->invalid = true; + } + } + + /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ + void update(typename T::Id id, const FetchScope &scope) + { + EntityCacheNode *node = cacheNodeForId(id); + if (node) { + mCache.removeAll(node); + if (node->pending) { + request(id, scope); + } + delete node; + } + } + + /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ + virtual bool ensureCached(typename T::Id id, const FetchScope &scope) + { + EntityCacheNode *node = cacheNodeForId(id); + if (!node) { + request(id, scope); + return false; + } + return !node->pending; + } + + /** + Asks the cache to retrieve @p id. @p request is used as + a token to indicate which request has been finished in the + dataAvailable() signal. + */ + virtual void request(typename T::Id id, const FetchScope &scope) + { + Q_ASSERT(!isRequested(id)); + shrinkCache(); + auto node = new EntityCacheNode(id); + FetchJob *job = createFetchJob(id, scope); + job->setProperty("EntityCacheNode", QVariant::fromValue(id)); + connect(job, SIGNAL(result(KJob *)), SLOT(processResult(KJob *))); + mCache.enqueue(node); + } + +private: + EntityCacheNode *cacheNodeForId(typename T::Id id) const + { + for (typename QQueue *>::const_iterator it = mCache.constBegin(), endIt = mCache.constEnd(); it != endIt; ++it) { + if ((*it)->entity.id() == id) { + return *it; + } + } + return nullptr; + } + + void processResult(KJob *job) override + { + if (job->error()) { + // This can happen if we have stale notifications for items that have already been removed + } + auto id = job->property("EntityCacheNode").template value(); + EntityCacheNode *node = cacheNodeForId(id); + if (!node) { + return; // got replaced in the meantime + } + + node->pending = false; + extractResult(node, job); + // make sure we find this node again if something went wrong here, + // most likely the object got deleted from the server in the meantime + if (node->entity.id() != id) { + // TODO: Recursion guard? If this is called with non-existing ids, the if will never be true! + node->entity.setId(id); + node->invalid = true; + } + Q_EMIT dataAvailable(); + } + + void extractResult(EntityCacheNode *node, KJob *job) const; + + inline FetchJob *createFetchJob(typename T::Id id, const FetchScope &scope) + { + auto fetch = new FetchJob(T(id), session); + fetch->setFetchScope(scope); + return fetch; + } + + /** Tries to reduce the cache size until at least one more object fits in. */ + void shrinkCache() + { + while (mCache.size() >= mCapacity && !mCache.first()->pending) { + delete mCache.dequeue(); + } + } + +private: + QQueue *> mCache; + int mCapacity; +}; + +template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const +{ + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->collections().isEmpty()) { + node->entity = Collection(); + } else { + node->entity = fetch->collections().at(0); + } +} + +template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const +{ + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->items().isEmpty()) { + node->entity = Item(); + } else { + node->entity = fetch->items().at(0); + } +} + +template<> inline void EntityCache::extractResult(EntityCacheNode *node, KJob *job) const +{ + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->tags().isEmpty()) { + node->entity = Tag(); + } else { + node->entity = fetch->tags().at(0); + } +} + +template<> +inline CollectionFetchJob *EntityCache::createFetchJob(Collection::Id id, + const CollectionFetchScope &scope) +{ + auto fetch = new CollectionFetchJob(Collection(id), CollectionFetchJob::Base, session); + fetch->setFetchScope(scope); + return fetch; +} + +using CollectionCache = EntityCache; +using ItemCache = EntityCache; +using TagCache = EntityCache; + +template struct EntityListCacheNode { + EntityListCacheNode() + : pending(false) + , invalid(false) + { + } + EntityListCacheNode(typename T::Id id) + : entity(id) + , pending(true) + , invalid(false) + { + } + + T entity; + bool pending; + bool invalid; +}; + +template class EntityListCache : public EntityCacheBase +{ +public: + using FetchScope = FetchScope_; + + explicit EntityListCache(int maxCapacity, Session *session = nullptr, QObject *parent = nullptr) + : EntityCacheBase(session, parent) + , mCapacity(maxCapacity) + { + } + + ~EntityListCache() override + { + qDeleteAll(mCache); + } + + /** Returns the cached object if available, an empty instance otherwise. */ + typename T::List retrieve(const QList &ids) const + { + typename T::List list; + + for (typename T::Id id : ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node || node->pending || node->invalid) { + return typename T::List(); + } + + list << node->entity; + } + + return list; + } + + /** Requests the object to be cached if it is not yet in the cache. @returns @c true if it was in the cache already. */ + bool ensureCached(const QList &ids, const FetchScope &scope) + { + QList toRequest; + bool result = true; + + for (typename T::Id id : ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node) { + toRequest << id; + continue; + } + + if (node->pending) { + result = false; + } + } + + if (!toRequest.isEmpty()) { + request(toRequest, scope, ids); + return false; + } + + return result; + } + + /** Marks the cache entry as invalid, use in case the object has been deleted on the server. */ + void invalidate(const QList &ids) + { + for (typename T::Id id : ids) { + EntityListCacheNode *node = mCache.value(id); + if (node) { + node->invalid = true; + } + } + } + + /** Triggers a re-fetching of a cache entry, use if it has changed on the server. */ + void update(const QList &ids, const FetchScope &scope) + { + QList toRequest; + + for (typename T::Id id : ids) { + EntityListCacheNode *node = mCache.value(id); + if (node) { + mCache.remove(id); + if (node->pending) { + toRequest << id; + } + delete node; + } + } + + if (!toRequest.isEmpty()) { + request(toRequest, scope); + } + } + + /** + Asks the cache to retrieve @p id. @p request is used as + a token to indicate which request has been finished in the + dataAvailable() signal. + */ + void request(const QList &ids, const FetchScope &scope, const QList &preserveIds = QList()) + { + Q_ASSERT(isNotRequested(ids)); + shrinkCache(preserveIds); + for (typename T::Id id : ids) { + auto node = new EntityListCacheNode(id); + mCache.insert(id, node); + } + FetchJob *job = createFetchJob(ids, scope); + job->setProperty("EntityListCacheIds", QVariant::fromValue>(ids)); + connect(job, SIGNAL(result(KJob *)), SLOT(processResult(KJob *))); + } + + bool isNotRequested(const QList &ids) const + { + for (typename T::Id id : ids) { + if (mCache.contains(id)) { + return false; + } + } + + return true; + } + + /** Object is available in the cache and can be retrieved. */ + bool isCached(const QList &ids) const + { + for (typename T::Id id : ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node || node->pending) { + return false; + } + } + return true; + } + +private: + /** Tries to reduce the cache size until at least one more object fits in. */ + void shrinkCache(const QList &preserveIds) + { + typename QHash *>::Iterator iter = mCache.begin(); + while (iter != mCache.end() && mCache.size() >= mCapacity) { + if (iter.value()->pending || preserveIds.contains(iter.key())) { + ++iter; + continue; + } + + delete iter.value(); + iter = mCache.erase(iter); + } + } + + inline FetchJob *createFetchJob(const QList &ids, const FetchScope &scope) + { + auto job = new FetchJob(ids, session); + job->setFetchScope(scope); + return job; + } + + void processResult(KJob *job) override + { + if (job->error()) { + qWarning() << job->errorString(); + } + const auto ids = job->property("EntityListCacheIds").value>(); + + typename T::List entities; + extractResults(job, entities); + + for (typename T::Id id : ids) { + EntityListCacheNode *node = mCache.value(id); + if (!node) { + continue; // got replaced in the meantime + } + + node->pending = false; + + T result; + typename T::List::Iterator iter = entities.begin(); + for (; iter != entities.end(); ++iter) { + if ((*iter).id() == id) { + result = *iter; + entities.erase(iter); + break; + } + } + + // make sure we find this node again if something went wrong here, + // most likely the object got deleted from the server in the meantime + if (!result.isValid()) { + node->entity = T(id); + node->invalid = true; + } else { + node->entity = result; + } + } + + Q_EMIT dataAvailable(); + } + + void extractResults(KJob *job, typename T::List &entities) const; + +private: + QHash *> mCache; + int mCapacity; +}; + +template<> inline void EntityListCache::extractResults(KJob *job, Collection::List &collections) const +{ + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + collections = fetch->collections(); +} + +template<> inline void EntityListCache::extractResults(KJob *job, Item::List &items) const +{ + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + items = fetch->items(); +} + +template<> inline void EntityListCache::extractResults(KJob *job, Tag::List &tags) const +{ + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + tags = fetch->tags(); +} + +template<> +inline CollectionFetchJob *EntityListCache::createFetchJob(const QList &ids, + const CollectionFetchScope &scope) +{ + auto fetch = new CollectionFetchJob(ids, CollectionFetchJob::Base, session); + fetch->setFetchScope(scope); + return fetch; +} + +using CollectionListCache = EntityListCache; +using ItemListCache = EntityListCache; +using TagListCache = EntityListCache; +} + diff --git a/src/core/exception.cpp b/src/core/exception.cpp new file mode 100644 index 0000000..a2196f2 --- /dev/null +++ b/src/core/exception.cpp @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "exceptionbase.h" + +#include + +#include + +using namespace Akonadi; + +class Exception::Private +{ +public: + explicit Private(const QByteArray &what) + : what(what) + { + } + + QByteArray what; + QByteArray assembledWhat; +}; + +Exception::Exception(const char *what) +{ + try { + d = std::make_unique(what); + } catch (...) { + } +} + +Exception::Exception(const QByteArray &what) +{ + try { + d = std::make_unique(what); + } catch (...) { + } +} + +Exception::Exception(const QString &what) +{ + try { + d = std::make_unique(what.toUtf8()); + } catch (...) { + } +} + +Exception::Exception(Exception &&) noexcept = default; + +Exception::~Exception() = default; + +QByteArray Exception::type() const +{ + static constexpr char mytype[] = "Akonadi::Exception"; + try { + return QByteArray::fromRawData("Akonadi::Exception", sizeof(mytype) - 1); + } catch (...) { + return QByteArray(); + } +} + +const char *Exception::what() const noexcept +{ + static constexpr char fallback[] = ""; + if (!d) { + return fallback; + } + if (d->assembledWhat.isEmpty()) { + try { + d->assembledWhat = QByteArray(type() + ": " + d->what); + } catch (...) { + return "caught some exception while assembling Akonadi::Exception::what() return value"; + } + } + return d->assembledWhat.constData(); +} + +#define AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE(classname) \ + Akonadi::classname::~classname() = default; \ + QByteArray Akonadi::classname::type() const \ + { \ + static constexpr char mytype[] = "Akonadi::" #classname; \ + try { \ + return QByteArray::fromRawData(mytype, sizeof(mytype) - 1); \ + } catch (...) { \ + return QByteArray(); \ + } \ + } + +AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE(PayloadException) + +#undef AKONADI_EXCEPTION_IMPLEMENT_TRIVIAL_INSTANCE diff --git a/src/core/exceptionbase.h b/src/core/exceptionbase.h new file mode 100644 index 0000000..24dda28 --- /dev/null +++ b/src/core/exceptionbase.h @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +#include +#include + +class QString; + +namespace Akonadi +{ +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4275) // we are exporting a subclass of an unexported class, MSVC complains +#endif + +/** + Base class for exceptions used by the Akonadi library. +*/ +class AKONADICORE_EXPORT Exception : public std::exception +{ +public: + /** + Creates a new exception with the error message @p what. + */ + explicit Exception(const char *what); + + /** + Creates a new exception with the error message @p what. + */ + explicit Exception(const QByteArray &what); + + /** + Creates a new exception with the error message @p what. + */ + explicit Exception(const QString &what); + + Exception(Exception &&) noexcept; + + /** + Destructor. + */ + ~Exception() override; + + /** + Returns the error message associated with this exception. + */ + const char *what() const noexcept override; + + /** + Returns the type of this exception. + */ + virtual QByteArray type() const; // ### Akonadi 2: return const char * + +private: + class Private; + std::unique_ptr d; +}; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#define AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE(classname) \ + class AKONADICORE_EXPORT classname : public Akonadi::Exception \ + { \ + public: \ + explicit classname(const char *what) \ + : Akonadi::Exception(what) \ + { \ + } \ + explicit classname(const QByteArray &what) \ + : Akonadi::Exception(what) \ + { \ + } \ + explicit classname(const QString &what) \ + : Akonadi::Exception(what) \ + { \ + } \ + classname(classname &&) = default; \ + ~classname() override; \ + QByteArray type() const override; \ + } + +AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE(PayloadException); + +#undef AKONADI_EXCEPTION_MAKE_TRIVIAL_INSTANCE + +} + diff --git a/src/core/firstrun.cpp b/src/core/firstrun.cpp new file mode 100644 index 0000000..81d3e90 --- /dev/null +++ b/src/core/firstrun.cpp @@ -0,0 +1,212 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "firstrun_p.h" +#include "servermanager.h" +#include + +#include "agentinstance.h" +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "agenttype.h" +#include + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static const char FIRSTRUN_DBUSLOCK[] = "org.kde.Akonadi.Firstrun.lock"; + +using namespace Akonadi; + +Firstrun::Firstrun(QObject *parent) + : QObject(parent) + , mConfig(new KConfig(ServerManager::addNamespace(QStringLiteral("akonadi-firstrunrc")))) +{ + // The code in firstrun is not safe in multi-instance mode + Q_ASSERT(!ServerManager::hasInstanceIdentifier()); + if (ServerManager::hasInstanceIdentifier()) { + deleteLater(); + return; + } + if (QDBusConnection::sessionBus().registerService(QLatin1String(FIRSTRUN_DBUSLOCK))) { + findPendingDefaults(); + qCDebug(AKONADICORE_LOG) << "D-Bus lock acquired, pending defaults:" << mPendingDefaults; + setupNext(); + } else { + qCDebug(AKONADICORE_LOG) << "D-Bus lock found, so someone else does the work for us already."; + deleteLater(); + } +} + +Firstrun::~Firstrun() +{ + if (qApp) { + QDBusConnection::sessionBus().unregisterService(QLatin1String(FIRSTRUN_DBUSLOCK)); + } + delete mConfig; + qCDebug(AKONADICORE_LOG) << "done"; +} + +void Firstrun::findPendingDefaults() +{ + const KConfigGroup cfg(mConfig, "ProcessedDefaults"); + const auto paths = StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/firstrun")); + for (const QString &dirName : paths) { + const QStringList files = QDir(dirName).entryList(QDir::Files | QDir::Readable); + for (const QString &fileName : files) { + const QString fullName = dirName + QLatin1Char('/') + fileName; + KConfig c(fullName); + const QString id = KConfigGroup(&c, "Agent").readEntry("Id", QString()); + if (id.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Found invalid default configuration in " << fullName; + continue; + } + if (cfg.hasKey(id)) { + continue; + } + mPendingDefaults << fullName; + } + } +} + +void Firstrun::setupNext() +{ + delete mCurrentDefault; + mCurrentDefault = nullptr; + + if (mPendingDefaults.isEmpty()) { + deleteLater(); + return; + } + + mCurrentDefault = new KConfig(mPendingDefaults.takeFirst()); + const KConfigGroup agentCfg = KConfigGroup(mCurrentDefault, "Agent"); + + AgentType type = AgentManager::self()->type(agentCfg.readEntry("Type", QString())); + if (!type.isValid()) { + qCCritical(AKONADICORE_LOG) << "Unable to obtain agent type for default resource agent configuration " << mCurrentDefault->name(); + setupNext(); + return; + } + if (type.capabilities().contains(QLatin1String("Unique"))) { + const Akonadi::AgentInstance::List lstAgents = AgentManager::self()->instances(); + for (const AgentInstance &agent : lstAgents) { + if (agent.type() == type) { + // remember we set this one up already + KConfigGroup cfg(mConfig, "ProcessedDefaults"); + cfg.writeEntry(agentCfg.readEntry("Id", QString()), agent.identifier()); + cfg.sync(); + setupNext(); + return; + } + } + } + + auto job = new AgentInstanceCreateJob(type); + connect(job, &AgentInstanceCreateJob::result, this, &Firstrun::instanceCreated); + job->start(); +} + +void Firstrun::instanceCreated(KJob *job) +{ + Q_ASSERT(mCurrentDefault); + + if (job->error()) { + qCCritical(AKONADICORE_LOG) << "Creating agent instance failed for " << mCurrentDefault->name(); + setupNext(); + return; + } + + AgentInstance instance = static_cast(job)->instance(); + const KConfigGroup agentCfg = KConfigGroup(mCurrentDefault, "Agent"); + const QString agentName = agentCfg.readEntry("Name", QString()); + if (!agentName.isEmpty()) { + instance.setName(agentName); + } + + const auto service = ServerManager::agentServiceName(ServerManager::Agent, instance.identifier()); + auto iface = new QDBusInterface(service, QStringLiteral("/Settings"), QString(), QDBusConnection::sessionBus(), this); + if (!iface->isValid()) { + qCCritical(AKONADICORE_LOG) << "Unable to obtain the KConfigXT D-Bus interface of " << instance.identifier(); + setupNext(); + delete iface; + return; + } + // agent specific settings, using the D-Bus <-> KConfigXT bridge + const KConfigGroup settings = KConfigGroup(mCurrentDefault, "Settings"); + + const QStringList lstSettings = settings.keyList(); + for (const QString &setting : lstSettings) { + qCDebug(AKONADICORE_LOG) << "Setting up " << setting << " for agent " << instance.identifier(); + const QString methodName = QStringLiteral("set%1").arg(setting); + const QVariant::Type argType = argumentType(iface->metaObject(), methodName); + if (argType == QVariant::Invalid) { + qCCritical(AKONADICORE_LOG) << "Setting " << setting << " not found in agent configuration interface of " << instance.identifier(); + continue; + } + + QVariant arg; + if (argType == QVariant::String) { + // Since a string could be a path we always use readPathEntry here, + // that shouldn't harm any normal string settings + arg = settings.readPathEntry(setting, QString()); + } else { + arg = settings.readEntry(setting, QVariant(argType)); + } + + const QDBusReply reply = iface->call(methodName, arg); + if (!reply.isValid()) { + qCCritical(AKONADICORE_LOG) << "Setting " << setting << " failed for agent " << instance.identifier(); + } + } + + iface->call(QStringLiteral("save")); + + instance.reconfigure(); + instance.synchronize(); + delete iface; + + // remember we set this one up already + KConfigGroup cfg(mConfig, "ProcessedDefaults"); + cfg.writeEntry(agentCfg.readEntry("Id", QString()), instance.identifier()); + cfg.sync(); + + setupNext(); +} + +QVariant::Type Firstrun::argumentType(const QMetaObject *mo, const QString &method) +{ + QMetaMethod m; + for (int i = 0; i < mo->methodCount(); ++i) { + const QString signature = QString::fromLatin1(mo->method(i).methodSignature()); + if (signature.startsWith(method)) { + m = mo->method(i); + } + } + + if (m.methodSignature().isEmpty()) { + return QVariant::Invalid; + } + + const QList argTypes = m.parameterTypes(); + if (argTypes.count() != 1) { + return QVariant::Invalid; + } + + return QVariant::nameToType(argTypes.first().constData()); +} + +#include "moc_firstrun_p.cpp" diff --git a/src/core/firstrun_p.h b/src/core/firstrun_p.h new file mode 100644 index 0000000..4739a38 --- /dev/null +++ b/src/core/firstrun_p.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class KConfig; +class KJob; +struct QMetaObject; + +namespace Akonadi +{ +/** + Takes care of setting up default resource agents when running Akonadi for the first time. + +

Defining your own default agent setups

+ + To add an additional agent to the default Akonadi setup, add a file with the + agent setup description into /akonadi/firstrun. + + Such a file looks as follows: + + @verbatim + [Agent] + Id=defaultaddressbook + Type=akonadi_vcard_resource + Name=My Addressbook + + [Settings] + Path[$e]=~/.kde/share/apps/kabc/std.ics + AutosaveInterval=1 + @endverbatim + + The keys in the [Agent] group are mandatory: +
    +
  • Id: A unique identifier of the setup description, should never change to avoid the agent + being set up twice.
  • +
  • Type: The agent type
  • +
  • Name: The user visible name for this agent (only used for resource agents currently)
  • +
+ + The [Settings] group is optional and contains agent-dependent settings. + For those settings to be applied, the agent needs to export its settings + via D-Bus using the KConfigXT <-> D-Bus bridge. +*/ +class Firstrun : public QObject +{ + Q_OBJECT +public: + explicit Firstrun(QObject *parent = nullptr); + ~Firstrun(); + +private: + void findPendingDefaults(); + void setupNext(); + static QVariant::Type argumentType(const QMetaObject *mo, const QString &method); + +private Q_SLOTS: + void instanceCreated(KJob *job); + +private: + QStringList mPendingDefaults; + KConfig *const mConfig; + KConfig *mCurrentDefault = nullptr; +}; + +} + diff --git a/src/core/gidextractor.cpp b/src/core/gidextractor.cpp new file mode 100644 index 0000000..b731ad2 --- /dev/null +++ b/src/core/gidextractor.cpp @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "gidextractor_p.h" +#include "gidextractorinterface.h" + +#include "item.h" +#include "typepluginloader_p.h" + +using namespace Akonadi; + +QString GidExtractor::extractGid(const Item &item) +{ + const QObject *object = TypePluginLoader::objectForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + if (object) { + const GidExtractorInterface *extractor = qobject_cast(object); + if (extractor) { + return extractor->extractGid(item); + } + } + return QString(); +} + +QString GidExtractor::getGid(const Item &item) +{ + const QString gid = item.gid(); + if (!gid.isNull()) { + return gid; + } + if (item.loadedPayloadParts().isEmpty()) { + return QString(); + } + return extractGid(item); +} diff --git a/src/core/gidextractor_p.h b/src/core/gidextractor_p.h new file mode 100644 index 0000000..9309c0a --- /dev/null +++ b/src/core/gidextractor_p.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +class Item; + +/** + * @internal + * Extracts the GID of an object contained in an akonadi item using a plugin that implements the GidExtractorInterface. + */ +class GidExtractor +{ +public: + /** + * Extracts the GID from @p item. using an extractor plugin. + */ + static QString extractGid(const Item &item); + + /** + * Extracts the gid from @p item. + * + * If the item has a GID set, that GID will be returned. + * If the item has no GID set, and the item has a payload, the GID is extracted using extractGid(). + * If the item has no GID set and no payload, a default constructed QString is returned. + */ + static QString getGid(const Item &item); +}; + +} + diff --git a/src/core/gidextractorinterface.h b/src/core/gidextractorinterface.h new file mode 100644 index 0000000..722af04 --- /dev/null +++ b/src/core/gidextractorinterface.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2013 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +class Item; + +/** + * @short An interface to extract the GID of an object contained in an akonadi item. + * + * @author Christian Mollekopf + * @since 4.11 + */ +class GidExtractorInterface +{ +public: + /** + * Destructor. + */ + virtual ~GidExtractorInterface() + { + } + /** + * Extracts the globally unique id of @p item + * + * If you want to clear the gid from the database return QString(""). + */ + virtual QString extractGid(const Item &item) const = 0; + +protected: + explicit GidExtractorInterface() = default; + +private: + Q_DISABLE_COPY_MOVE(GidExtractorInterface) +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::GidExtractorInterface, "org.freedesktop.Akonadi.GidExtractorInterface/1.0") + diff --git a/src/core/item.cpp b/src/core/item.cpp new file mode 100644 index 0000000..4e68730 --- /dev/null +++ b/src/core/item.cpp @@ -0,0 +1,547 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "item.h" +#include "akonadicore_debug.h" +#include "item_p.h" +#include "itemserializer_p.h" +#include "private/protocol_p.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +Q_GLOBAL_STATIC(Akonadi::Collection, s_defaultParentCollection) // NOLINT(readability-redundant-member-init) + +uint Akonadi::qHash(const Akonadi::Item &item) +{ + return ::qHash(item.id()); +} + +// Change to something != RFC822 as soon as the server supports it +const char Item::FullPayload[] = "RFC822"; + +Item::Item() + : d_ptr(new ItemPrivate) +{ +} + +Item::Item(Id id) + : d_ptr(new ItemPrivate(id)) +{ +} + +Item::Item(const QString &mimeType) + : d_ptr(new ItemPrivate) +{ + d_ptr->mMimeType = mimeType; +} + +Item::Item(const Item &other) = default; + +Item::Item(Item &&other) noexcept = default; + +Item::~Item() = default; + +void Item::setId(Item::Id identifier) +{ + d_ptr->mId = identifier; +} + +Item::Id Item::id() const +{ + return d_ptr->mId; +} + +void Item::setRemoteId(const QString &id) +{ + d_ptr->mRemoteId = id; +} + +QString Item::remoteId() const +{ + return d_ptr->mRemoteId; +} + +void Item::setRemoteRevision(const QString &revision) +{ + d_ptr->mRemoteRevision = revision; +} + +QString Item::remoteRevision() const +{ + return d_ptr->mRemoteRevision; +} + +bool Item::isValid() const +{ + return (d_ptr->mId >= 0); +} + +bool Item::operator==(const Item &other) const +{ + // Invalid collections are the same, no matter what their internal ID is + return (!isValid() && !other.isValid()) || (d_ptr->mId == other.d_ptr->mId); +} + +bool Akonadi::Item::operator!=(const Item &other) const +{ + return (isValid() || other.isValid()) && (d_ptr->mId != other.d_ptr->mId); +} + +Item &Item ::operator=(const Item &other) +{ + if (this != &other) { + d_ptr = other.d_ptr; + } + + return *this; +} + +bool Akonadi::Item::operator<(const Item &other) const +{ + return d_ptr->mId < other.d_ptr->mId; +} + +void Item::addAttribute(Attribute *attr) +{ + ItemChangeLog::instance()->attributeStorage(d_ptr).addAttribute(attr); +} + +void Item::removeAttribute(const QByteArray &type) +{ + ItemChangeLog::instance()->attributeStorage(d_ptr).removeAttribute(type); +} + +bool Item::hasAttribute(const QByteArray &type) const +{ + return ItemChangeLog::instance()->attributeStorage(d_ptr).hasAttribute(type); +} + +Attribute::List Item::attributes() const +{ + return ItemChangeLog::instance()->attributeStorage(d_ptr).attributes(); +} + +void Akonadi::Item::clearAttributes() +{ + ItemChangeLog::instance()->attributeStorage(d_ptr).clearAttributes(); +} + +Attribute *Item::attribute(const QByteArray &type) +{ + return ItemChangeLog::instance()->attributeStorage(d_ptr).attribute(type); +} + +const Attribute *Item::attribute(const QByteArray &type) const +{ + return ItemChangeLog::instance()->attributeStorage(d_ptr).attribute(type); +} + +Collection &Item::parentCollection() +{ + if (!d_ptr->mParent) { + d_ptr->mParent.reset(new Collection()); + } + return *(d_ptr->mParent); +} + +Collection Item::parentCollection() const +{ + if (!d_ptr->mParent) { + return *(s_defaultParentCollection); + } else { + return *(d_ptr->mParent); + } +} + +void Item::setParentCollection(const Collection &parent) +{ + d_ptr->mParent.reset(new Collection(parent)); +} + +Item::Flags Item::flags() const +{ + return d_ptr->mFlags; +} + +void Item::setFlag(const QByteArray &name) +{ + d_ptr->mFlags.insert(name); + if (!d_ptr->mFlagsOverwritten) { + Item::Flags &deletedFlags = ItemChangeLog::instance()->deletedFlags(d_ptr); + auto iter = deletedFlags.find(name); + if (iter != deletedFlags.end()) { + deletedFlags.erase(iter); + } else { + ItemChangeLog::instance()->addedFlags(d_ptr).insert(name); + } + } +} + +void Item::clearFlag(const QByteArray &name) +{ + d_ptr->mFlags.remove(name); + if (!d_ptr->mFlagsOverwritten) { + Item::Flags &addedFlags = ItemChangeLog::instance()->addedFlags(d_ptr); + auto iter = addedFlags.find(name); + if (iter != addedFlags.end()) { + addedFlags.erase(iter); + } else { + ItemChangeLog::instance()->deletedFlags(d_ptr).insert(name); + } + } +} + +void Item::setFlags(const Flags &flags) +{ + d_ptr->mFlags = flags; + d_ptr->mFlagsOverwritten = true; +} + +void Item::clearFlags() +{ + d_ptr->mFlags.clear(); + d_ptr->mFlagsOverwritten = true; +} + +QDateTime Item::modificationTime() const +{ + return d_ptr->mModificationTime; +} + +void Item::setModificationTime(const QDateTime &datetime) +{ + d_ptr->mModificationTime = datetime; +} + +bool Item::hasFlag(const QByteArray &name) const +{ + return d_ptr->mFlags.contains(name); +} + +void Item::setTags(const Tag::List &list) +{ + d_ptr->mTags = list; + d_ptr->mTagsOverwritten = true; +} + +void Item::setTag(const Tag &tag) +{ + d_ptr->mTags << tag; + if (!d_ptr->mTagsOverwritten) { + Tag::List &deletedTags = ItemChangeLog::instance()->deletedTags(d_ptr); + if (deletedTags.contains(tag)) { + deletedTags.removeOne(tag); + } else { + ItemChangeLog::instance()->addedTags(d_ptr).push_back(tag); + } + } +} + +void Item::clearTags() +{ + d_ptr->mTags.clear(); + d_ptr->mTagsOverwritten = true; +} + +void Item::clearTag(const Tag &tag) +{ + d_ptr->mTags.removeOne(tag); + if (!d_ptr->mTagsOverwritten) { + Tag::List &addedTags = ItemChangeLog::instance()->addedTags(d_ptr); + if (addedTags.contains(tag)) { + addedTags.removeOne(tag); + } else { + ItemChangeLog::instance()->deletedTags(d_ptr).push_back(tag); + } + } +} + +bool Item::hasTag(const Tag &tag) const +{ + return d_ptr->mTags.contains(tag); +} + +Tag::List Item::tags() const +{ + return d_ptr->mTags; +} + +Relation::List Item::relations() const +{ + return d_ptr->mRelations; +} + +QSet Item::loadedPayloadParts() const +{ + return ItemSerializer::parts(*this); +} + +QByteArray Item::payloadData() const +{ + int version = 0; + QByteArray data; + ItemSerializer::serialize(*this, FullPayload, data, version); + return data; +} + +void Item::setPayloadFromData(const QByteArray &data) +{ + ItemSerializer::deserialize(*this, FullPayload, data, 0, ItemSerializer::Internal); +} + +void Item::clearPayload() +{ + d_ptr->mClearPayload = true; +} + +int Item::revision() const +{ + return d_ptr->mRevision; +} + +void Item::setRevision(int rev) +{ + d_ptr->mRevision = rev; +} + +Collection::Id Item::storageCollectionId() const +{ + return d_ptr->mCollectionId; +} + +void Item::setStorageCollectionId(Collection::Id collectionId) +{ + d_ptr->mCollectionId = collectionId; +} + +QString Item::mimeType() const +{ + return d_ptr->mMimeType; +} + +void Item::setSize(qint64 size) +{ + d_ptr->mSize = size; + d_ptr->mSizeChanged = true; +} + +qint64 Item::size() const +{ + return d_ptr->mSize; +} + +void Item::setMimeType(const QString &mimeType) +{ + d_ptr->mMimeType = mimeType; +} + +void Item::setGid(const QString &id) +{ + d_ptr->mGid = id; +} + +QString Item::gid() const +{ + return d_ptr->mGid; +} + +void Item::setVirtualReferences(const Collection::List &collections) +{ + d_ptr->mVirtualReferences = collections; +} + +Collection::List Item::virtualReferences() const +{ + return d_ptr->mVirtualReferences; +} + +bool Item::hasPayload() const +{ + return d_ptr->hasMetaTypeId(-1); +} + +QUrl Item::url(UrlType type) const +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("item"), QString::number(id())); + if (type == UrlWithMimeType) { + query.addQueryItem(QStringLiteral("type"), mimeType()); + } + + QUrl url; + url.setScheme(QStringLiteral("akonadi")); + url.setQuery(query); + return url; +} + +Item Item::fromUrl(const QUrl &url) +{ + if (url.scheme() != QLatin1String("akonadi")) { + return Item(); + } + + const QString itemStr = QUrlQuery(url).queryItemValue(QStringLiteral("item")); + bool ok = false; + Item::Id itemId = itemStr.toLongLong(&ok); + if (!ok) { + return Item(); + } + + return Item(itemId); +} + +Internal::PayloadBase *Item::payloadBaseV2(int spid, int mtid) const +{ + return d_ptr->payloadBaseImpl(spid, mtid); +} + +bool Item::ensureMetaTypeId(int mtid) const +{ + // 0. Nothing there - nothing to convert from, either + if (d_ptr->mPayloads.empty()) { + return false; + } + + // 1. Look whether we already have one: + if (d_ptr->hasMetaTypeId(mtid)) { + return true; + } + + // recursion detection (shouldn't trigger, but does if the + // serialiser plugins are acting funky): + if (d_ptr->mConversionInProgress) { + return false; + } + + // 2. Try to create one by conversion from a different representation: + try { + const QScopedValueRollback guard(d_ptr->mConversionInProgress, true); + Item converted = ItemSerializer::convert(*this, mtid); + return d_ptr->movePayloadFrom(converted.d_ptr, mtid); + } catch (const std::exception &e) { + qCWarning(AKONADICORE_LOG) << "Item payload conversion threw:" << e.what(); + return false; + } catch (...) { + qCCritical(AKONADICORE_LOG, "conversion threw something not derived from std::exception: fix the program!"); + return false; + } +} + +static QString format_type(int spid, int mtid) +{ + return QStringLiteral("sp(%1)<%2>").arg(spid).arg(QLatin1String(QMetaType::typeName(mtid))); +} + +static QString format_types(const PayloadContainer &c) +{ + QStringList result; + result.reserve(c.size()); + for (auto it = c.begin(), end = c.end(); it != end; ++it) { + result.push_back(format_type(it->sharedPointerId, it->metaTypeId)); + } + return result.join(QLatin1String(", ")); +} + +void Item::throwPayloadException(int spid, int mtid) const +{ + if (d_ptr->mPayloads.empty()) { + qCDebug(AKONADICORE_LOG) << "Throwing PayloadException: No payload set"; + throw PayloadException("No payload set"); + } else { + qCDebug(AKONADICORE_LOG) << "Throwing PayloadException: Wrong payload type (requested:" << format_type(spid, mtid) + << "; present: " << format_types(d_ptr->mPayloads) << "), item mime type is" << mimeType(); + throw PayloadException(QStringLiteral("Wrong payload type (requested: %1; present: %2)").arg(format_type(spid, mtid), format_types(d_ptr->mPayloads))); + } +} + +void Item::setPayloadBaseV2(int spid, int mtid, std::unique_ptr &p) +{ + d_ptr->setPayloadBaseImpl(spid, mtid, p, false); +} + +void Item::addPayloadBaseVariant(int spid, int mtid, std::unique_ptr &p) const +{ + d_ptr->setPayloadBaseImpl(spid, mtid, p, true); +} + +QSet Item::cachedPayloadParts() const +{ + return d_ptr->mCachedPayloadParts; +} + +void Item::setCachedPayloadParts(const QSet &cachedParts) +{ + d_ptr->mCachedPayloadParts = cachedParts; +} + +QSet Item::availablePayloadParts() const +{ + return ItemSerializer::availableParts(*this); +} + +QVector Item::availablePayloadMetaTypeIds() const +{ + QVector result; + result.reserve(d_ptr->mPayloads.size()); + // Stable Insertion Sort - N is typically _very_ low (1 or 2). + for (auto it = d_ptr->mPayloads.begin(), end = d_ptr->mPayloads.end(); it != end; ++it) { + result.insert(std::upper_bound(result.begin(), result.end(), it->metaTypeId), it->metaTypeId); + } + return result; +} + +void Item::setPayloadPath(const QString &filePath) +{ + // Load payload from the external file, so that it's accessible via + // Item::payload(). It internally calls setPayload(), which will clear + // mPayloadPath, so we call it afterwards + ItemSerializer::deserialize(*this, "RFC822", filePath.toUtf8(), 0, ItemSerializer::Foreign); + d_ptr->mPayloadPath = filePath; +} + +QString Item::payloadPath() const +{ + return d_ptr->mPayloadPath; +} + +void Item::apply(const Item &other) +{ + if (mimeType() != other.mimeType() || id() != other.id()) { + qCDebug(AKONADICORE_LOG) << "mimeType() = " << mimeType() << "; other.mimeType() = " << other.mimeType(); + qCDebug(AKONADICORE_LOG) << "id() = " << id() << "; other.id() = " << other.id(); + Q_ASSERT_X(false, "Item::apply", "mimetype or id missmatch"); + } + + setRemoteId(other.remoteId()); + setRevision(other.revision()); + setRemoteRevision(other.remoteRevision()); + setFlags(other.flags()); + setTags(other.tags()); + setModificationTime(other.modificationTime()); + setSize(other.size()); + setParentCollection(other.parentCollection()); + setStorageCollectionId(other.storageCollectionId()); + + ItemChangeLog *changelog = ItemChangeLog::instance(); + changelog->attributeStorage(d_ptr) = changelog->attributeStorage(other.d_ptr); + + ItemSerializer::apply(*this, other); + d_ptr->resetChangeLog(); + + // Must happen after payload update + d_ptr->mPayloadPath = other.payloadPath(); +} diff --git a/src/core/item.h b/src/core/item.h new file mode 100644 index 0000000..7c2c6cb --- /dev/null +++ b/src/core/item.h @@ -0,0 +1,937 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + 2007 Till Adam + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" +#include "collection.h" +#include "exceptionbase.h" +#include "itempayloadinternals_p.h" +#include "job.h" +#include "relation.h" +#include "tag.h" + +#include +#include +#include + +#include +#include +#include + +class QUrl; + +template class QVector; + +namespace Akonadi +{ +class ItemPrivate; + +/** + * @short Represents a PIM item stored in Akonadi storage. + * + * A PIM item consists of one or more parts, allowing a fine-grained access on its + * content where needed (eg. mail envelope, mail body and attachments). + * + * There is also a namespace (prefix) for special parts which are local to Akonadi. + * These parts, prefixed by "akonadi-", will never be fetched in the resource. + * They are useful for local extensions like agents which might want to add meta data + * to items in order to handle them but the meta data should not be stored back to the + * resource. + * + * This class is implicitly shared. + * + *

Payload

+ * + * This class contains, beside some type-agnostic information (flags, revision), + * zero or more payload objects representing its actual data. Which objects these actually + * are depends on the mimetype of the item and the corresponding serializer plugin(s). + * + * Technically the only restriction on payload objects is that they have to be copyable. + * For safety reasons, pointer payloads are forbidden as well though, as the + * ownership would not be clear. In this case, usage of a shared pointer is + * recommended (such as boost::shared_ptr, QSharedPointer or std::shared_ptr). + * + * Using a shared pointer is also required in case the payload is a polymorphic + * type. For supported shared pointer types implicit casting is provided when possible. + * + * When using a value-based class as payload, it is recommended to use one that does + * support implicit sharing as setting and retrieving a payload as well as copying + * an Akonadi::Item object imply copying of the payload object. + * + * Since KDE 4.6, Item supports multiple payload types per mime type, + * and will automatically convert between them using the serialiser + * plugins (which is slow). It also supports mixing shared pointer + * types, e.g. inserting a boost::shared_ptr and extracting a + * QSharedPointer. Since the two shared pointer types cannot + * share ownership of the same object, the payload class @c T needs to + * provide a @c clone() method with the usual signature, ie. + * + * @code + * virtual T * T::clone() const + * @endcode + * + * If the class that does not have a @c clone() method, asking for an + * incompatible shared pointer will throw a PayloadException. + * + * Since using different shared pointer types and different payload + * types for the same mimetype incurs slow conversions (between + * payload types) and cloning (between shared pointer types), as well + * as manifold memory usage (results of conversions are cached inside + * the Item, and only destroyed when a new payload is set by the user + * of the class), you want to restrict yourself to just one type and + * one shared pointer type. This mechanism was mainly introduced for + * backwards compatibility (e.g., putting in a + * boost::shared_ptr and extracting a + * QSharedPointer), so it is not optimized for + * performance. + * + * The availability of a payload of a specific type can be checked using hasPayload(), + * payloads can be retrieved by using payload() and set by using setPayload(). Refer + * to the documentation of those methods for more details. + * + * @author Volker Krause , Till Adam , Marc Mutz + */ +class AKONADICORE_EXPORT Item +{ +public: + /** + * Describes the unique id type. + */ + using Id = qint64; + + /** + * Describes a list of items. + */ + using List = QVector; + + /** + * Describes a flag name. + */ + using Flag = QByteArray; + + /** + * Describes a set of flag names. + */ + using Flags = QSet; + + /** + * Describes the part name that is used to fetch the + * full payload of an item. + */ + static const char FullPayload[]; + + /** + * Creates a new item. + */ + Item(); + + /** + * Creates a new item with the given unique @p id. + */ + explicit Item(Id id); + + /** + * Creates a new item with the given mime type. + * + * @param mimeType The mime type of the item. + */ + explicit Item(const QString &mimeType); + + /** + * Creates a new item from an @p other item. + */ + Item(const Item &other); + + /** + * Move constructor. + */ + Item(Item &&) noexcept; + + /** + * Destroys the item. + */ + ~Item(); + + /** + * Creates an item from the given @p url. + */ + static Item fromUrl(const QUrl &url); + + /** + * Sets the unique @p identifier of the item. + */ + void setId(Id identifier); + + /** + * Returns the unique identifier of the item. + */ + Id id() const; + + /** + * Sets the remote @p id of the item. + */ + void setRemoteId(const QString &id); + + /** + * Returns the remote id of the item. + */ + QString remoteId() const; + + /** + * Sets the remote @p revision of the item. + * @param revision the item's remote revision + * The remote revision can be used by resources to store some + * revision information of the backend to detect changes there. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + void setRemoteRevision(const QString &revision); + + /** + * Returns the remote revision of the item. + * + * @note This method is supposed to be used by resources only. + * @since 4.5 + */ + QString remoteRevision() const; + + /** + * Returns whether the item is valid. + */ + bool isValid() const; + + /** + * Returns whether this item's id equals the id of the @p other item. + */ + bool operator==(const Item &other) const; + + /** + * Returns whether the item's id does not equal the id of the @p other item. + */ + bool operator!=(const Item &other) const; + + /** + * Assigns the @p other to this item and returns a reference to this item. + * @param other the item to assign + */ + Item &operator=(const Item &other); + + /** + * @internal For use with containers only. + * + * @since 4.8 + */ + bool operator<(const Item &other) const; + + /** + * Returns the parent collection of this object. + * @note This will of course only return a useful value if it was explicitly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection parentCollection() const; + + /** + * Returns a reference to the parent collection of this object. + * @note This will of course only return a useful value if it was explicitly retrieved + * from the Akonadi server. + * @since 4.4 + */ + Collection &parentCollection(); + + /** + * Set the parent collection of this object. + * @note Calling this method has no immediate effect for the object itself, + * such as being moved to another collection. + * It is mainly relevant to provide a context for RID-based operations + * inside resources. + * @param parent The parent collection. + * @since 4.4 + */ + void setParentCollection(const Collection &parent); + + /** + * Adds an attribute to the item. + * + * If an attribute of the same type name already exists, it is deleted and + * replaced with the new one. + * + * @param attribute The new attribute. + * + * @note The collection takes the ownership of the attribute. + */ + void addAttribute(Attribute *attribute); + + /** + * Removes and deletes the attribute of the given type @p name. + */ + void removeAttribute(const QByteArray &name); + + /** + * Returns @c true if the item has an attribute of the given type @p name, + * false otherwise. + */ + bool hasAttribute(const QByteArray &name) const; + + /** + * Returns a list of all attributes of the item. + * + * @warning Do not modify the attributes returned from this method, + * the change will not be reflected when updating the Item through + * ItemModifyJob. + */ + Attribute::List attributes() const; + + /** + * Removes and deletes all attributes of the item. + */ + void clearAttributes(); + + /** + * Returns the attribute of the given type @p name if available, 0 otherwise. + */ + Attribute *attribute(const QByteArray &name); + const Attribute *attribute(const QByteArray &name) const; + + /** + * Describes the options that can be passed to access attributes. + */ + enum CreateOption { + AddIfMissing, ///< Creates the attribute if it is missing + DontCreate ///< Do not create the attribute if it is missing (default) + }; + + /** + * Returns the attribute of the requested type. + * If the item has no attribute of that type yet, a new one + * is created and added to the entity. + * + * @param option The create options. + */ + template inline T *attribute(CreateOption option = DontCreate); + + /** + * Returns the attribute of the requested type or 0 if it is not available. + */ + template inline const T *attribute() const; + + /** + * Removes and deletes the attribute of the requested type. + */ + template inline void removeAttribute(); + + /** + * Returns whether the item has an attribute of the requested type. + */ + template inline bool hasAttribute() const; + + /** + * Returns all flags of this item. + */ + Flags flags() const; + + /** + * Returns the timestamp of the last modification of this item. + * @since 4.2 + */ + QDateTime modificationTime() const; + + /** + * Sets the timestamp of the last modification of this item. + * @param datetime the modification time to set + * @note Do not modify this value from within an application, + * it is updated automatically by the revision checking functions. + * @since 4.2 + */ + void setModificationTime(const QDateTime &datetime); + + /** + * Returns whether the flag with the given @p name is + * set in the item. + */ + bool hasFlag(const QByteArray &name) const; + + /** + * Sets the flag with the given @p name in the item. + */ + void setFlag(const QByteArray &name); + + /** + * Removes the flag with the given @p name from the item. + */ + void clearFlag(const QByteArray &name); + + /** + * Overwrites all flags of the item by the given @p flags. + */ + void setFlags(const Flags &flags); + + /** + * Removes all flags from the item. + */ + void clearFlags(); + + void setTags(const Tag::List &list); + + void setTag(const Tag &tag); + + Tag::List tags() const; + + bool hasTag(const Tag &tag) const; + + void clearTag(const Tag &tag); + + void clearTags(); + + /** + * Returns all relations of this item. + * @since 4.15 + * @see RelationCreateJob, RelationDeleteJob to modify relations + */ + Relation::List relations() const; + + /** + * Sets the payload based on the canonical representation normally + * used for data of this mime type. + * + * @param data The encoded data. + * @see fullPayloadData + */ + void setPayloadFromData(const QByteArray &data); + + /** + * Returns the full payload in its canonical representation, e.g. the + * binary or textual format usually used for data with this mime type. + * This is useful when communicating with non-Akonadi application by + * e.g. drag&drop, copy&paste or stored files. + */ + QByteArray payloadData() const; + + /** + * Returns the list of loaded payload parts. This is not necessarily + * identical to all parts in the cache or to all available parts on the backend. + */ + QSet loadedPayloadParts() const; + + /** + * Marks that the payload shall be cleared from the cache when this + * item is passed to an ItemModifyJob the next time. + * This will trigger a refetch of the payload from the backend when the + * item is accessed afterwards. Only resources should have a need for + * this functionality. + * + * @since 4.5 + */ + void clearPayload(); + + /** + * Sets the @p revision number of the item. + * @param revision the revision number to set + * @note Do not modify this value from within an application, + * it is updated automatically by the revision checking functions. + */ + void setRevision(int revision); + + /** + * Returns the revision number of the item. + */ + int revision() const; + + /** + * Returns the unique identifier of the collection this item is stored in. There is only + * a single such collection, although the item can be linked into arbitrary many + * virtual collections. + * Calling this method makes sense only after running an ItemFetchJob on the item. + * @returns the collection ID if it is known, -1 otherwise. + * @since 4.3 + */ + Collection::Id storageCollectionId() const; + + /** + * Set the size of the item in bytes. + * @param size the size of the item in bytes + * @since 4.2 + */ + void setSize(qint64 size); + + /** + * Returns the size of the items in bytes. + * + * @since 4.2 + */ + qint64 size() const; + + /** + * Sets the mime type of the item to @p mimeType. + */ + void setMimeType(const QString &mimeType); + + /** + * Returns the mime type of the item. + */ + QString mimeType() const; + + /** + * Sets the @p gid of the entity. + * + * @since 4.12 + */ + void setGid(const QString &gid); + + /** + * Returns the gid of the entity. + * + * @since 4.12 + */ + QString gid() const; + + /** + * Sets the virtual @p collections that this item is linked into. + * + * @note Note that changing this value makes no effect on what collections + * this item is linked to. To link or unlink an item to/from a virtual + * collection, use LinkJob and UnlinkJob. + * + * @since 4.14 + */ + void setVirtualReferences(const Collection::List &collections); + + /** + * Lists virtual collections that this item is linked to. + * + * @note This value is populated only when this item was retrieved by + * ItemFetchJob with fetchVirtualReferences set to true in ItemFetchScope, + * otherwise this list is always empty. + * + * @since 4.14 + */ + Collection::List virtualReferences() const; + + /** + * Returns a list of metatype-ids, describing the different + * variants of payload that are currently contained in this item. + * + * The result is always sorted (increasing ids). + */ + QVector availablePayloadMetaTypeIds() const; + + /** + * Sets a path to a file with full payload. + * + * This method can only be used by Resources and should not be used by Akonadi + * clients. Clients should use setPayload() instead. + * + * Akonadi will not duplicate content of the file in its database but will + * instead directly refer to this file. This means that the file must be + * persistent (don't use this method with a temporary files), and the Akonadi + * resource that owns the storage is responsible for updating the file path + * if the file is changed, moved or removed. + * + * The payload can still be accessed via payload() methods. + * + * @see setPayload(), setPayloadFromData() + * @since 5.6 + */ + void setPayloadPath(const QString &filePath); + + /** + * Returns path to the payload file set by setPayloadPath() + * + * If payload was set via setPayload() or setPayloadFromData() then this + * method will return a null string. + */ + QString payloadPath() const; + + /** + * Sets the payload object of this PIM item. + * + * @param p The payload object. Must be copyable and must not be a pointer, + * will cause a compilation failure otherwise. Using a type that can be copied + * fast (such as implicitly shared classes) is recommended. + * If the payload type is polymorphic and you intend to set and retrieve payload + * objects with mismatching but castable types, make sure to use a supported + * shared pointer implementation (currently boost::shared_ptr, QSharedPointer + * and std::shared_ptr and make sure there is a specialization of + * Akonadi::super_trait for your class. + */ + template void setPayload(const T &p); + /// @cond PRIVATE + template void setPayload(T *p); + /// @endcond + + /** + * Returns the payload object of this PIM item. This method will only succeed if either + * you requested the exact same payload type that was put in or the payload uses a + * supported shared pointer type (currently boost::shared_ptr, QSharedPointer and + * std::shared_ptr), and is castable to the requested type. For this to work there needs + * to be a specialization of Akonadi::super_trait of the used classes. + * + * If a mismatching or non-castable payload type is requested, an Akonadi::PayloadException + * is thrown. Therefore it is generally recommended to guard calls to payload() with a + * corresponding hasPayload() call. + * + * Trying to retrieve a pointer type will fail to compile. + */ + template T payload() const; + + /** + * Returns whether the item has a payload object. + */ + bool hasPayload() const; + + /** + * Returns whether the item has a payload of type @c T. + * This method will only return @c true if either you requested the exact same payload type + * that was put in or the payload uses a supported shared pointer type (currently boost::shared_ptr, + * QSharedPointer and std::shared_ptr), and is castable to the requested type. For this to work there needs + * to be a specialization of Akonadi::super_trait of the used classes. + * + * Trying to retrieve a pointer type will fail to compile. + */ + template bool hasPayload() const; + + /** + * Describes the type of url which is returned in url(). + */ + enum UrlType { + UrlShort = 0, ///< A short url which contains the identifier only (default) + UrlWithMimeType = 1 ///< A url with identifier and mimetype + }; + + /** + * Returns the url of the item. + */ + QUrl url(UrlType type = UrlShort) const; + + /** + * Returns the parts available for this item. + * + * The returned set refers to parts available on the akonadi server or remotely, + * but does not include the loadedPayloadParts() of this item. + * + * @since 4.4 + */ + QSet availablePayloadParts() const; + + /** + * Returns the parts available for this item in the cache. The list might be a subset + * of the actual parts in cache, as it contains only the requested parts. See @see ItemFetchJob and + * @see ItemFetchScope + * + * The returned set refers to parts available on the akonadi server. + * + * @since 4.11 + */ + QSet cachedPayloadParts() const; + + /** + * Applies the parts of Item @p other to this item. + * Any parts or attributes available in other, will be applied to this item, + * and the payload parts of other will be inserted into this item, overwriting + * any existing parts with the same part name. + * + * If there is an ItemSerialzerPluginV2 for the type, the merge method in that plugin is + * used to perform the merge. If only an ItemSerialzerPlugin class is found, or the merge + * method of the -V2 plugin is not implemented, the merge is performed with multiple deserializations + * of the payload. + * @param other the item to get values from + * @since 4.4 + */ + void apply(const Item &other); + + void setCachedPayloadParts(const QSet &cachedParts); + +private: + /// @cond PRIVATE + friend class ItemCreateJob; + friend class ItemCreateJobPrivate; + friend class ItemModifyJob; + friend class ItemModifyJobPrivate; + friend class ItemSync; + friend class ProtocolHelper; + Internal::PayloadBase *payloadBaseV2(int sharedPointerId, int metaTypeId) const; + void setPayloadBaseV2(int sharedPointerId, int metaTypeId, std::unique_ptr &p); + void addPayloadBaseVariant(int sharedPointerId, int metaTypeId, std::unique_ptr &p) const; + + /** + * Try to ensure that we have a variant of the payload for metatype id @a mtid. + * @return @c true if a type exists or could be created through conversion, @c false otherwise. + */ + bool ensureMetaTypeId(int mtid) const; + + template + typename std::enable_if::isPolymorphic, void>::type setPayloadImpl(const T &p, const int * /*disambiguate*/ = nullptr); + template typename std::enable_if::isPolymorphic, void>::type setPayloadImpl(const T &p); + + template typename std::enable_if::isPolymorphic, T>::type payloadImpl(const int * /*disambiguate*/ = nullptr) const; + template typename std::enable_if::isPolymorphic, T>::type payloadImpl() const; + + template + typename std::enable_if::isPolymorphic, bool>::type hasPayloadImpl(const int * /*disambiguate*/ = nullptr) const; + template typename std::enable_if::isPolymorphic, bool>::type hasPayloadImpl() const; + + template + typename std::enable_if::value, bool>::type tryToClone(T *ret, const int * /*disambiguate*/ = nullptr) const; + template typename std::enable_if::value, bool>::type tryToClone(T *ret) const; + + template + typename std::enable_if::value, bool>::type tryToCloneImpl(T *ret, const int * /*disambiguate*/ = nullptr) const; + template typename std::enable_if::value, bool>::type tryToCloneImpl(T *ret) const; + + /** + * Set the collection ID to where the item is stored in. Should be set only by the ItemFetchJob. + * @param collectionId the unique identifier of the collection where this item is stored in. + * @since 4.3 + */ + void setStorageCollectionId(Collection::Id collectionId); + +#if 0 + /** + * Helper function for non-template throwing of PayloadException. + */ + QString payloadExceptionText(int spid, int mtid) const; + + /** + * Non-template throwing of PayloadException. + * Needs to be inline, otherwise catch (Akonadi::PayloadException) + * won't work (only catch (Akonadi::Exception)) + */ + inline void throwPayloadException(int spid, int mtid) const + { + throw PayloadException(payloadExceptionText(spid, mtid)); + } +#else + void throwPayloadException(int spid, int mtid) const; +#endif + + QSharedDataPointer d_ptr; + friend class ItemPrivate; + /// @endcond +}; + +AKONADICORE_EXPORT uint qHash(const Akonadi::Item &item); + +template inline T *Item::attribute(Item::CreateOption option) +{ + const QByteArray type = T().type(); + if (hasAttribute(type)) { + if (T *attr = dynamic_cast(attribute(type))) { + return attr; + } + qWarning() << "Found attribute of unknown type" << type << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } else if (option == AddIfMissing) { + T *attr = new T(); + addAttribute(attr); + return attr; + } + + return nullptr; +} + +template inline const T *Item::attribute() const +{ + const QByteArray type = T().type(); + if (hasAttribute(type)) { + if (const T *attr = dynamic_cast(attribute(type))) { + return attr; + } + qWarning() << "Found attribute of unknown type" << type << ". Did you forget to call AttributeFactory::registerAttribute()?"; + } + + return nullptr; +} + +template inline void Item::removeAttribute() +{ + removeAttribute(T().type()); +} + +template inline bool Item::hasAttribute() const +{ + return hasAttribute(T().type()); +} + +template T Item::payload() const +{ + static_assert(!std::is_pointer::value, "Payload must not be a pointer"); + + if (!hasPayload()) { + throwPayloadException(-1, -1); + } + + return payloadImpl(); +} + +template typename std::enable_if::isPolymorphic, T>::type Item::payloadImpl(const int *) const +{ + using PayloadType = Internal::PayloadTrait; + static_assert(PayloadType::isPolymorphic, "Non-polymorphic payload type in polymorphic implementation is not allowed"); + + using Root_T = typename Internal::get_hierarchy_root::type; + using RootType = Internal::PayloadTrait; + static_assert(!RootType::isPolymorphic, + "Root type of payload type must not be polymorphic"); // prevent endless recursion + + return PayloadType::castFrom(payloadImpl()); +} + +template typename std::enable_if::isPolymorphic, T>::type Item::payloadImpl() const +{ + using PayloadType = Internal::PayloadTrait; + static_assert(!PayloadType::isPolymorphic, "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + const int metaTypeId = PayloadType::elementMetaTypeId(); + + // make sure that we have a payload format represented by 'metaTypeId': + if (!ensureMetaTypeId(metaTypeId)) { + throwPayloadException(PayloadType::sharedPointerId, metaTypeId); + } + + // Check whether we have the exact payload + // (metatype id and shared pointer type match) + if (const Internal::Payload *const p = Internal::payload_cast(payloadBaseV2(PayloadType::sharedPointerId, metaTypeId))) { + return p->payload; + } + + T ret; + if (!tryToClone(&ret)) { + throwPayloadException(PayloadType::sharedPointerId, metaTypeId); + } + return ret; +} + +template typename std::enable_if::value, bool>::type Item::tryToCloneImpl(T *ret, const int *) const +{ + using PayloadType = Internal::PayloadTrait; + using NewPayloadType = Internal::PayloadTrait; + + const int metaTypeId = PayloadType::elementMetaTypeId(); + Internal::PayloadBase *payloadBase = payloadBaseV2(NewPayloadType::sharedPointerId, metaTypeId); + if (const Internal::Payload *const p = Internal::payload_cast(payloadBase)) { + // If found, attempt to make a clone (required the payload to provide virtual T * T::clone() const) + const T nt = PayloadType::clone(p->payload); + if (!PayloadType::isNull(nt)) { + // if clone succeeded, add the clone to the Item: + std::unique_ptr npb(new Internal::Payload(nt)); + addPayloadBaseVariant(PayloadType::sharedPointerId, metaTypeId, npb); + // and return it + if (ret) { + *ret = nt; + } + return true; + } + } + + return tryToCloneImpl::next_shared_ptr>(ret); +} + +template typename std::enable_if::value, bool>::type Item::tryToCloneImpl(T *) const +{ + return false; +} + +template typename std::enable_if::value, bool>::type Item::tryToClone(T *ret, const int *) const +{ + using PayloadType = Internal::PayloadTrait; + static_assert(!PayloadType::isPolymorphic, "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + return tryToCloneImpl::next_shared_ptr>(ret); +} + +template typename std::enable_if::value, bool>::type Item::tryToClone(T *) const +{ + using PayloadType = Internal::PayloadTrait; + static_assert(!PayloadType::isPolymorphic, "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + return false; +} + +template bool Item::hasPayload() const +{ + static_assert(!std::is_pointer::value, "Payload type cannot be a pointer"); + return hasPayload() && hasPayloadImpl(); +} + +template typename std::enable_if::isPolymorphic, bool>::type Item::hasPayloadImpl(const int *) const +{ + using PayloadType = Internal::PayloadTrait; + static_assert(PayloadType::isPolymorphic, "Non-polymorphic payload type in polymorphic implementation is no allowed"); + + using Root_T = typename Internal::get_hierarchy_root::type; + using RootType = Internal::PayloadTrait; + static_assert(!RootType::isPolymorphic, + "Root type of payload type must not be polymorphic"); // prevent endless recursion + + try { + return hasPayloadImpl() && PayloadType::canCastFrom(payload()); + } catch (const Akonadi::PayloadException &e) { + qDebug() << e.what(); + Q_UNUSED(e) + return false; + } +} + +template typename std::enable_if::isPolymorphic, bool>::type Item::hasPayloadImpl() const +{ + using PayloadType = Internal::PayloadTrait; + static_assert(!PayloadType::isPolymorphic, "Polymorphic payload type in non-polymorphic implementation is not allowed"); + + const int metaTypeId = PayloadType::elementMetaTypeId(); + + // make sure that we have a payload format represented by 'metaTypeId': + if (!ensureMetaTypeId(metaTypeId)) { + return false; + } + + // Check whether we have the exact payload + // (metatype id and shared pointer type match) + if (const Internal::Payload *const p = Internal::payload_cast(payloadBaseV2(PayloadType::sharedPointerId, metaTypeId))) { + return true; + } + + return tryToClone(nullptr); +} + +template void Item::setPayload(const T &p) +{ + static_assert(!std::is_pointer::value, "Payload type must not be a pointer"); + setPayloadImpl(p); +} + +template typename std::enable_if::isPolymorphic>::type Item::setPayloadImpl(const T &p, const int *) +{ + using PayloadType = Internal::PayloadTrait; + static_assert(PayloadType::isPolymorphic, "Non-polymorphic payload type in polymorphic implementation is not allowed"); + + using Root_T = typename Internal::get_hierarchy_root::type; + using RootType = Internal::PayloadTrait; + static_assert(!RootType::isPolymorphic, + "Root type of payload type must not be polymorphic"); // prevent endless recursion + + setPayloadImpl(p); +} + +template typename std::enable_if::isPolymorphic>::type Item::setPayloadImpl(const T &p) +{ + using PayloadType = Internal::PayloadTrait; + std::unique_ptr pb(new Internal::Payload(p)); + setPayloadBaseV2(PayloadType::sharedPointerId, PayloadType::elementMetaTypeId(), pb); +} + +template void Item::setPayload(T *p) +{ + p->You_MUST_NOT_use_a_pointer_as_payload; +} + +} // namespace Akonadi + +Q_DECLARE_METATYPE(Akonadi::Item) +Q_DECLARE_METATYPE(Akonadi::Item::List) + diff --git a/src/core/item_p.h b/src/core/item_p.h new file mode 100644 index 0000000..061cf00 --- /dev/null +++ b/src/core/item_p.h @@ -0,0 +1,294 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "itemchangelog_p.h" +#include "itempayloadinternals_p.h" +#include "tag.h" + +#include +#include +#include +#include + +namespace Akonadi +{ +namespace _detail +{ +template class clone_ptr +{ + std::unique_ptr t; + +public: + explicit clone_ptr() = default; + explicit clone_ptr(T *t) + : t(t) + { + } + + clone_ptr(const clone_ptr &other) + : t(other.t ? other.t->clone() : nullptr) + { + } + + clone_ptr(clone_ptr &&) noexcept = default; + + ~clone_ptr() = default; + + clone_ptr &operator=(const clone_ptr &other) + { + if (this != &other) { + clone_ptr copy(other); + swap(copy); + } + return *this; + } + + clone_ptr &operator=(clone_ptr &&) noexcept = default; + + void swap(clone_ptr &other) + { + using std::swap; + swap(t, other.t); + } + + T *operator->() const + { + return get(); + } + + T &operator*() const + { + assert(get() != nullptr); + return *get(); + } + + T *get() const + { + return t.get(); + } + + T *release() + { + return t.release(); + } + + void reset(T *other = nullptr) + { + t.reset(other); + } + + explicit operator bool() const noexcept + { + return get() != nullptr; + } +}; + +template inline void swap(clone_ptr &lhs, clone_ptr &rhs) noexcept +{ + lhs.swap(rhs); +} + +struct TypedPayload { + clone_ptr payload; + int sharedPointerId; + int metaTypeId; +}; + +struct BySharedPointerAndMetaTypeID { + const int spid; + const int mtid; + BySharedPointerAndMetaTypeID(int spid, int mtid) + : spid(spid) + , mtid(mtid) + { + } + bool operator()(const TypedPayload &tp) const + { + return (mtid == -1 || mtid == tp.metaTypeId) && (spid == -1 || spid == tp.sharedPointerId); + } +}; + +} + +} // namespace Akonadi + +namespace std +{ +template<> inline void swap(Akonadi::_detail::TypedPayload &lhs, Akonadi::_detail::TypedPayload &rhs) noexcept +{ + lhs.payload.swap(rhs.payload); + swap(lhs.sharedPointerId, rhs.sharedPointerId); + swap(lhs.metaTypeId, rhs.metaTypeId); +} +} + +namespace Akonadi +{ +using PayloadContainer = std::vector<_detail::TypedPayload>; +} + +namespace QtPrivate +{ +// disable Q_FOREACH on PayloadContainer (b/c it likes to take copies and clone_ptr doesn't like that) +template<> class QForeachContainer +{ +}; +} + +namespace Akonadi +{ +/** + * @internal + */ +class ItemPrivate : public QSharedData +{ +public: + explicit ItemPrivate(Item::Id id = -1) + : QSharedData() + , mRevision(-1) + , mId(id) + , mPayloads() + , mCollectionId(-1) + , mSize(0) + , mModificationTime() + , mFlagsOverwritten(false) + , mTagsOverwritten(false) + , mSizeChanged(false) + , mClearPayload(false) + , mConversionInProgress(false) + { + } + + ItemPrivate(const ItemPrivate &other) + : QSharedData(other) + { + mId = other.mId; + mRemoteId = other.mRemoteId; + mRemoteRevision = other.mRemoteRevision; + mPayloadPath = other.mPayloadPath; + if (other.mParent) { + mParent.reset(new Collection(*(other.mParent))); + } + mFlags = other.mFlags; + mRevision = other.mRevision; + mTags = other.mTags; + mRelations = other.mRelations; + mSize = other.mSize; + mModificationTime = other.mModificationTime; + mMimeType = other.mMimeType; + mPayloads = other.mPayloads; + mFlagsOverwritten = other.mFlagsOverwritten; + mSizeChanged = other.mSizeChanged; + mCollectionId = other.mCollectionId; + mClearPayload = other.mClearPayload; + mVirtualReferences = other.mVirtualReferences; + mGid = other.mGid; + mCachedPayloadParts = other.mCachedPayloadParts; + mTagsOverwritten = other.mTagsOverwritten; + mConversionInProgress = false; + + ItemChangeLog *changelog = ItemChangeLog::instance(); + changelog->addedFlags(this) = changelog->addedFlags(&other); + changelog->deletedFlags(this) = changelog->deletedFlags(&other); + changelog->addedTags(this) = changelog->addedTags(&other); + changelog->deletedTags(this) = changelog->deletedTags(&other); + changelog->attributeStorage(this) = changelog->attributeStorage(&other); + } + + ~ItemPrivate() + { + ItemChangeLog::instance()->removeItem(this); + } + + void resetChangeLog() + { + mFlagsOverwritten = false; + mSizeChanged = false; + mTagsOverwritten = false; + ItemChangeLog::instance()->clearItemChangelog(this); + } + + bool hasMetaTypeId(int mtid) const + { + return std::any_of(mPayloads.cbegin(), mPayloads.cend(), _detail::BySharedPointerAndMetaTypeID(-1, mtid)); + } + + Internal::PayloadBase *payloadBaseImpl(int spid, int mtid) const + { + auto it = std::find_if(mPayloads.cbegin(), mPayloads.cend(), _detail::BySharedPointerAndMetaTypeID(spid, mtid)); + return it == mPayloads.cend() ? nullptr : it->payload.get(); + } + + bool movePayloadFrom(ItemPrivate *other, int mtid) const /*sic!*/ + { + assert(other); + const size_t oldSize = mPayloads.size(); + PayloadContainer &oPayloads = other->mPayloads; + const _detail::BySharedPointerAndMetaTypeID matcher(-1, mtid); + const size_t numMatching = std::count_if(oPayloads.begin(), oPayloads.end(), matcher); + mPayloads.resize(oldSize + numMatching); + using namespace std; // for swap() + for (auto dst = mPayloads.begin() + oldSize, src = oPayloads.begin(), end = oPayloads.end(); src != end; ++src) { + if (matcher(*src)) { + swap(*dst, *src); + ++dst; + } + } + return numMatching > 0; + } + + void setPayloadBaseImpl(int spid, int mtid, std::unique_ptr &p, bool add) const /*sic!*/ + { + if (!p.get()) { + if (!add) { + mPayloads.clear(); + } + return; + } + + // if !add, delete all payload variants + // (they're conversions of each other) + mPayloadPath.clear(); + mPayloads.resize(add ? mPayloads.size() + 1 : 1); + _detail::TypedPayload &tp = mPayloads.back(); + tp.payload.reset(p.release()); + tp.sharedPointerId = spid; + tp.metaTypeId = mtid; + } + + // Utilise the 4-bytes padding from QSharedData + int mRevision; + Item::Id mId; + QString mRemoteId; + QString mRemoteRevision; + mutable QString mPayloadPath; + mutable QScopedPointer mParent; + mutable PayloadContainer mPayloads; + Item::Flags mFlags; + Tag::List mTags; + Relation::List mRelations; + Item::Id mCollectionId; + Collection::List mVirtualReferences; + // TODO: Maybe just use uint? Would save us another 8 bytes after reordering + qint64 mSize; + QDateTime mModificationTime; + QString mMimeType; + QString mGid; + QSet mCachedPayloadParts; + bool mFlagsOverwritten : 1; + bool mTagsOverwritten : 1; + bool mSizeChanged : 1; + bool mClearPayload : 1; + mutable bool mConversionInProgress; + // 6 bytes padding here +}; + +} + diff --git a/src/core/itemchangelog.cpp b/src/core/itemchangelog.cpp new file mode 100644 index 0000000..cbb6b0b --- /dev/null +++ b/src/core/itemchangelog.cpp @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#include "itemchangelog_p.h" + +using namespace Akonadi; + +ItemChangeLog *ItemChangeLog::sInstance = nullptr; + +ItemChangeLog *ItemChangeLog::instance() +{ + if (!sInstance) { + sInstance = new ItemChangeLog; + } + return sInstance; +} + +ItemChangeLog::ItemChangeLog() +{ +} + +Item::Flags &ItemChangeLog::addedFlags(const ItemPrivate *priv) +{ + return m_addedFlags[const_cast(priv)]; +} + +Item::Flags &ItemChangeLog::deletedFlags(const ItemPrivate *priv) +{ + return m_deletedFlags[const_cast(priv)]; +} + +Tag::List &ItemChangeLog::addedTags(const ItemPrivate *priv) +{ + return m_addedTags[const_cast(priv)]; +} + +Tag::List &ItemChangeLog::deletedTags(const ItemPrivate *priv) +{ + return m_deletedTags[const_cast(priv)]; +} + +AttributeStorage &ItemChangeLog::attributeStorage(ItemPrivate *priv) +{ + return m_attributeStorage[priv]; +} + +const AttributeStorage &ItemChangeLog::attributeStorage(const ItemPrivate *priv) +{ + return m_attributeStorage[const_cast(priv)]; +} + +void ItemChangeLog::removeItem(const ItemPrivate *priv) +{ + auto p = const_cast(priv); + m_addedFlags.remove(p); + m_deletedFlags.remove(p); + m_addedTags.remove(p); + m_deletedTags.remove(p); + m_attributeStorage.remove(p); +} + +void ItemChangeLog::clearItemChangelog(const ItemPrivate *priv) +{ + auto p = const_cast(priv); + m_addedFlags.remove(p); + m_deletedFlags.remove(p); + m_addedTags.remove(p); + m_deletedTags.remove(p); + m_attributeStorage[p].resetChangeLog(); // keep the attributes +} diff --git a/src/core/itemchangelog_p.h b/src/core/itemchangelog_p.h new file mode 100644 index 0000000..aa96052 --- /dev/null +++ b/src/core/itemchangelog_p.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + * + */ + +#pragma once + +#include "item.h" + +#include "akonaditests_export.h" +#include "attributestorage_p.h" + +namespace Akonadi +{ +class AKONADI_TESTS_EXPORT ItemChangeLog +{ +public: + static ItemChangeLog *instance(); + + Item::Flags &addedFlags(const ItemPrivate *priv); + Item::Flags &deletedFlags(const ItemPrivate *priv); + + Tag::List &addedTags(const ItemPrivate *priv); + Tag::List &deletedTags(const ItemPrivate *priv); + + const AttributeStorage &attributeStorage(const ItemPrivate *priv); + AttributeStorage &attributeStorage(ItemPrivate *priv); + + void removeItem(const ItemPrivate *priv); + void clearItemChangelog(const ItemPrivate *priv); + +private: + explicit ItemChangeLog(); + + static ItemChangeLog *sInstance; + + QHash m_addedFlags; + QHash m_deletedFlags; + QHash m_addedTags; + QHash m_deletedTags; + QHash m_attributeStorage; +}; + +} // namespace Akonadi + diff --git a/src/core/itemfetchscope.cpp b/src/core/itemfetchscope.cpp new file mode 100644 index 0000000..17bf282 --- /dev/null +++ b/src/core/itemfetchscope.cpp @@ -0,0 +1,218 @@ +/* + SPDX-FileCopyrightText: 2008 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemfetchscope.h" + +#include "itemfetchscope_p.h" + +using namespace Akonadi; + +ItemFetchScope::ItemFetchScope() +{ + d = new ItemFetchScopePrivate(); +} + +ItemFetchScope::ItemFetchScope(const ItemFetchScope &other) + : d(other.d) +{ +} + +ItemFetchScope::~ItemFetchScope() +{ +} + +ItemFetchScope &ItemFetchScope::operator=(const ItemFetchScope &other) +{ + if (&other != this) { + d = other.d; + } + + return *this; +} + +QSet ItemFetchScope::payloadParts() const +{ + return d->mPayloadParts; +} + +void ItemFetchScope::fetchPayloadPart(const QByteArray &part, bool fetch) +{ + if (fetch) { + d->mPayloadParts.insert(part); + } else { + d->mPayloadParts.remove(part); + } +} + +bool ItemFetchScope::fullPayload() const +{ + return d->mFullPayload; +} + +void ItemFetchScope::fetchFullPayload(bool fetch) +{ + d->mFullPayload = fetch; +} + +QSet ItemFetchScope::attributes() const +{ + return d->mAttributes; +} + +void ItemFetchScope::fetchAttribute(const QByteArray &type, bool fetch) +{ + if (fetch) { + d->mAttributes.insert(type); + } else { + d->mAttributes.remove(type); + } +} + +bool ItemFetchScope::allAttributes() const +{ + return d->mAllAttributes; +} + +void ItemFetchScope::fetchAllAttributes(bool fetch) +{ + d->mAllAttributes = fetch; +} + +bool ItemFetchScope::isEmpty() const +{ + return d->mPayloadParts.isEmpty() && d->mAttributes.isEmpty() && !d->mFullPayload && !d->mAllAttributes && !d->mCacheOnly + && !d->mCheckCachedPayloadPartsOnly && d->mFetchMtime // true by deafult -> false = non-empty + && !d->mIgnoreRetrievalErrors && d->mFetchRid // true by default + && !d->mFetchGid && !d->mFetchTags && !d->mFetchVRefs && !d->mFetchRelations && d->mAncestorDepth == AncestorRetrieval::None; +} + +bool ItemFetchScope::cacheOnly() const +{ + return d->mCacheOnly; +} + +void ItemFetchScope::setCacheOnly(bool cacheOnly) +{ + d->mCacheOnly = cacheOnly; +} + +void ItemFetchScope::setCheckForCachedPayloadPartsOnly(bool check) +{ + if (check) { + setCacheOnly(true); + } + d->mCheckCachedPayloadPartsOnly = check; +} + +bool ItemFetchScope::checkForCachedPayloadPartsOnly() const +{ + return d->mCheckCachedPayloadPartsOnly; +} + +ItemFetchScope::AncestorRetrieval ItemFetchScope::ancestorRetrieval() const +{ + return d->mAncestorDepth; +} + +void ItemFetchScope::setAncestorRetrieval(AncestorRetrieval depth) +{ + d->mAncestorDepth = depth; +} + +void ItemFetchScope::setFetchModificationTime(bool retrieveMtime) +{ + d->mFetchMtime = retrieveMtime; +} + +bool ItemFetchScope::fetchModificationTime() const +{ + return d->mFetchMtime; +} + +void ItemFetchScope::setFetchGid(bool retrieveGid) +{ + d->mFetchGid = retrieveGid; +} + +bool ItemFetchScope::fetchGid() const +{ + return d->mFetchGid; +} + +void ItemFetchScope::setIgnoreRetrievalErrors(bool ignore) +{ + d->mIgnoreRetrievalErrors = ignore; +} + +bool ItemFetchScope::ignoreRetrievalErrors() const +{ + return d->mIgnoreRetrievalErrors; +} + +void ItemFetchScope::setFetchChangedSince(const QDateTime &changedSince) +{ + d->mChangedSince = changedSince; +} + +QDateTime ItemFetchScope::fetchChangedSince() const +{ + return d->mChangedSince; +} + +void ItemFetchScope::setFetchRemoteIdentification(bool retrieveRid) +{ + d->mFetchRid = retrieveRid; +} + +bool ItemFetchScope::fetchRemoteIdentification() const +{ + return d->mFetchRid; +} + +void ItemFetchScope::setFetchTags(bool fetchTags) +{ + d->mFetchTags = fetchTags; +} + +bool ItemFetchScope::fetchTags() const +{ + return d->mFetchTags; +} + +void ItemFetchScope::setTagFetchScope(const TagFetchScope &tagFetchScope) +{ + d->mTagFetchScope = tagFetchScope; +} + +TagFetchScope &ItemFetchScope::tagFetchScope() +{ + return d->mTagFetchScope; +} + +TagFetchScope ItemFetchScope::tagFetchScope() const +{ + return d->mTagFetchScope; +} + +void ItemFetchScope::setFetchVirtualReferences(bool fetchVRefs) +{ + d->mFetchVRefs = fetchVRefs; +} + +bool ItemFetchScope::fetchVirtualReferences() const +{ + return d->mFetchVRefs; +} + +void ItemFetchScope::setFetchRelations(bool fetchRelations) +{ + d->mFetchRelations = fetchRelations; +} + +bool ItemFetchScope::fetchRelations() const +{ + return d->mFetchRelations; +} diff --git a/src/core/itemfetchscope.h b/src/core/itemfetchscope.h new file mode 100644 index 0000000..02da5ab --- /dev/null +++ b/src/core/itemfetchscope.h @@ -0,0 +1,422 @@ +/* + SPDX-FileCopyrightText: 2008 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include +#include +#include + +template class QSet; + +namespace Akonadi +{ +class ItemFetchScopePrivate; +class TagFetchScope; + +/** + * @short Specifies which parts of an item should be fetched from the Akonadi storage. + * + * When items are fetched from server either by using ItemFetchJob explicitly or + * when it is being used internally by other classes, e.g. ItemModel, the scope + * of the fetch operation can be tailored to the application's current needs. + * + * There are two supported ways of changing the currently active ItemFetchScope + * of classes: + * - in-place: modify the ItemFetchScope object the other class holds as a member + * - replace: replace the other class' member with a new scope object + * + * Example: modifying an ItemFetchJob's scope @c in-place + * @code + * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( collection ); + * job->fetchScope().fetchFullPayload(); + * job->fetchScope().fetchAttribute(); + * @endcode + * + * Example: @c replacing an ItemFetchJob's scope + * @code + * Akonadi::ItemFetchScope scope; + * scope.fetchFullPayload(); + * scope.fetchAttribute(); + * + * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob( collection ); + * job->setFetchScope( scope ); + * @endcode + * + * This class is implicitly shared. + * + * @author Kevin Krammer + */ +class AKONADICORE_EXPORT ItemFetchScope +{ +public: + /** + * Describes the ancestor retrieval depth. + * @since 4.4 + */ + enum AncestorRetrieval { + None, ///< No ancestor retrieval at all (the default) + Parent, ///< Only retrieve the immediate parent collection + All ///< Retrieve all ancestors, up to Collection::root() + }; + + /** + * Creates an empty item fetch scope. + * + * Using an empty scope will only fetch the very basic meta data of items, + * e.g. local id, remote id and mime type + */ + ItemFetchScope(); + + /** + * Creates a new item fetch scope from an @p other. + */ + ItemFetchScope(const ItemFetchScope &other); + + /** + * Destroys the item fetch scope. + */ + ~ItemFetchScope(); + + /** + * Assigns the @p other to this scope and returns a reference to this scope. + */ + ItemFetchScope &operator=(const ItemFetchScope &other); + + /** + * Returns the payload parts that should be fetched. + * + * @see fetchPayloadPart() + */ + Q_REQUIRED_RESULT QSet payloadParts() const; + + /** + * Sets which payload parts shall be fetched. + * + * @param part The payload part identifier. + * Valid values depend on the item type. + * @param fetch @c true to fetch this part, @c false otherwise. + */ + void fetchPayloadPart(const QByteArray &part, bool fetch = true); + + /** + * Returns whether the full payload should be fetched. + * + * @see fetchFullPayload() + */ + Q_REQUIRED_RESULT bool fullPayload() const; + + /** + * Sets whether the full payload shall be fetched. + * The default is @c false. + * + * @param fetch @c true if the full payload should be fetched, @c false otherwise. + */ + void fetchFullPayload(bool fetch = true); + + /** + * Returns all explicitly fetched attributes. + * + * Undefined if fetchAllAttributes() returns true. + * + * @see fetchAttribute() + */ + Q_REQUIRED_RESULT QSet attributes() const; + + /** + * Sets whether the attribute of the given @p type should be fetched. + * + * @param type The attribute type to fetch. + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + void fetchAttribute(const QByteArray &type, bool fetch = true); + + /** + * Sets whether the attribute of the requested type should be fetched. + * + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + template inline void fetchAttribute(bool fetch = true) + { + T dummy; + fetchAttribute(dummy.type(), fetch); + } + + /** + * Returns whether all available attributes should be fetched. + * + * @see fetchAllAttributes() + */ + Q_REQUIRED_RESULT bool allAttributes() const; + + /** + * Sets whether all available attributes should be fetched. + * The default is @c false. + * + * @param fetch @c true if all available attributes should be fetched, @c false otherwise. + */ + void fetchAllAttributes(bool fetch = true); + + /** + * Returns whether payload data should be requested from remote sources or just + * from the local cache. + * + * @see setCacheOnly() + */ + Q_REQUIRED_RESULT bool cacheOnly() const; + + /** + * Sets whether payload data should be requested from remote sources or just + * from the local cache. + * + * @param cacheOnly @c true if no remote data should be requested, + * @c false otherwise (the default). + */ + void setCacheOnly(bool cacheOnly); + + /** + * Sets whether payload will be fetched or there will be only a test performed if the + * requested payload is in the cache. Calling it calls @see setCacheOnly with true automatically. + * Default is fetching the data. + * + * @since 4.11 + */ + void setCheckForCachedPayloadPartsOnly(bool check = true); + + /** + * Returns whether payload data should be fetched or only checked for presence in the cache. + * + * @see setCheckForCachedPayloadPartsOnly() + * + * @since 4.11 + */ + Q_REQUIRED_RESULT bool checkForCachedPayloadPartsOnly() const; + + /** + * Sets how many levels of ancestor collections should be included in the retrieval. + * The default is AncestorRetrieval::None. + * + * @param ancestorDepth The desired ancestor retrieval depth. + * @since 4.4 + */ + void setAncestorRetrieval(AncestorRetrieval ancestorDepth); + + /** + * Returns the ancestor retrieval depth. + * + * @see setAncestorRetrieval() + * @since 4.4 + */ + Q_REQUIRED_RESULT AncestorRetrieval ancestorRetrieval() const; + + /** + * Enables retrieval of the item modification time. + * This is enabled by default for backward compatibility reasons. + * + * @param retrieveMtime @c true to retrieve the modification time, @c false otherwise + * @since 4.6 + */ + void setFetchModificationTime(bool retrieveMtime); + + /** + * Returns whether item modification time should be retrieved. + * + * @see setFetchModificationTime() + * @since 4.6 + */ + Q_REQUIRED_RESULT bool fetchModificationTime() const; + + /** + * Enables retrieval of the item GID. + * This is disabled by default. + * + * @param retrieveGID @c true to retrieve the GID, @c false otherwise + * @since 4.12 + */ + void setFetchGid(bool retrieveGID); + + /** + * Returns whether item GID should be retrieved. + * + * @see setFetchGid() + * @since 4.12 + */ + Q_REQUIRED_RESULT bool fetchGid() const; + + /** + * Ignore retrieval errors while fetching items, and always deliver what is available. + * If items have missing parts and the part can't be retrieved from the resource (i.e. because the system is offline), + * the fetch job would normally just fail. By setting this flag, the errors are ignored, + * and all items which could be fetched completely are returned. + * Note that all items that are returned are completely fetched, and incomplete items are simply ignored. + * This flag is useful for displaying everything that is available, where it is not crucial to have all items. + * Never use this for things like data migration or alike. + * + * @since 4.10 + */ + void setIgnoreRetrievalErrors(bool enabled); + + /** + * Returns whether retrieval errors should be ignored. + * + * @see setIgnoreRetrievalErrors() + * @since 4.10 + */ + Q_REQUIRED_RESULT bool ignoreRetrievalErrors() const; + + /** + * Returns @c true if there is nothing to fetch. + */ + Q_REQUIRED_RESULT bool isEmpty() const; + + /** + * Only fetch items that were added or modified after given timestamp + * + * When this property is set, all results are filtered, i.e. even when you + * request an item with a specific ID, it will not be fetched unless it was + * modified after @p changedSince timestamp. + * + * @param changedSince The timestamp of oldest modified item to fetch + * @since 4.11 + */ + void setFetchChangedSince(const QDateTime &changedSince); + + /** + * Returns timestamp of the oldest item to fetch. + */ + Q_REQUIRED_RESULT QDateTime fetchChangedSince() const; + + /** + * Fetch remote identification for items. + * + * These include Akonadi::Item::remoteId() and Akonadi::Item::remoteRevision(). This should + * be off for normal clients usually, to save memory (not to mention normal clients should + * not be concerned with these information anyway). It is however crucial for resource agents. + * For backward compatibility the default is @c true. + * + * @param retrieveRid whether or not to load remote identification. + * @since 4.12 + */ + void setFetchRemoteIdentification(bool retrieveRid); + + /** + * Returns whether item remote identification should be retrieved. + * + * @see setFetchRemoteIdentification() + * @since 4.12 + */ + Q_REQUIRED_RESULT bool fetchRemoteIdentification() const; + + /** + * Fetch tags for items. + * + * The fetched tags have only the Tag::id() set and need to be fetched first to access further attributes. + * + * The default is @c false. + * + * @param fetchTags whether or not to load tags. + * @since 4.13 + */ + void setFetchTags(bool fetchTags); + + /** + * Returns whether tags should be retrieved. + * + * @see setFetchTags() + * @since 4.13 + */ + Q_REQUIRED_RESULT bool fetchTags() const; + + /** + * Sets the tag fetch scope. + * + * The TagFetchScope controls how much of an tags's data is fetched + * from the server. + * + * By default setFetchIdOnly is set to true on the tag fetch scope. + * + * @param fetchScope The new fetch scope for tag fetch operations. + * @see fetchScope() + * @since 4.15 + */ + void setTagFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the TagFetchScope documentation + * for an example. + * + * By default setFetchIdOnly is set to true on the tag fetch scope. + * + * @return a reference to the current tag fetch scope + * + * @see setFetchScope() for replacing the current tag fetch scope + * @since 4.15 + */ + TagFetchScope &tagFetchScope(); + + /** + * Returns the tag fetch scope. + * + * By default setFetchIdOnly is set to true on the tag fetch scope. + * + * @return a reference to the current tag fetch scope + * + * @see setFetchScope() for replacing the current tag fetch scope + * @since 4.15 + */ + Q_REQUIRED_RESULT TagFetchScope tagFetchScope() const; + + /** + * Returns whether to fetch list of virtual collections the item is linked to + * + * @param fetchVRefs whether or not to fetch virtualc references + * @since 4.14 + */ + void setFetchVirtualReferences(bool fetchVRefs); + + /** + * Returns whether virtual references should be retrieved. + * + * @see setFetchVirtualReferences() + * @since 4.14 + */ + Q_REQUIRED_RESULT bool fetchVirtualReferences() const; + + /** + * Fetch relations for items. + * + * The default is @c false. + * + * @param fetchRelations whether or not to load relations. + * @since 4.15 + */ + void setFetchRelations(bool fetchRelations); + + /** + * Returns whether relations should be retrieved. + * + * @see setFetchRelations() + * @since 4.15 + */ + Q_REQUIRED_RESULT bool fetchRelations() const; + +private: + /// @cond PRIVATE + QSharedDataPointer d; + /// @endcond +}; + +} + +Q_DECLARE_METATYPE(Akonadi::ItemFetchScope) + diff --git a/src/core/itemfetchscope_p.h b/src/core/itemfetchscope_p.h new file mode 100644 index 0000000..0615e3e --- /dev/null +++ b/src/core/itemfetchscope_p.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2008 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "itemfetchscope.h" +#include "tagfetchscope.h" +#include +#include + +namespace Akonadi +{ +/** + * @internal + */ +class ItemFetchScopePrivate : public QSharedData +{ +public: + ItemFetchScopePrivate() + : mAncestorDepth(ItemFetchScope::None) + , mFullPayload(false) + , mAllAttributes(false) + , mCacheOnly(false) + , mCheckCachedPayloadPartsOnly(false) + , mFetchMtime(true) + , mIgnoreRetrievalErrors(false) + , mFetchRid(true) + , mFetchGid(false) + , mFetchTags(false) + , mFetchVRefs(false) + , mFetchRelations(false) + { + mTagFetchScope.setFetchIdOnly(true); + } + + ItemFetchScopePrivate(const ItemFetchScopePrivate &other) + : QSharedData(other) + { + mPayloadParts = other.mPayloadParts; + mAttributes = other.mAttributes; + mAncestorDepth = other.mAncestorDepth; + mFullPayload = other.mFullPayload; + mAllAttributes = other.mAllAttributes; + mCacheOnly = other.mCacheOnly; + mCheckCachedPayloadPartsOnly = other.mCheckCachedPayloadPartsOnly; + mFetchMtime = other.mFetchMtime; + mIgnoreRetrievalErrors = other.mIgnoreRetrievalErrors; + mChangedSince = other.mChangedSince; + mFetchRid = other.mFetchRid; + mFetchGid = other.mFetchGid; + mFetchTags = other.mFetchTags; + mTagFetchScope = other.mTagFetchScope; + mFetchVRefs = other.mFetchVRefs; + mFetchRelations = other.mFetchRelations; + } + +public: + QSet mPayloadParts; + QSet mAttributes; + ItemFetchScope::AncestorRetrieval mAncestorDepth; + bool mFullPayload; + bool mAllAttributes; + bool mCacheOnly; + bool mCheckCachedPayloadPartsOnly; + bool mFetchMtime; + bool mIgnoreRetrievalErrors; + QDateTime mChangedSince; + bool mFetchRid; + bool mFetchGid; + bool mFetchTags; + TagFetchScope mTagFetchScope; + bool mFetchVRefs; + bool mFetchRelations; +}; + +} + diff --git a/src/core/itemmonitor.cpp b/src/core/itemmonitor.cpp new file mode 100644 index 0000000..b12a866 --- /dev/null +++ b/src/core/itemmonitor.cpp @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2007-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemmonitor.h" +#include "itemmonitor_p.h" + +#include "itemfetchscope.h" + +using namespace Akonadi; + +ItemMonitor::ItemMonitor() + : d(new Private(this)) +{ +} + +ItemMonitor::~ItemMonitor() +{ + delete d; +} + +void ItemMonitor::setItem(const Item &item) +{ + if (item == d->mItem) { + return; + } + + d->mMonitor->setItemMonitored(d->mItem, false); + + d->mItem = item; + + d->mMonitor->setItemMonitored(d->mItem, true); + + if (!d->mItem.isValid()) { + itemRemoved(); + return; + } + + // start initial fetch of the new item + auto job = new ItemFetchJob(d->mItem); + job->setFetchScope(fetchScope()); + + d->connect(job, &ItemFetchJob::result, d, [this](KJob *job) { + d->initialFetchDone(job); + }); +} + +Item ItemMonitor::item() const +{ + return d->mItem; +} + +void ItemMonitor::itemChanged(const Item &item) +{ + Q_UNUSED(item) +} + +void ItemMonitor::itemRemoved() +{ +} + +void ItemMonitor::setFetchScope(const ItemFetchScope &fetchScope) +{ + d->mMonitor->setItemFetchScope(fetchScope); +} + +ItemFetchScope &ItemMonitor::fetchScope() +{ + return d->mMonitor->itemFetchScope(); +} + +#include "moc_itemmonitor_p.cpp" diff --git a/src/core/itemmonitor.h b/src/core/itemmonitor.h new file mode 100644 index 0000000..91a2f1b --- /dev/null +++ b/src/core/itemmonitor.h @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2007-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ +class Item; +class ItemFetchScope; + +/** + * @short A convenience class to monitor a single item for changes. + * + * This class can be used as a base class for classes that want to show + * a single item to the user and keep track of status changes of the item + * without having to using a Monitor object themself. + * + * Example: + * + * @code + * + * // A label that shows the name of a contact item + * + * class ContactLabel : public QLabel, public Akonadi::ItemMonitor + * { + * public: + * ContactLabel( QWidget *parent = nullptr ) + * : QLabel( parent ) + * { + * setText( "No Name" ); + * } + * + * protected: + * virtual void itemChanged( const Akonadi::Item &item ) + * { + * if ( item.mimeType() != "text/directory" ) + * return; + * + * const KContacts::Addressee addr = item.payload(); + * setText( addr.fullName() ); + * } + * + * virtual void itemRemoved() + * { + * setText( "No Name" ); + * } + * }; + * + * ... + * + * ContactLabel *label = new ContactLabel( this ); + * + * const Akonadi::Item item = fetchJob->items().at(0); + * label->setItem( item ); + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT ItemMonitor +{ +public: + /** + * Creates a new item monitor. + */ + ItemMonitor(); + + /** + * Destroys the item monitor. + */ + virtual ~ItemMonitor(); + + /** + * Sets the @p item that shall be monitored. + */ + void setItem(const Item &item); + + /** + * Returns the currently monitored item. + */ + Item item() const; + +protected: + /** + * This method is called whenever the monitored item has changed. + * + * @param item The changed item. + */ + virtual void itemChanged(const Item &item); + + /** + * This method is called whenever the monitored item has been removed. + */ + virtual void itemRemoved(); + + /** + * Sets the item fetch scope. + * + * Controls how much of an item's data is fetched from the server, e.g. + * whether to fetch the full item payload or only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond + + Q_DISABLE_COPY(ItemMonitor) +}; + +} + diff --git a/src/core/itemmonitor_p.h b/src/core/itemmonitor_p.h new file mode 100644 index 0000000..4c45eff --- /dev/null +++ b/src/core/itemmonitor_p.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2007-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "itemfetchjob.h" +#include "monitor.h" + +namespace Akonadi +{ +/** + * @internal + */ +class Q_DECL_HIDDEN ItemMonitor::Private : public QObject +{ + Q_OBJECT + +public: + Private(ItemMonitor *parent) + : QObject(nullptr) + , mParent(parent) + , mMonitor(new Monitor()) + { + mMonitor->setObjectName(QStringLiteral("ItemMonitorMonitor")); + connect(mMonitor, &Monitor::itemChanged, this, &Private::slotItemChanged); + connect(mMonitor, &Monitor::itemRemoved, this, &Private::slotItemRemoved); + } + + ~Private() + { + delete mMonitor; + } + + ItemMonitor *mParent = nullptr; + Item mItem; + Monitor *mMonitor = nullptr; + +private Q_SLOTS: + void slotItemChanged(const Akonadi::Item &item, const QSet &aSet) + { + Q_UNUSED(aSet) + mItem.apply(item); + mParent->itemChanged(item); + } + + void slotItemRemoved(const Akonadi::Item &item) + { + Q_UNUSED(item) + mItem = Item(); + mParent->itemRemoved(); + } +public Q_SLOTS: + void initialFetchDone(KJob *job) + { + if (job->error()) { + return; + } + + auto fetchJob = qobject_cast(job); + + if (!fetchJob->items().isEmpty()) { + mItem = fetchJob->items().at(0); + mParent->itemChanged(mItem); + } + } +}; + +} + diff --git a/src/core/itempayloadinternals_p.h b/src/core/itempayloadinternals_p.h new file mode 100644 index 0000000..902d167 --- /dev/null +++ b/src/core/itempayloadinternals_p.h @@ -0,0 +1,435 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "supertrait.h" + +#include + +#include +#include +#include + +#include + +#include "exceptionbase.h" + +/// @cond PRIVATE Doxygen 1.7.1 hangs processing this file. so skip it. +// for more info, see https://bugzilla.gnome.org/show_bug.cgi?id=531637 + +/* WARNING + * The below is an implementation detail of the Item class. It is not to be + * considered public API, and subject to change without notice + */ + +// Forward-declare boost::shared_ptr so that we don't have to explicitly include +// it. Caller that tries to use it will already have it included anyway +namespace boost +{ +template class shared_ptr; +template shared_ptr dynamic_pointer_cast(shared_ptr const &ptr) noexcept; +} + +namespace Akonadi +{ +namespace Internal +{ +template struct has_clone_method { +private: + template struct sfinae { + }; + struct No { + }; + struct Yes { + No no[2]; + }; + template static No test(...); + template static Yes test(sfinae *); + +public: + static const bool value = sizeof(test(nullptr)) == sizeof(Yes); +}; + +template struct clone_traits_helper { + // runtime error (commented in) or compiletime error (commented out)? + // ### runtime error, until we check has_clone_method in the + // ### Item::payload impl directly... + template static T *clone(U) + { + return nullptr; + } +}; + +template struct clone_traits_helper { + static T *clone(T *t) + { + return t ? t->clone() : nullptr; + } +}; + +template struct clone_traits : clone_traits_helper::value> { +}; + +template struct shared_pointer_traits { + static const bool defined = false; +}; + +template struct shared_pointer_traits> { + static const bool defined = true; + using element_type = T; + + template struct make { + using type = boost::shared_ptr; + }; + + using next_shared_ptr = QSharedPointer; +}; + +template struct shared_pointer_traits> { + static const bool defined = true; + using element_type = T; + + template struct make { + using type = QSharedPointer; + }; + + using next_shared_ptr = std::shared_ptr; +}; + +template struct shared_pointer_traits> { + static const bool defined = true; + using element_type = T; + + template struct make { + using type = std::shared_ptr; + }; + + using next_shared_ptr = boost::shared_ptr; +}; + +template struct is_shared_pointer { + static const bool value = shared_pointer_traits::defined; +}; + +template struct identity { + using type = T; +}; + +template struct get_hierarchy_root; + +template struct get_hierarchy_root_recurse : get_hierarchy_root { +}; + +template struct get_hierarchy_root_recurse : identity { +}; + +template struct get_hierarchy_root : get_hierarchy_root_recurse::Type> { +}; + +template struct get_hierarchy_root> { + using type = boost::shared_ptr::type>; +}; + +template struct get_hierarchy_root> { + using type = QSharedPointer::type>; +}; + +template struct get_hierarchy_root> { + using type = std::shared_ptr::type>; +}; + +/** + @internal + Payload type traits. Implements specialized handling for polymorphic types and smart pointers. + The default one is never used (as isPolymorphic is always false) and only contains safe dummy + implementations to make the compiler happy (in practice it will always optimized away anyway). +*/ +template struct PayloadTrait { + /// type of the payload object contained inside a shared pointer + using ElementType = T; + // the metatype id for the element type, or for pointer-to-element + // type, if in a shared pointer + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + /// type of the base class of the payload object inside a shared pointer, + /// same as ElementType if there is no super class + using SuperElementType = typename Akonadi::SuperClass::Type; + /// type of this payload object + using Type = T; + /// type of the payload to store a base class of this payload + /// (eg. a shared pointer containing a pointer to SuperElementType) + /// same as Type if there is not super class + using SuperType = typename Akonadi::SuperClass::Type; + /// indicates if this payload is polymorphic, that it is a shared pointer + /// and has a known super class + static const bool isPolymorphic = false; + /// checks an object of this payload type for being @c null + static inline bool isNull(const Type &p) + { + Q_UNUSED(p) + return true; + } + /// casts to Type from @c U + /// throws a PayloadException if casting failed + template static inline Type castFrom(const U &) + { + throw PayloadException("you should never get here"); + } + /// tests if casting from @c U to Type is possible + template static inline bool canCastFrom(const U &) + { + return false; + } + /// cast to @c U from Type + template static inline U castTo(const Type &) + { + throw PayloadException("you should never get here"); + } + template static T clone(const U &) + { + throw PayloadException("clone: you should never get here"); + } + /// defines the type of shared pointer used (0: none, > 0: boost::shared_ptr, QSharedPointer, ...) + static const unsigned int sharedPointerId = 0; +}; + +/** + @internal + Payload type trait specialization for boost::shared_ptr + for documentation of the various members, see above +*/ +template struct PayloadTrait> { + using ElementType = T; + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + using SuperElementType = typename Akonadi::SuperClass::Type; + using Type = boost::shared_ptr; + using SuperType = boost::shared_ptr; + static const bool isPolymorphic = !std::is_same::value; + static inline bool isNull(const Type &p) + { + return p.get() == nullptr; + } + template static inline Type castFrom(const boost::shared_ptr &p) + { + const Type sp = boost::dynamic_pointer_cast(p); + if (sp.get() != nullptr || p.get() == nullptr) { + return sp; + } + throw PayloadException("boost::dynamic_pointer_cast failed"); + } + template static inline bool canCastFrom(const boost::shared_ptr &p) + { + const Type sp = boost::dynamic_pointer_cast(p); + return sp.get() != nullptr || p.get() == nullptr; + } + template static inline boost::shared_ptr castTo(const Type &p) + { + const boost::shared_ptr sp = boost::dynamic_pointer_cast(p); + return sp; + } + static boost::shared_ptr clone(const QSharedPointer &t) + { + if (T *nt = clone_traits::clone(t.data())) { + return boost::shared_ptr(nt); + } else { + return boost::shared_ptr(); + } + } + static boost::shared_ptr clone(const std::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return boost::shared_ptr(nt); + } else { + return boost::shared_ptr(); + } + } + static const unsigned int sharedPointerId = 1; +}; + +/** + @internal + Payload type trait specialization for QSharedPointer + for documentation of the various members, see above +*/ +template struct PayloadTrait> { + using ElementType = T; + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + using SuperElementType = typename Akonadi::SuperClass::Type; + using Type = QSharedPointer; + using SuperType = QSharedPointer; + static const bool isPolymorphic = !std::is_same::value; + static inline bool isNull(const Type &p) + { + return p.isNull(); + } + template static inline Type castFrom(const QSharedPointer &p) + { + const Type sp = qSharedPointerDynamicCast(p); + if (!sp.isNull() || p.isNull()) { + return sp; + } + throw PayloadException("qSharedPointerDynamicCast failed"); + } + template static inline bool canCastFrom(const QSharedPointer &p) + { + const Type sp = qSharedPointerDynamicCast(p); + return !sp.isNull() || p.isNull(); + } + template static inline QSharedPointer castTo(const Type &p) + { + const QSharedPointer sp = qSharedPointerDynamicCast(p); + return sp; + } + static QSharedPointer clone(const boost::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return QSharedPointer(nt); + } else { + return QSharedPointer(); + } + } + static QSharedPointer clone(const std::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return QSharedPointer(nt); + } else { + return QSharedPointer(); + } + } + static const unsigned int sharedPointerId = 2; +}; + +/** + @internal + Payload type trait specialization for std::shared_ptr + for documentation of the various members, see above +*/ +template struct PayloadTrait> { + using ElementType = T; + static int elementMetaTypeId() + { + return qMetaTypeId(); + } + using SuperElementType = typename Akonadi::SuperClass::Type; + using Type = std::shared_ptr; + using SuperType = std::shared_ptr; + static const bool isPolymorphic = !std::is_same::value; + static inline bool isNull(const Type &p) + { + return p.get() == nullptr; + } + template static inline Type castFrom(const std::shared_ptr &p) + { + const Type sp = std::dynamic_pointer_cast(p); + if (sp.get() != nullptr || p.get() == nullptr) { + return sp; + } + throw PayloadException("std::dynamic_pointer_cast failed"); + } + template static inline bool canCastFrom(const std::shared_ptr &p) + { + const Type sp = std::dynamic_pointer_cast(p); + return sp.get() != nullptr || p.get() == nullptr; + } + template static inline std::shared_ptr castTo(const Type &p) + { + const std::shared_ptr sp = std::dynamic_pointer_cast(p); + return sp; + } + static std::shared_ptr clone(const boost::shared_ptr &t) + { + if (T *nt = clone_traits::clone(t.get())) { + return std::shared_ptr(nt); + } else { + return std::shared_ptr(); + } + } + static std::shared_ptr clone(const QSharedPointer &t) + { + if (T *nt = clone_traits::clone(t.data())) { + return std::shared_ptr(nt); + } else { + return std::shared_ptr(); + } + } + static const unsigned int sharedPointerId = 3; +}; + +/** + * @internal + * Non-template base class for the payload container. + */ +struct PayloadBase { + virtual ~PayloadBase() = default; + virtual PayloadBase *clone() const = 0; + virtual const char *typeName() const = 0; + +protected: + PayloadBase() = default; + +private: + Q_DISABLE_COPY_MOVE(PayloadBase) +}; + +/** + * @internal + * Container for the actual payload object. + */ +template struct Payload : public PayloadBase { + Payload(const T &p) + : payload(p) + { + } + + PayloadBase *clone() const override + { + return new Payload(const_cast *>(this)->payload); + } + + const char *typeName() const override + { + return typeid(const_cast *>(this)).name(); + } + + T payload; +}; + +/** + * @internal + * abstract, will therefore always fail to compile for pointer payloads + */ +template struct Payload : public PayloadBase { +}; + +/** + @internal + Basically a dynamic_cast that also works across DSO boundaries. +*/ +template inline Payload *payload_cast(PayloadBase *payloadBase) +{ + auto p = dynamic_cast *>(payloadBase); + // try harder to cast, workaround for some gcc issue with template instances in multiple DSO's + if (!p && payloadBase && strcmp(payloadBase->typeName(), typeid(p).name()) == 0) { + p = static_cast *>(payloadBase); + } + return p; +} + +} // namespace Internal + +} // namespace Akonadi + +/// @endcond + diff --git a/src/core/itemserializer.cpp b/src/core/itemserializer.cpp new file mode 100644 index 0000000..33776c9 --- /dev/null +++ b/src/core/itemserializer.cpp @@ -0,0 +1,238 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "config_p.h" +#include "item.h" +#include "itemserializer_p.h" +#include "itemserializerplugin.h" +#include "protocolhelper_p.h" +#include "typepluginloader_p.h" + +#include "private/compressionstream_p.h" +#include "private/externalpartstorage_p.h" + +#include "akonadicore_debug.h" + +// Qt +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(std::string) + +namespace Akonadi +{ +DefaultItemSerializerPlugin::DefaultItemSerializerPlugin() = default; + +bool DefaultItemSerializerPlugin::deserialize(Item &item, const QByteArray &label, QIODevice &data, int /*version*/) +{ + if (label != Item::FullPayload) { + return false; + } + + item.setPayload(data.readAll()); + return true; +} + +void DefaultItemSerializerPlugin::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + Q_UNUSED(version) + Q_ASSERT(label == Item::FullPayload); + Q_UNUSED(label) + data.write(item.payload()); +} + +bool StdStringItemSerializerPlugin::deserialize(Item &item, const QByteArray &label, QIODevice &data, int /*version*/) +{ + if (label != Item::FullPayload) { + return false; + } + std::string str; + { + const QByteArray ba = data.readAll(); + str.assign(ba.data(), ba.size()); + } + item.setPayload(str); + return true; +} + +void StdStringItemSerializerPlugin::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + Q_UNUSED(version) + Q_ASSERT(label == Item::FullPayload); + Q_UNUSED(label) + const auto str = item.payload(); + data.write(QByteArray::fromRawData(str.data(), str.size())); +} + +/*static*/ +void ItemSerializer::deserialize(Item &item, const QByteArray &label, const QByteArray &data, int version, PayloadStorage storage) +{ + if (storage == Internal) { + QBuffer buffer; + buffer.setData(data); + buffer.open(QIODevice::ReadOnly); + deserialize(item, label, buffer, version); + buffer.close(); + } else { + QFile file; + if (storage == External) { + file.setFileName(ExternalPartStorage::resolveAbsolutePath(data)); + } else if (storage == Foreign) { + file.setFileName(QString::fromUtf8(data)); + } + + if (file.open(QIODevice::ReadOnly)) { + deserialize(item, label, file, version); + file.close(); + } else { + qCWarning(AKONADICORE_LOG) << "Failed to open" << ((storage == External) ? "external" : "foreign") << "payload:" << file.fileName() + << file.errorString(); + } + } +} + +/*static*/ +void ItemSerializer::deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) +{ + auto plugin = TypePluginLoader::defaultPluginForMimeType(item.mimeType()); + + const auto handleError = [&](QIODevice &device, bool compressed) { + device.seek(0); + QByteArray data; + if (compressed) { + CompressionStream decompressor(&device); + decompressor.open(QIODevice::ReadOnly); + data = decompressor.readAll(); + } else { + data = device.readAll(); + } + + qCWarning(AKONADICORE_LOG) << "Unable to deserialize payload part:" << label << "in item" << item.id() << "collection" << item.parentCollection().id(); + qCWarning(AKONADICORE_LOG) << (compressed ? "Decompressed" : "") << "payload data was: " << data; + }; + + if (CompressionStream::isCompressed(&data)) { + CompressionStream decompressor(&data); + decompressor.open(QIODevice::ReadOnly); + if (!plugin->deserialize(item, label, decompressor, version)) { + handleError(data, true); + } + if (decompressor.error()) { + qCWarning(AKONADICORE_LOG) << "Deserialization failed due to decompression error:" << QString::fromStdString(decompressor.error().message()); + } + } else { + if (!plugin->deserialize(item, label, data, version)) { + handleError(data, false); + } + } +} + +/*static*/ +void ItemSerializer::serialize(const Item &item, const QByteArray &label, QByteArray &data, int &version) +{ + QBuffer buffer; + buffer.setBuffer(&data); + buffer.open(QIODevice::WriteOnly); + buffer.seek(0); + serialize(item, label, buffer, version); + buffer.close(); +} + +/*static*/ +void ItemSerializer::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + if (!item.hasPayload()) { + return; + } + ItemSerializerPlugin *plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + + if (Config::get().payloadCompression.enabled) { + CompressionStream compressor(&data); + compressor.open(QIODevice::WriteOnly); + plugin->serialize(item, label, compressor, version); + } else { + plugin->serialize(item, label, data, version); + } +} + +void ItemSerializer::apply(Item &item, const Item &other) +{ + if (!other.hasPayload()) { + return; + } + + ItemSerializerPlugin *plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + plugin->apply(item, other); +} + +QSet ItemSerializer::parts(const Item &item) +{ + if (!item.hasPayload()) { + return QSet(); + } + return TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds())->parts(item); +} + +QSet ItemSerializer::availableParts(const Item &item) +{ + if (!item.hasPayload()) { + return QSet(); + } + ItemSerializerPlugin *plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + return plugin->availableParts(item); +} + +QSet ItemSerializer::allowedForeignParts(const Item &item) +{ + if (!item.hasPayload()) { + return QSet(); + } + + ItemSerializerPlugin *plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), item.availablePayloadMetaTypeIds()); + return plugin->allowedForeignParts(item); +} + +Item ItemSerializer::convert(const Item &item, int mtid) +{ + qCDebug(AKONADICORE_LOG) << "asked to convert a" << item.mimeType() << "item to format" << (mtid ? QMetaType::typeName(mtid) : ""); + if (!item.hasPayload()) { + qCDebug(AKONADICORE_LOG) << " -> but item has no payload!"; + return Item(); + } + + if (ItemSerializerPlugin *const plugin = TypePluginLoader::pluginForMimeTypeAndClass(item.mimeType(), QVector(1, mtid), TypePluginLoader::NoDefault)) { + qCDebug(AKONADICORE_LOG) << " -> found a plugin that feels responsible, trying serialising the payload"; + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + int version = 0; + serialize(item, Item::FullPayload, buffer, version); + buffer.seek(0); + qCDebug(AKONADICORE_LOG) << " -> serialized payload into" << buffer.size() << "bytes\n" + << " -> going to deserialize"; + Item newItem; + if (plugin->deserialize(newItem, Item::FullPayload, buffer, version)) { + qCDebug(AKONADICORE_LOG) << " -> conversion successful"; + return newItem; + } else { + qCDebug(AKONADICORE_LOG) << " -> conversion FAILED"; + } + } else { + // qCDebug(AKONADICORE_LOG) << " -> found NO plugin that feels responsible"; + } + return Item(); +} + +void ItemSerializer::overridePluginLookup(QObject *p) +{ + TypePluginLoader::overridePluginLookup(p); +} + +} // namespace Akonadi diff --git a/src/core/itemserializer_p.h b/src/core/itemserializer_p.h new file mode 100644 index 0000000..54e926d --- /dev/null +++ b/src/core/itemserializer_p.h @@ -0,0 +1,128 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "akonaditests_export.h" + +#include "itemserializerplugin.h" + +#include + +class QIODevice; + +namespace Akonadi +{ +class Item; + +/** + @internal + Serialization/Deserialization of item parts, serializer plugin management. +*/ +class AKONADI_TESTS_EXPORT ItemSerializer +{ +public: + enum PayloadStorage { + Internal, + External, + Foreign, + }; + + /** throws ItemSerializerException on failure */ + static void deserialize(Item &item, const QByteArray &label, const QByteArray &data, int version, PayloadStorage storage); + /** throws ItemSerializerException on failure */ + static void deserialize(Item &item, const QByteArray &label, QIODevice &data, int version); + /** throws ItemSerializerException on failure */ + static void serialize(const Item &item, const QByteArray &label, QByteArray &data, int &version); + /** throws ItemSerializerException on failure */ + static void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version); + + /** + * Throws ItemSerializerException on failure. + * @param item the item to apply to + * @param other the item to get values from + * @since 4.4 + */ + static void apply(Item &item, const Item &other); + + /** + * Returns a list of parts available in the item payload. + */ + static QSet parts(const Item &item); + + /** + * Returns a list of parts available remotely in the item payload. + * @param item the item for which to list payload parts + * @since 4.4 + */ + static QSet availableParts(const Item &item); + + /** + * Returns list of parts of the item payload that can be stored using + * foreign payload. + * + * @since 5.7 + */ + static QSet allowedForeignParts(const Item &item); + + /** + * Tries to convert the payload in \a item into type with + * metatype-id \a metaTypeId. + * Throws ItemSerializerException or returns an Item w/o payload on failure. + * @param item the item to convert + * @param metaTypeId the meta type id used to convert items payload + * @since 4.6 + */ + static Item convert(const Item &item, int metaTypeId); + + /** + * Override the plugin-lookup with @p plugin. + * + * After calling this each lookup will always return @p plugin. + * This is useful to inject a special plugin for testing purposes. + * To reset the plugin, set to 0. + * + * @since 4.12 + */ + static void overridePluginLookup(QObject *plugin); +}; + +/** + @internal + Default implementation for serializer plugin. +*/ +class DefaultItemSerializerPlugin : public QObject, public ItemSerializerPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) +public: + DefaultItemSerializerPlugin(); + + bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) override; + void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) override; +}; + +/** + @internal + Serializer plugin implementation for std::string +*/ +class StdStringItemSerializerPlugin : public QObject, public ItemSerializerPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) +public: + StdStringItemSerializerPlugin(); + + bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) override; + void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) override; +}; + +} + diff --git a/src/core/itemserializerplugin.cpp b/src/core/itemserializerplugin.cpp new file mode 100644 index 0000000..6a1cc37 --- /dev/null +++ b/src/core/itemserializerplugin.cpp @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemserializerplugin.h" +#include "itemserializer_p.h" + +#include + +using namespace Akonadi; + +ItemSerializerPlugin::~ItemSerializerPlugin() = default; + +QSet ItemSerializerPlugin::parts(const Item &item) const +{ + if (!item.hasPayload()) { + return {}; + } + + return {Item::FullPayload}; +} + +void ItemSerializerPlugin::overridePluginLookup(QObject *p) +{ + ItemSerializer::overridePluginLookup(p); +} + +QSet ItemSerializerPlugin::availableParts(const Item &item) const +{ + if (!item.hasPayload()) { + return {}; + } + + return {Item::FullPayload}; +} + +void ItemSerializerPlugin::apply(Item &item, const Item &other) +{ + Q_FOREACH (const QByteArray &part, other.loadedPayloadParts()) { + QByteArray partData; + QBuffer buffer; + buffer.setBuffer(&partData); + buffer.open(QIODevice::ReadWrite); + buffer.seek(0); + int version; + // NOTE: we can't just pass other.payloadData() into deserialize(), + // because that does not preserve payload version. + serialize(other, part, buffer, version); + buffer.seek(0); + deserialize(item, part, buffer, version); + } +} + +QSet ItemSerializerPlugin::allowedForeignParts(const Item &item) const +{ + if (!item.hasPayload()) { + return {}; + } + + return {Item::FullPayload}; +} diff --git a/src/core/itemserializerplugin.h b/src/core/itemserializerplugin.h new file mode 100644 index 0000000..8ea4d6f --- /dev/null +++ b/src/core/itemserializerplugin.h @@ -0,0 +1,218 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "akonadicore_export.h" +#include "item.h" + +class QIODevice; + +namespace Akonadi +{ +/** + * @short The base class for item type serializer plugins. + * + * Serializer plugins convert between the payload of Akonadi::Item objects and + * a textual or binary representation of the actual content data. + * This allows to easily add support for new types to Akonadi. + * + * The following example shows how to implement a serializer plugin for + * a new data type PimNote. + * + * The PimNote data structure: + * @code + * typedef struct { + * QString author; + * QDateTime dateTime; + * QString text; + * } PimNote; + * @endcode + * + * The serializer plugin code: + * @code + * #include + * + * class SerializerPluginPimNote : public QObject, public Akonadi::ItemSerializerPlugin + * { + * Q_OBJECT + * Q_INTERFACES( Akonadi::ItemSerializerPlugin ) + * + * public: + * bool deserialize( Akonadi::Item& item, const QByteArray& label, QIODevice& data, int version ) + * { + * // we don't handle versions in this example + * Q_UNUSED(version) + * + * // we work only on full payload + * if ( label != Akonadi::Item::FullPayload ) + * return false; + * + * QDataStream stream( &data ); + * + * PimNote note; + * stream >> note.author; + * stream >> note.dateTime; + * stream >> note.text; + * + * item.setPayload( note ); + * + * return true; + * } + * + * void serialize( const Akonadi::Item& item, const QByteArray& label, QIODevice& data, int &version ) + * { + * // we don't handle versions in this example + * Q_UNUSED(version) + * + * if ( label != Akonadi::Item::FullPayload || !item.hasPayload() ) + * return; + * + * QDataStream stream( &data ); + * + * PimNote note = item.payload(); + * + * stream << note.author; + * stream << note.dateTime; + * stream << note.text; + * } + * }; + * + * Q_EXPORT_PLUGIN2( akonadi_serializer_pimnote, SerializerPluginPimNote ) + * + * @endcode + * + * The desktop file: + * @code + * [Misc] + * Name=Pim Note Serializer + * Comment=An Akonadi serializer plugin for note objects + * + * [Plugin] + * Type=application/x-pimnote + * X-KDE-Library=akonadi_serializer_pimnote + * @endcode + * + * @author Till Adam , Volker Krause + */ +class AKONADICORE_EXPORT ItemSerializerPlugin +{ +public: + /** + * Destroys the item serializer plugin. + */ + virtual ~ItemSerializerPlugin(); + + /** + * Converts serialized item data provided in @p data into payload for @p item. + * + * @param item The item to which the payload should be added. + * It is guaranteed to have a mime type matching one of the supported + * mime types of this plugin. + * However it might contain a unsuited payload added manually + * by the application developer. + * Verifying the payload type in case a payload is already available + * is recommended therefore. + * @param label The part identifier of the part to deserialize. + * @p label might be an unsupported item part, return @c false if this is the case. + * @param data A QIODevice providing access to the serialized data. + * The QIODevice is opened in read-only mode and positioned at the beginning. + * The QIODevice is guaranteed to be valid. + * @param version The version of the data format as set by the user in serialize() or @c 0 (default). + * @return @c false if the specified part is not supported by this plugin, @c true if the part + * could be de-serialized successfully. + */ + virtual bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) = 0; + + /** + * Convert the payload object provided in @p item into its serialzed form into @p data. + * + * @param item The item which contains the payload. + * It is guaranteed to have a mimetype matching one of the supported + * mimetypes of this plugin as well as the existence of a payload object. + * However it might contain an unsupported payload added manually by + * the application developer. + * Verifying the payload type is recommended therefore. + * @param label The part identifier of the part to serialize. + * @p label will be one of the item parts returned by parts(). + * @param data The QIODevice where the serialized data should be written to. + * The QIODevice is opened in write-only mode and positioned at the beginning. + * The QIODevice is guaranteed to be valid. + * @param version The version of the data format. Can be set by the user to handle different + * versions. + */ + virtual void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) = 0; + + /** + * Returns a list of available parts for the given item payload. + * The default implementation returns Item::FullPayload if a payload is set. + * + * @param item The item. + */ + virtual QSet parts(const Item &item) const; + + /** + * Override the plugin-lookup with @p plugin. + * + * After calling this each lookup will always return @p plugin. + * This is useful to inject a special plugin for testing purposes. + * To reset the plugin, set to 0. + * + * @since 4.12 + */ + static void overridePluginLookup(QObject *plugin); + + /** + * Merges the payload parts in @p other into @p item. + * + * The default implementation is slow as it requires serializing @p other, and deserializing @p item multiple times. + * Reimplementing this is recommended if your type uses payload parts. + * @param item receives merged parts from @p other + * @param other the paylod parts to merge into @p item + * @since 4.4 + */ + virtual void apply(Item &item, const Item &other); + + /** + * Returns the parts available in the item @p item. + * + * This should be reimplemented to return available parts. + * + * The default implementation returns an empty set if the item has a payload, + * and a set containing Item::FullPayload if the item has no payload. + * @param item the item for which to list payload parts + * @since 4.4 + */ + virtual QSet availableParts(const Item &item) const; + + /** + * Returns the parts available in the item @p item that can be stored using + * foreign payload mechanism. Is only called for items whose payload has been + * set via Item::setPayloadPath(). + * + * By default returns "RFC822", which can always be stored as foreign payload. + * Some implementations can also allow "HEAD" to be stored as foreign payload, + * if HEAD is only a subset of RFC822 part. + * + * @since 5.7 + */ + virtual QSet allowedForeignParts(const Item &item) const; + +protected: + explicit ItemSerializerPlugin() = default; + +private: + Q_DISABLE_COPY_MOVE(ItemSerializerPlugin) +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::ItemSerializerPlugin, "org.freedesktop.Akonadi.ItemSerializerPlugin/2.0") + diff --git a/src/core/itemsync.cpp b/src/core/itemsync.cpp new file mode 100644 index 0000000..311de74 --- /dev/null +++ b/src/core/itemsync.cpp @@ -0,0 +1,565 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemsync.h" + +#include "collection.h" +#include "item_p.h" +#include "itemcreatejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "job_p.h" +#include "transactionsequence.h" + +#include "akonadicore_debug.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Akonadi::ItemSyncPrivate : public JobPrivate +{ +public: + explicit ItemSyncPrivate(ItemSync *parent) + : JobPrivate(parent) + , mTransactionMode(ItemSync::SingleTransaction) + , mCurrentTransaction(nullptr) + , mTransactionJobs(0) + , mPendingJobs(0) + , mProgress(0) + , mTotalItems(-1) + , mTotalItemsProcessed(0) + , mStreaming(false) + , mIncremental(false) + , mDeliveryDone(false) + , mFinished(false) + , mFullListingDone(false) + , mProcessingBatch(false) + , mDisableAutomaticDeliveryDone(false) + , mBatchSize(10) + , mMergeMode(Akonadi::ItemSync::RIDMerge) + { + // we want to fetch all data by default + mFetchScope.fetchFullPayload(); + mFetchScope.fetchAllAttributes(); + } + + void createOrMerge(const Item &item); + void checkDone(); + void slotItemsReceived(const Item::List &items); + void slotLocalListDone(KJob *job); + void slotLocalDeleteDone(KJob *job); + void slotLocalChangeDone(KJob *job); + void execute(); + void processItems(); + void processBatch(); + void deleteItems(const Item::List &items); + void slotTransactionResult(KJob *job); + void requestTransaction(); + Job *subjobParent() const; + void fetchLocalItemsToDelete(); + QString jobDebuggingString() const override; + bool allProcessed() const; + + Q_DECLARE_PUBLIC(ItemSync) + Collection mSyncCollection; + QSet mListedItems; + + ItemSync::TransactionMode mTransactionMode; + TransactionSequence *mCurrentTransaction = nullptr; + int mTransactionJobs; + + // fetch scope for initial item listing + ItemFetchScope mFetchScope; + + Akonadi::Item::List mRemoteItemQueue; + Akonadi::Item::List mRemovedRemoteItemQueue; + Akonadi::Item::List mCurrentBatchRemoteItems; + Akonadi::Item::List mCurrentBatchRemovedRemoteItems; + Akonadi::Item::List mItemsToDelete; + + // create counter + int mPendingJobs; + int mProgress; + int mTotalItems; + int mTotalItemsProcessed; + + bool mStreaming; + bool mIncremental; + bool mDeliveryDone; + bool mFinished; + bool mFullListingDone; + bool mProcessingBatch; + bool mDisableAutomaticDeliveryDone; + + int mBatchSize; + Akonadi::ItemSync::MergeMode mMergeMode; +}; + +void ItemSyncPrivate::createOrMerge(const Item &item) +{ + Q_Q(ItemSync); + // don't try to do anything in error state + if (q->error()) { + return; + } + mPendingJobs++; + auto create = new ItemCreateJob(item, mSyncCollection, subjobParent()); + ItemCreateJob::MergeOptions merge = ItemCreateJob::Silent; + if (mMergeMode == ItemSync::GIDMerge && !item.gid().isEmpty()) { + merge |= ItemCreateJob::GID; + } else { + merge |= ItemCreateJob::RID; + } + create->setMerge(merge); + q->connect(create, &ItemCreateJob::result, q, [this](KJob *job) { + slotLocalChangeDone(job); + }); +} + +bool ItemSyncPrivate::allProcessed() const +{ + return mDeliveryDone && mCurrentBatchRemoteItems.isEmpty() && mRemoteItemQueue.isEmpty() && mRemovedRemoteItemQueue.isEmpty() + && mCurrentBatchRemovedRemoteItems.isEmpty(); +} + +void ItemSyncPrivate::checkDone() +{ + Q_Q(ItemSync); + q->setProcessedAmount(KJob::Bytes, mProgress); + if (mPendingJobs > 0) { + return; + } + + if (mTransactionJobs > 0) { + // Commit the current transaction if we're in batch processing mode or done + // and wait until the transaction is committed to process the next batch + if (mTransactionMode == ItemSync::MultipleTransactions || (mDeliveryDone && mRemoteItemQueue.isEmpty())) { + if (mCurrentTransaction) { + // Note that mCurrentTransaction->commit() is a no-op if we're already rolling back + // so this signal is a bit misleading (but it's only used by unittests it seems) + Q_EMIT q->transactionCommitted(); + mCurrentTransaction->commit(); + mCurrentTransaction = nullptr; + } + return; + } + } + mProcessingBatch = false; + + if (q->error() == Job::UserCanceled && mTransactionJobs == 0 && !mFinished) { + qCDebug(AKONADICORE_LOG) << "ItemSync of collection" << mSyncCollection.id() << "finished due to user cancelling"; + mFinished = true; + q->emitResult(); + return; + } + + if (!mRemoteItemQueue.isEmpty()) { + execute(); + // We don't have enough items, request more + if (!mProcessingBatch) { + Q_EMIT q->readyForNextBatch(mBatchSize - mRemoteItemQueue.size()); + } + return; + } + Q_EMIT q->readyForNextBatch(mBatchSize); + + if (allProcessed() && !mFinished) { + // prevent double result emission, can happen since checkDone() is called from all over the place + qCDebug(AKONADICORE_LOG) << "ItemSync of collection" << mSyncCollection.id() << "finished"; + mFinished = true; + q->emitResult(); + } +} + +ItemSync::ItemSync(const Collection &collection, QObject *parent) + : Job(new ItemSyncPrivate(this), parent) +{ + Q_D(ItemSync); + d->mSyncCollection = collection; +} + +ItemSync::~ItemSync() +{ +} + +void ItemSync::setFullSyncItems(const Item::List &items) +{ + /* + * We received a list of items from the server: + * * fetch all local id's + rid's only + * * check each full sync item whether it's locally available + * * if it is modify the item + * * if it's not create it + * * delete all superfluous items + */ + Q_D(ItemSync); + Q_ASSERT(!d->mIncremental); + if (!d->mStreaming) { + d->mDeliveryDone = true; + } + d->mRemoteItemQueue += items; + d->mTotalItemsProcessed += items.count(); + qCDebug(AKONADICORE_LOG) << "Received batch: " << items.count() << "Already processed: " << d->mTotalItemsProcessed + << "Expected total amount: " << d->mTotalItems; + if (!d->mDisableAutomaticDeliveryDone && (d->mTotalItemsProcessed == d->mTotalItems)) { + d->mDeliveryDone = true; + } + d->execute(); +} + +void ItemSync::setTotalItems(int amount) +{ + Q_D(ItemSync); + Q_ASSERT(!d->mIncremental); + Q_ASSERT(amount >= 0); + setStreamingEnabled(true); + qCDebug(AKONADICORE_LOG) << "Expected total amount:" << amount; + d->mTotalItems = amount; + setTotalAmount(KJob::Bytes, amount); + if (!d->mDisableAutomaticDeliveryDone && (d->mTotalItems == 0)) { + d->mDeliveryDone = true; + d->execute(); + } +} + +void ItemSync::setDisableAutomaticDeliveryDone(bool disable) +{ + Q_D(ItemSync); + d->mDisableAutomaticDeliveryDone = disable; +} + +void ItemSync::setIncrementalSyncItems(const Item::List &changedItems, const Item::List &removedItems) +{ + /* + * We received an incremental listing of items: + * * for each changed item: + * ** If locally available => modify + * ** else => create + * * removed items can be removed right away + */ + Q_D(ItemSync); + d->mIncremental = true; + if (!d->mStreaming) { + d->mDeliveryDone = true; + } + d->mRemoteItemQueue += changedItems; + d->mRemovedRemoteItemQueue += removedItems; + d->mTotalItemsProcessed += changedItems.count() + removedItems.count(); + qCDebug(AKONADICORE_LOG) << "Received: " << changedItems.count() << "Removed: " << removedItems.count() << "In total: " << d->mTotalItemsProcessed + << " Wanted: " << d->mTotalItems; + if (!d->mDisableAutomaticDeliveryDone && (d->mTotalItemsProcessed == d->mTotalItems)) { + d->mDeliveryDone = true; + } + d->execute(); +} + +void ItemSync::setFetchScope(ItemFetchScope &fetchScope) +{ + Q_D(ItemSync); + d->mFetchScope = fetchScope; +} + +ItemFetchScope &ItemSync::fetchScope() +{ + Q_D(ItemSync); + return d->mFetchScope; +} + +void ItemSync::doStart() +{ +} + +void ItemSyncPrivate::fetchLocalItemsToDelete() +{ + Q_Q(ItemSync); + if (mIncremental) { + qFatal("This must not be called while in incremental mode"); + return; + } + auto job = new ItemFetchJob(mSyncCollection, subjobParent()); + job->fetchScope().setFetchRemoteIdentification(true); + job->fetchScope().setFetchModificationTime(false); + job->setDeliveryOption(ItemFetchJob::EmitItemsIndividually); + // we only can fetch parts already in the cache, otherwise this will deadlock + job->fetchScope().setCacheOnly(true); + + QObject::connect(job, &ItemFetchJob::itemsReceived, q, [this](const Akonadi::Item::List &lst) { + slotItemsReceived(lst); + }); + QObject::connect(job, &ItemFetchJob::result, q, [this](KJob *job) { + slotLocalListDone(job); + }); + mPendingJobs++; +} + +void ItemSyncPrivate::slotItemsReceived(const Item::List &items) +{ + for (const Akonadi::Item &item : items) { + // Don't delete items that have not yet been synchronized + if (item.remoteId().isEmpty()) { + continue; + } + if (!mListedItems.contains(item.remoteId())) { + mItemsToDelete << Item(item.id()); + } + } +} + +void ItemSyncPrivate::slotLocalListDone(KJob *job) +{ + mPendingJobs--; + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + } + deleteItems(mItemsToDelete); + checkDone(); +} + +QString ItemSyncPrivate::jobDebuggingString() const +{ + // TODO: also print out mIncremental and mTotalItemsProcessed, but they are set after the job + // started, so this requires passing jobDebuggingString to jobEnded(). + return QStringLiteral("Collection %1 (%2)").arg(mSyncCollection.id()).arg(mSyncCollection.name()); +} + +void ItemSyncPrivate::execute() +{ + // shouldn't happen + if (mFinished) { + qCWarning(AKONADICORE_LOG) << "Call to execute() on finished job."; + Q_ASSERT(false); + return; + } + // not doing anything, start processing + if (!mProcessingBatch) { + if (mRemoteItemQueue.size() >= mBatchSize || mDeliveryDone) { + // we have a new batch to process + const int num = qMin(mBatchSize, mRemoteItemQueue.size()); + mCurrentBatchRemoteItems.reserve(mBatchSize); + std::move(mRemoteItemQueue.begin(), mRemoteItemQueue.begin() + num, std::back_inserter(mCurrentBatchRemoteItems)); + mRemoteItemQueue.erase(mRemoteItemQueue.begin(), mRemoteItemQueue.begin() + num); + + mCurrentBatchRemovedRemoteItems += mRemovedRemoteItemQueue; + mRemovedRemoteItemQueue.clear(); + } else { + // nothing to do, let's wait for more data + return; + } + mProcessingBatch = true; + processBatch(); + return; + } + checkDone(); +} + +// process the current batch of items +void ItemSyncPrivate::processBatch() +{ + Q_Q(ItemSync); + if (mCurrentBatchRemoteItems.isEmpty() && !mDeliveryDone) { + return; + } + if (q->error() == Job::UserCanceled) { + checkDone(); + return; + } + + // request a transaction, there are items that require processing + requestTransaction(); + + processItems(); + + // removed + if (!mIncremental && allProcessed()) { + // the full listing is done and we know which items to remove + fetchLocalItemsToDelete(); + } else { + deleteItems(mCurrentBatchRemovedRemoteItems); + mCurrentBatchRemovedRemoteItems.clear(); + } + + checkDone(); +} + +void ItemSyncPrivate::processItems() +{ + // added / updated + for (const Item &remoteItem : std::as_const(mCurrentBatchRemoteItems)) { + if (remoteItem.remoteId().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Item " << remoteItem.id() << " does not have a remote identifier"; + continue; + } + if (!mIncremental) { + mListedItems << remoteItem.remoteId(); + } + createOrMerge(remoteItem); + } + mCurrentBatchRemoteItems.clear(); +} + +void ItemSyncPrivate::deleteItems(const Item::List &itemsToDelete) +{ + Q_Q(ItemSync); + // if in error state, better not change anything anymore + if (q->error()) { + return; + } + + if (itemsToDelete.isEmpty()) { + return; + } + + mPendingJobs++; + auto job = new ItemDeleteJob(itemsToDelete, subjobParent()); + q->connect(job, &ItemDeleteJob::result, q, [this](KJob *job) { + slotLocalDeleteDone(job); + }); + + // It can happen that the groupware servers report us deleted items + // twice, in this case this item delete job will fail on the second try. + // To avoid a rollback of the complete transaction we gracefully allow the job + // to fail :) + auto transaction = qobject_cast(subjobParent()); + if (transaction) { + transaction->setIgnoreJobFailure(job); + } +} + +void ItemSyncPrivate::slotLocalDeleteDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Deleting items from the akonadi database failed:" << job->errorString(); + } + mPendingJobs--; + mProgress++; + + checkDone(); +} + +void ItemSyncPrivate::slotLocalChangeDone(KJob *job) +{ + if (job->error() && job->error() != Job::KilledJobError) { + qCWarning(AKONADICORE_LOG) << "Creating/updating items from the akonadi database failed:" << job->errorString(); + mRemoteItemQueue.clear(); // don't try to process any more items after a rollback + } + mPendingJobs--; + mProgress++; + + checkDone(); +} + +void ItemSyncPrivate::slotTransactionResult(KJob *job) +{ + --mTransactionJobs; + if (mCurrentTransaction == job) { + mCurrentTransaction = nullptr; + } + + checkDone(); +} + +void ItemSyncPrivate::requestTransaction() +{ + Q_Q(ItemSync); + // we never want parallel transactions, single transaction just makes one big transaction, and multi transaction uses multiple transaction sequentially + if (!mCurrentTransaction) { + ++mTransactionJobs; + mCurrentTransaction = new TransactionSequence(q); + mCurrentTransaction->setAutomaticCommittingEnabled(false); + QObject::connect(mCurrentTransaction, &TransactionSequence::result, q, [this](KJob *job) { + slotTransactionResult(job); + }); + } +} + +Job *ItemSyncPrivate::subjobParent() const +{ + Q_Q(const ItemSync); + if (mCurrentTransaction && mTransactionMode != ItemSync::NoTransaction) { + return mCurrentTransaction; + } + return const_cast(q); +} + +void ItemSync::setStreamingEnabled(bool enable) +{ + Q_D(ItemSync); + d->mStreaming = enable; +} + +void ItemSync::deliveryDone() +{ + Q_D(ItemSync); + Q_ASSERT(d->mStreaming); + d->mDeliveryDone = true; + d->execute(); +} + +void ItemSync::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during ItemSync: " << job->errorString(); + // pretend there were no errors + Akonadi::Job::removeSubjob(job); + // propagate the first error we got but continue, we might still be fed with stuff from a resource + if (!error()) { + setError(job->error()); + setErrorText(job->errorText()); + } + } else { + Akonadi::Job::slotResult(job); + } +} + +void ItemSync::rollback() +{ + Q_D(ItemSync); + qCDebug(AKONADICORE_LOG) << "The item sync is being rolled-back."; + setError(UserCanceled); + if (d->mCurrentTransaction) { + d->mCurrentTransaction->rollback(); + } + d->mDeliveryDone = true; // user won't deliver more data + d->execute(); // end this in an ordered way, since we have an error set no real change will be done +} + +void ItemSync::setTransactionMode(ItemSync::TransactionMode mode) +{ + Q_D(ItemSync); + d->mTransactionMode = mode; +} + +int ItemSync::batchSize() const +{ + Q_D(const ItemSync); + return d->mBatchSize; +} + +void ItemSync::setBatchSize(int size) +{ + Q_D(ItemSync); + d->mBatchSize = size; +} + +ItemSync::MergeMode ItemSync::mergeMode() const +{ + Q_D(const ItemSync); + return d->mMergeMode; +} + +void ItemSync::setMergeMode(MergeMode mergeMode) +{ + Q_D(ItemSync); + d->mMergeMode = mergeMode; +} + +#include "moc_itemsync.cpp" diff --git a/src/core/itemsync.h b/src/core/itemsync.h new file mode 100644 index 0000000..91e146e --- /dev/null +++ b/src/core/itemsync.h @@ -0,0 +1,256 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class ItemFetchScope; +class ItemSyncPrivate; + +/** + * @short Syncs between items known to a client (usually a resource) and the Akonadi storage. + * + * Remote Id must only be set by the resource storing the item, other clients + * should leave it empty, since the resource responsible for the target collection + * will be notified about the addition and then create a suitable remote Id. + * + * There are two different forms of ItemSync usage: + * - Full-Sync: meaning the client provides all valid items, i.e. any item not + * part of the list but currently stored in Akonadi will be removed + * - Incremental-Sync: meaning the client provides two lists, one for items which + * are new or modified and one for items which should be removed. Any item not + * part of either list but currently stored in Akonadi will not be changed. + * + * @note This is provided for convenience to implement "save all" like behavior, + * however it is strongly recommended to use single item jobs whenever + * possible, e.g. ItemCreateJob, ItemModifyJob and ItemDeleteJob + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT ItemSync : public Job +{ + Q_OBJECT + +public: + enum MergeMode { + RIDMerge, + GIDMerge, + }; + + /** + * Creates a new item synchronizer. + * + * @param collection The collection we are syncing. + * @param parent The parent object. + */ + explicit ItemSync(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the item synchronizer. + */ + ~ItemSync() override; + + /** + * Sets the full item list for the collection. + * + * Usually the result of a full item listing. + * + * @warning If the client using this is a resource, all items must have + * a valid remote identifier. + * + * @param items A list of items. + */ + void setFullSyncItems(const Item::List &items); + + /** + * Set the amount of items which you are going to return in total + * by using the setFullSyncItems()/setIncrementalSyncItems() methods. + * + * @warning By default the item sync will automatically end once + * sufficient items have been provided. + * To disable this use setDisableAutomaticDeliveryDone + * + * @see setDisableAutomaticDeliveryDone + * @param amount The amount of items in total. + */ + void setTotalItems(int amount); + + /** + Enable item streaming. Item streaming means that the items delivered by setXItems() calls + are delivered in chunks and you manually indicate when all items have been delivered + by calling deliveryDone(). + @param enable @c true to enable item streaming + */ + void setStreamingEnabled(bool enable); + + /** + Notify ItemSync that all remote items have been delivered. + Only call this in streaming mode. + */ + void deliveryDone(); + + /** + * Sets the item lists for incrementally syncing the collection. + * + * Usually the result of an incremental remote item listing. + * + * @warning If the client using this is a resource, all items must have + * a valid remote identifier. + * + * @param changedItems A list of items added or changed by the client. + * @param removedItems A list of items deleted by the client. + */ + void setIncrementalSyncItems(const Item::List &changedItems, const Item::List &removedItems); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + + /** + * Aborts the sync process and rolls back all not yet committed transactions. + * Use this if an external error occurred during the sync process (such as the + * user canceling it). + * @since 4.5 + */ + void rollback(); + + /** + * Transaction mode used by ItemSync. + * @since 4.6 + */ + enum TransactionMode { + SingleTransaction, ///< Use a single transaction for the entire sync process (default), provides maximum consistency ("all or nothing") and best + ///< performance + MultipleTransactions, ///< Use one transaction per chunk of delivered items, good compromise between the other two when using streaming + NoTransaction ///< Use no transaction at all, provides highest responsiveness (might therefore feel faster even when actually taking slightly longer), + ///< no consistency guaranteed (can fail anywhere in the sync process) + }; + + /** + * Set the transaction mode to use for this sync. + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @param mode the transaction mode to use + * @since 4.6 + */ + void setTransactionMode(TransactionMode mode); + + /** + * Minimum number of items required to start processing in streaming mode. + * When MultipleTransactions is used, one transaction per batch will be created. + * + * @see setBatchSize() + * @since 4.14 + */ + Q_REQUIRED_RESULT int batchSize() const; + + /** + * Set the batch size. + * + * The default is 10. + * + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @see batchSize() + * @since 4.14 + */ + void setBatchSize(int); + + /** + * Disables the automatic completion of the item sync, + * based on the number of delivered items. + * + * This ensures that the item sync only finishes once deliveryDone() + * is called, while still making it possible to use the progress + * reporting of the ItemSync. + * + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @see setTotalItems + * @since 4.14 + */ + void setDisableAutomaticDeliveryDone(bool disable); + + /** + * Returns current merge mode + * + * @see setMergeMode() + * @since 5.1 + */ + Q_REQUIRED_RESULT MergeMode mergeMode() const; + + /** + * Set what merge method should be used for next ItemSync run + * + * By default ItemSync uses RIDMerge method. + * + * See ItemCreateJob for details on Item merging. + * + * @note You must call this method before starting the sync, changes afterwards lead to undefined results. + * @see mergeMode + * @since 4.14.11 + */ + void setMergeMode(MergeMode mergeMode); + +Q_SIGNALS: + /** + * Signals the resource that new items can be delivered. + * @param remainingBatchSize the number of items required to complete the batch (typically the same as batchSize()) + * + * @since 4.14 + */ + void readyForNextBatch(int remainingBatchSize); + + /** + * @internal + * Emitted whenever a transaction is committed. This is for testing only. + * + * @since 4.14 + */ + void transactionCommitted(); + +protected: + void doStart() override; + void slotResult(KJob *job) override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(ItemSync) + + Q_PRIVATE_SLOT(d_func(), void slotLocalListDone(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotTransactionResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void slotItemsReceived(const Akonadi::Item::List &)) + /// @endcond +}; + +} + diff --git a/src/core/jobs/agentinstancecreatejob.cpp b/src/core/jobs/agentinstancecreatejob.cpp new file mode 100644 index 0000000..59ad396 --- /dev/null +++ b/src/core/jobs/agentinstancecreatejob.cpp @@ -0,0 +1,190 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstancecreatejob.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "agentmanager_p.h" +#include "controlinterface.h" +#include "kjobprivatebase_p.h" +#include "servermanager.h" +#include + +#include + +#include + +#ifdef Q_OS_UNIX +#include +#include +#endif + +using namespace Akonadi; + +static const int safetyTimeout = 10000; // ms + +namespace Akonadi +{ +/** + * @internal + */ +class AgentInstanceCreateJobPrivate : public KJobPrivateBase +{ + Q_OBJECT +public: + explicit AgentInstanceCreateJobPrivate(AgentInstanceCreateJob *parent) + : q(parent) + , safetyTimer(new QTimer(parent)) + { + connect(AgentManager::self(), &AgentManager::instanceAdded, this, &AgentInstanceCreateJobPrivate::agentInstanceAdded); + connect(safetyTimer, &QTimer::timeout, this, &AgentInstanceCreateJobPrivate::timeout); + } + + void agentInstanceAdded(const AgentInstance &instance) const + { + if (agentInstance == instance && !tooLate) { + safetyTimer->stop(); + if (doConfig) { + // return from dbus call first before doing the next one + QTimer::singleShot(0, this, &AgentInstanceCreateJobPrivate::doConfigure); + } else { + q->emitResult(); + } + } + } + + void doConfigure() + { + auto agentControlIface = + new org::freedesktop::Akonadi::Agent::Control(ServerManager::agentServiceName(ServerManager::Agent, agentInstance.identifier()), + QStringLiteral("/"), + QDBusConnection::sessionBus(), + q); + if (!agentControlIface || !agentControlIface->isValid()) { + delete agentControlIface; + + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to access D-Bus interface of created agent.")); + q->emitResult(); + return; + } + + connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogAccepted, this, [agentControlIface, this]() { + agentControlIface->deleteLater(); + q->emitResult(); + }); + connect(agentControlIface, &org::freedesktop::Akonadi::Agent::Control::configurationDialogRejected, this, [agentControlIface, this]() { + agentControlIface->deleteLater(); + AgentManager::self()->removeInstance(agentInstance); + q->emitResult(); + }); + + agentInstance.configure(parentWidget); + } + + void timeout() + { + tooLate = true; + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Agent instance creation timed out.")); + q->emitResult(); + } + + void doStart() override; + + AgentInstanceCreateJob *const q; + AgentType agentType; + QString agentTypeId; + AgentInstance agentInstance; + QWidget *parentWidget = nullptr; + QTimer *const safetyTimer; + bool doConfig = false; + bool tooLate = false; +}; + +} // namespace Akonadi + +AgentInstanceCreateJob::AgentInstanceCreateJob(const AgentType &agentType, QObject *parent) + : KJob(parent) + , d(new AgentInstanceCreateJobPrivate(this)) +{ + d->agentType = agentType; +} + +AgentInstanceCreateJob::AgentInstanceCreateJob(const QString &typeId, QObject *parent) + : KJob(parent) + , d(new AgentInstanceCreateJobPrivate(this)) +{ + d->agentTypeId = typeId; +} + +AgentInstanceCreateJob::~AgentInstanceCreateJob() +{ + delete d; +} + +void AgentInstanceCreateJob::configure(QWidget *parent) +{ + d->parentWidget = parent; + d->doConfig = true; +} + +AgentInstance AgentInstanceCreateJob::instance() const +{ + return d->agentInstance; +} + +void AgentInstanceCreateJob::start() +{ + d->start(); +} + +void AgentInstanceCreateJobPrivate::doStart() +{ + if (!agentType.isValid() && !agentTypeId.isEmpty()) { + agentType = AgentManager::self()->type(agentTypeId); + } + + if (!agentType.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to obtain agent type '%1'.", agentTypeId)); + QTimer::singleShot(0, q, &AgentInstanceCreateJob::emitResult); + return; + } + + agentInstance = AgentManager::self()->d->createInstance(agentType); + if (!agentInstance.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to create agent instance.")); + QTimer::singleShot(0, q, &AgentInstanceCreateJob::emitResult); + } else { + int timeout = safetyTimeout; +#ifdef Q_OS_UNIX + // Increate the timeout when valgrinding the agent, because that slows down things a log. + QString agentValgrind = QString::fromLocal8Bit(qgetenv("AKONADI_VALGRIND")); + if (!agentValgrind.isEmpty() && agentType.identifier().contains(agentValgrind)) { + timeout *= 15; + } +#endif + // change the timeout when debugging the agent, because we need time to start the debugger + const QString agentDebugging = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_WAIT")); + if (!agentDebugging.isEmpty()) { + // we are debugging + const QString agentDebuggingTimeout = QString::fromLocal8Bit(qgetenv("AKONADI_DEBUG_TIMEOUT")); + if (agentDebuggingTimeout.isEmpty()) { + // use default value of 150 seconds (the same as "valgrinding", this has to be checked) + timeout = 15 * safetyTimeout; + } else { + // use own value + timeout = agentDebuggingTimeout.toInt(); + } + } + safetyTimer->start(timeout); + } +} + +#include "agentinstancecreatejob.moc" diff --git a/src/core/jobs/agentinstancecreatejob.h b/src/core/jobs/agentinstancecreatejob.h new file mode 100644 index 0000000..7d05def --- /dev/null +++ b/src/core/jobs/agentinstancecreatejob.h @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agenttype.h" +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +class AgentInstance; +class AgentInstanceCreateJobPrivate; + +/** + * @short Job for creating new agent instances. + * + * This class encapsulates the procedure of creating a new agent instance + * and optionally configuring it immediately. + * + * @code + * + * MyClass::MyClass( QWidget *parent ) + * : QWidget( parent ) + * { + * // Get agent type object + * Akonadi::AgentType type = Akonadi::AgentManager::self()->type( "akonadi_vcard_resource" ); + * + * Akonadi::AgentInstanceCreateJob *job = new Akonadi::AgentInstanceCreateJob( type ); + * connect( job, SIGNAL(result(KJob*)), + * this, SLOT(slotCreated(KJob*)) ); + * + * // use this widget as parent for the config dialog + * job->configure( this ); + * + * job->start(); + * } + * + * ... + * + * void MyClass::slotCreated( KJob *job ) + * { + * Akonadi::AgentInstanceCreateJob *createJob = static_cast( job ); + * + * qDebug() << "Created agent instance:" << createJob->instance().identifier(); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT AgentInstanceCreateJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new agent instance create job. + * + * @param type The type of the agent to create. + * @param parent The parent object. + */ + explicit AgentInstanceCreateJob(const AgentType &type, QObject *parent = nullptr); + + /** + * Creates a new agent instance create job. + * + * @param typeId The identifier of type of the agent to create. + * @param parent The parent object. + * @since 4.5 + */ + explicit AgentInstanceCreateJob(const QString &typeId, QObject *parent = nullptr); + + /** + * Destroys the agent instance create job. + */ + ~AgentInstanceCreateJob() override; + + /** + * Setup the job to show agent configuration dialog once the agent instance + * has been successfully started. + * @param parent The parent window for the configuration dialog. + */ + void configure(QWidget *parent = nullptr); + + /** + * Returns the AgentInstance object of the newly created agent instance. + */ + Q_REQUIRED_RESULT AgentInstance instance() const; + + /** + * Starts the instance creation. + */ + void start() override; + +private: + /// @cond PRIVATE + friend class Akonadi::AgentInstanceCreateJobPrivate; + AgentInstanceCreateJobPrivate *const d; + /// @endcond +}; + +} + diff --git a/src/core/jobs/collectionattributessynchronizationjob.cpp b/src/core/jobs/collectionattributessynchronizationjob.cpp new file mode 100644 index 0000000..3258a78 --- /dev/null +++ b/src/core/jobs/collectionattributessynchronizationjob.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "collectionattributessynchronizationjob.h" +#include "akonadicore_debug.h" +#include "kjobprivatebase_p.h" +#include "servermanager.h" +#include + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collection.h" + +#include + +#include +#include + +namespace Akonadi +{ +class CollectionAttributesSynchronizationJobPrivate : public KJobPrivateBase +{ + Q_OBJECT + +public: + explicit CollectionAttributesSynchronizationJobPrivate(CollectionAttributesSynchronizationJob *parent) + : q(parent) + { + connect(&safetyTimer, &QTimer::timeout, this, &CollectionAttributesSynchronizationJobPrivate::slotTimeout); + safetyTimer.setInterval(std::chrono::seconds{5}); + safetyTimer.setSingleShot(false); + } + + void doStart() override; + + CollectionAttributesSynchronizationJob *const q; + AgentInstance instance; + Collection collection; + QDBusInterface *interface = nullptr; + QTimer safetyTimer; + int timeoutCount = 0; + static const int timeoutCountLimit; + +private Q_SLOTS: + void slotSynchronized(qlonglong /*id*/); + void slotTimeout(); +}; + +const int CollectionAttributesSynchronizationJobPrivate::timeoutCountLimit = 2; + +CollectionAttributesSynchronizationJob::CollectionAttributesSynchronizationJob(const Collection &collection, QObject *parent) + : KJob(parent) + , d(new CollectionAttributesSynchronizationJobPrivate(this)) +{ + d->instance = AgentManager::self()->instance(collection.resource()); + d->collection = collection; +} + +CollectionAttributesSynchronizationJob::~CollectionAttributesSynchronizationJob() +{ + delete d; +} + +void CollectionAttributesSynchronizationJob::start() +{ + d->start(); +} + +void CollectionAttributesSynchronizationJobPrivate::doStart() +{ + if (!collection.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Invalid collection instance.")); + q->emitResult(); + return; + } + + if (!instance.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Invalid resource instance.")); + q->emitResult(); + return; + } + + interface = new QDBusInterface(ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Resource"), + QDBusConnection::sessionBus(), + this); + connect(interface, SIGNAL(attributesSynchronized(qlonglong)), this, SLOT(slotSynchronized(qlonglong))); // clazy:exclude=old-style-connect + + if (interface->isValid()) { + const QDBusMessage reply = interface->call(QStringLiteral("synchronizeCollectionAttributes"), collection.id()); + if (reply.type() == QDBusMessage::ErrorMessage) { + // This means that the resource doesn't provide a synchronizeCollectionAttributes method, so we just finish the job + q->emitResult(); + return; + } + safetyTimer.start(); + } else { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to obtain D-Bus interface for resource '%1'", instance.identifier())); + q->emitResult(); + return; + } +} + +void CollectionAttributesSynchronizationJobPrivate::slotSynchronized(qlonglong id) +{ + if (id == collection.id()) { + disconnect(interface, SIGNAL(attributesSynchronized(qlonglong)), this, SLOT(slotSynchronized(qlonglong))); // clazy:exclude=old-style-connect + safetyTimer.stop(); + q->emitResult(); + } +} + +void CollectionAttributesSynchronizationJobPrivate::slotTimeout() +{ + instance = AgentManager::self()->instance(instance.identifier()); + timeoutCount++; + + if (timeoutCount > timeoutCountLimit) { + safetyTimer.stop(); + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Collection attributes synchronization timed out.")); + q->emitResult(); + return; + } + + if (instance.status() == AgentInstance::Idle) { + // try again, we might have lost the synchronized() signal + qCDebug(AKONADICORE_LOG) << "collection attributes" << collection.id() << instance.identifier(); + interface->call(QStringLiteral("synchronizeCollectionAttributes"), collection.id()); + } +} + +} // namespace Akonadi + +#include "collectionattributessynchronizationjob.moc" diff --git a/src/core/jobs/collectionattributessynchronizationjob.h b/src/core/jobs/collectionattributessynchronizationjob.h new file mode 100644 index 0000000..ab91ec1 --- /dev/null +++ b/src/core/jobs/collectionattributessynchronizationjob.h @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +class Collection; +class CollectionAttributesSynchronizationJobPrivate; + +/** + * @short Job that synchronizes the attributes of a collection. + * + * This job will trigger a resource to synchronize the attributes of + * a collection based on what the backend is reporting to store them in the + * Akonadi storage. + * + * Example: + * + * @code + * using namespace Akonadi; + * + * const Collection collection = ...; + * + * CollectionAttributesSynchronizationJob *job = new CollectionAttributesSynchronizationJob( collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(synchronizationFinished(KJob*)) ); + * + * @endcode + * + * @note This is a KJob not an Akonadi::Job, so it wont auto-start! + * + * @author Volker Krause + * @since 4.6 + */ +class AKONADICORE_EXPORT CollectionAttributesSynchronizationJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new synchronization job for the given collection. + * + * @param collection The collection to synchronize. + */ + explicit CollectionAttributesSynchronizationJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the synchronization job. + */ + ~CollectionAttributesSynchronizationJob() override; + + /* reimpl */ + void start() override; + +private: + /// @cond PRIVATE + CollectionAttributesSynchronizationJobPrivate *const d; + friend class CollectionAttributesSynchronizationJobPrivate; + /// @endcond +}; + +} + diff --git a/src/core/jobs/collectioncopyjob.cpp b/src/core/jobs/collectioncopyjob.cpp new file mode 100644 index 0000000..64aaa91 --- /dev/null +++ b/src/core/jobs/collectioncopyjob.cpp @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectioncopyjob.h" +#include "collection.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::CollectionCopyJobPrivate : public JobPrivate +{ +public: + explicit CollectionCopyJobPrivate(CollectionCopyJob *parent) + : JobPrivate(parent) + { + } + + Collection mSource; + Collection mTarget; + + QString jobDebuggingString() const override; +}; + +QString Akonadi::CollectionCopyJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("copy collection from %1 to %2").arg(mSource.id()).arg(mTarget.id()); +} + +CollectionCopyJob::CollectionCopyJob(const Collection &source, const Collection &target, QObject *parent) + : Job(new CollectionCopyJobPrivate(this), parent) +{ + Q_D(CollectionCopyJob); + + d->mSource = source; + d->mTarget = target; +} + +CollectionCopyJob::~CollectionCopyJob() +{ +} + +void CollectionCopyJob::doStart() +{ + Q_D(CollectionCopyJob); + + if (!d->mSource.isValid() && d->mSource.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid collection to copy")); + emitResult(); + return; + } + if (!d->mTarget.isValid() && d->mTarget.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid destination collection")); + emitResult(); + return; + } + d->sendCommand(Protocol::CopyCollectionCommandPtr::create(d->mSource.id(), d->mTarget.id())); +} + +bool CollectionCopyJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::CopyCollection) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/collectioncopyjob.h b/src/core/jobs/collectioncopyjob.h new file mode 100644 index 0000000..1ef5512 --- /dev/null +++ b/src/core/jobs/collectioncopyjob.h @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class CollectionCopyJobPrivate; + +/** + * @short Job that copies a collection into another collection in the Akonadi storage. + * + * This job copies a single collection into a specified target collection. + * + * @code + * + * Akonadi::Collection source = ... + * Akonadi::Collection target = ... + * + * Akonadi::CollectionCopyJob *job = new Akonadi::CollectionCopyJob( source, target ); + * connect( job, SIGNAL(result(KJob*)), SLOT(copyFinished(KJob*)) ); + * + * ... + * + * MyClass::copyFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Copied successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionCopyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection copy job to copy the given @p source collection into @p target. + * + * @param source The collection to copy. + * @param target The target collection. + * @param parent The parent object. + */ + CollectionCopyJob(const Collection &source, const Collection &target, QObject *parent = nullptr); + + /** + * Destroys the collection copy job. + */ + ~CollectionCopyJob() override; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(CollectionCopyJob) +}; + +} + diff --git a/src/core/jobs/collectioncreatejob.cpp b/src/core/jobs/collectioncreatejob.cpp new file mode 100644 index 0000000..5d1ac9b --- /dev/null +++ b/src/core/jobs/collectioncreatejob.cpp @@ -0,0 +1,115 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectioncreatejob.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::CollectionCreateJobPrivate : public JobPrivate +{ +public: + explicit CollectionCreateJobPrivate(CollectionCreateJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const override; + Collection mCollection; +}; + +QString Akonadi::CollectionCreateJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Create collection: %1").arg(mCollection.id()); +} + +CollectionCreateJob::CollectionCreateJob(const Collection &collection, QObject *parent) + : Job(new CollectionCreateJobPrivate(this), parent) +{ + Q_D(CollectionCreateJob); + + d->mCollection = collection; +} + +CollectionCreateJob::~CollectionCreateJob() +{ +} + +void CollectionCreateJob::doStart() +{ + Q_D(CollectionCreateJob); + if (d->mCollection.parentCollection().id() < 0 && d->mCollection.parentCollection().remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid parent")); + emitResult(); + return; + } + + auto cmd = Protocol::CreateCollectionCommandPtr::create(); + cmd->setName(d->mCollection.name()); + cmd->setParent(ProtocolHelper::entityToScope(d->mCollection.parentCollection())); + cmd->setMimeTypes(d->mCollection.contentMimeTypes()); + cmd->setRemoteId(d->mCollection.remoteId()); + cmd->setRemoteRevision(d->mCollection.remoteRevision()); + cmd->setIsVirtual(d->mCollection.isVirtual()); + cmd->setEnabled(d->mCollection.enabled()); + cmd->setDisplayPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListDisplay))); + cmd->setSyncPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListSync))); + cmd->setIndexPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListIndex))); + cmd->setCachePolicy(ProtocolHelper::cachePolicyToProtocol(d->mCollection.cachePolicy())); + Protocol::Attributes attrs; + const Akonadi::Attribute::List attrList = d->mCollection.attributes(); + for (Attribute *attr : attrList) { + attrs.insert(attr->type(), attr->serialized()); + } + cmd->setAttributes(attrs); + + d->sendCommand(cmd); + emitWriteFinished(); +} + +Collection CollectionCreateJob::collection() const +{ + Q_D(const CollectionCreateJob); + + return d->mCollection; +} + +bool CollectionCreateJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(CollectionCreateJob); + + if (!response->isResponse()) { + return Job::doHandleResponse(tag, response); + } + + if (response->type() == Protocol::Command::FetchCollections) { + const auto &resp = Protocol::cmdCast(response); + Collection col = ProtocolHelper::parseCollection(resp); + if (!col.isValid()) { + setError(Unknown); + setErrorText(i18n("Failed to parse Collection from response")); + return true; + } + col.setParentCollection(d->mCollection.parentCollection()); + col.setName(d->mCollection.name()); + col.setRemoteId(d->mCollection.remoteId()); + col.setRemoteRevision(d->mCollection.remoteRevision()); + col.setVirtual(d->mCollection.isVirtual()); + d->mCollection = col; + return false; + } + + if (response->type() == Protocol::Command::CreateCollection) { + return true; + } + + return Job::doHandleResponse(tag, response); +} diff --git a/src/core/jobs/collectioncreatejob.h b/src/core/jobs/collectioncreatejob.h new file mode 100644 index 0000000..632b905 --- /dev/null +++ b/src/core/jobs/collectioncreatejob.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class CollectionCreateJobPrivate; + +/** + * @short Job that creates a new collection in the Akonadi storage. + * + * This job creates a new collection with all the set properties. + * You have to use setParentCollection() to define the collection the + * new collection shall be located in. + * + * @code + * + * // create a new top-level collection + * Akonadi::Collection collection; + * collection.setParentCollection( Collection::root() ); + * collection.setName( "Events" ); + * collection.setContentMimeTypes( QStringList( "text/calendar" ) ); + * + * Akonadi::CollectionCreateJob *job = new Akonadi::CollectionCreateJob( collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(createResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionCreateJob : public Job +{ + Q_OBJECT +public: + /** + * Creates a new collection create job. + * + * @param collection The new collection. @p collection must have a parent collection + * set with a unique identifier. If a resource context is specified in the current session + * (that is you are using it within Akonadi::ResourceBase), the parent collection can be + * identified by its remote identifier as well. + * @param parent The parent object. + */ + explicit CollectionCreateJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the collection create job. + */ + ~CollectionCreateJob() override; + + /** + * Returns the created collection if the job was executed successfully. + */ + Q_REQUIRED_RESULT Collection collection() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(CollectionCreateJob) +}; + +} + diff --git a/src/core/jobs/collectiondeletejob.cpp b/src/core/jobs/collectiondeletejob.cpp new file mode 100644 index 0000000..b56f064 --- /dev/null +++ b/src/core/jobs/collectiondeletejob.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiondeletejob.h" +#include "collection.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::CollectionDeleteJobPrivate : public JobPrivate +{ +public: + explicit CollectionDeleteJobPrivate(CollectionDeleteJob *parent) + : JobPrivate(parent) + { + } + QString jobDebuggingString() const override; + + Collection mCollection; +}; + +QString Akonadi::CollectionDeleteJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Delete Collection id: %1").arg(mCollection.id()); +} + +CollectionDeleteJob::CollectionDeleteJob(const Collection &collection, QObject *parent) + : Job(new CollectionDeleteJobPrivate(this), parent) +{ + Q_D(CollectionDeleteJob); + + d->mCollection = collection; +} + +CollectionDeleteJob::~CollectionDeleteJob() +{ +} + +void CollectionDeleteJob::doStart() +{ + Q_D(CollectionDeleteJob); + + if (!d->mCollection.isValid() && d->mCollection.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid collection")); + emitResult(); + return; + } + + d->sendCommand(Protocol::DeleteCollectionCommandPtr::create(ProtocolHelper::entityToScope(d->mCollection))); +} + +bool CollectionDeleteJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::DeleteCollection) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/collectiondeletejob.h b/src/core/jobs/collectiondeletejob.h new file mode 100644 index 0000000..0888182 --- /dev/null +++ b/src/core/jobs/collectiondeletejob.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class CollectionDeleteJobPrivate; + +/** + * @short Job that deletes a collection in the Akonadi storage. + * + * This job deletes a collection and all its sub-collections as well as all associated content. + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionDeleteJob *job = new Akonadi::CollectionDeleteJob( collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*)) ); + * + * @endcode + * + * @note This job deletes the data from the backend storage. To delete the collection + * from the Akonadi storage only, leaving the backend storage unchanged, delete + * the Agent instead, as follows. (Note that if it's a sub-collection, deleting + * the agent will also delete its parent collection; in this case the only + * option is to delete the sub-collection data in both Akonadi and backend + * storage.) + * + * @code + * + * const Akonadi::AgentInstance instance = + * Akonadi::AgentManager::self()->instance( collection.resource() ); + * if ( instance.isValid() ) { + * Akonadi::AgentManager::self()->removeInstance( instance ); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionDeleteJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection delete job. The collection needs to either have a unique + * identifier or a remote identifier set. Note that using a remote identifier only works + * in a resource context (that is from within ResourceBase), as remote identifiers + * are not guaranteed to be globally unique. + * + * @param collection The collection to delete. + * @param parent The parent object. + */ + explicit CollectionDeleteJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the collection delete job. + */ + ~CollectionDeleteJob() override; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(CollectionDeleteJob) +}; + +} + diff --git a/src/core/jobs/collectionfetchjob.cpp b/src/core/jobs/collectionfetchjob.cpp new file mode 100644 index 0000000..8cbf2bb --- /dev/null +++ b/src/core/jobs/collectionfetchjob.cpp @@ -0,0 +1,411 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionfetchjob.h" + +#include "collection_p.h" +#include "collectionfetchscope.h" +#include "collectionutils.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include "akonadicore_debug.h" + +#include + +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::CollectionFetchJobPrivate : public JobPrivate +{ +public: + explicit CollectionFetchJobPrivate(CollectionFetchJob *parent) + : JobPrivate(parent) + { + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + } + + void init() + { + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { + timeout(); + }); + } + + Q_DECLARE_PUBLIC(CollectionFetchJob) + + CollectionFetchJob::Type mType = CollectionFetchJob::Base; + Collection mBase; + Collection::List mBaseList; + Collection::List mCollections; + CollectionFetchScope mScope; + Collection::List mPendingCollections; + QTimer mEmitTimer; + bool mBasePrefetch = false; + Collection::List mPrefetchList; + + void aboutToFinish() override + { + timeout(); + } + + void timeout() + { + Q_Q(CollectionFetchJob); + + mEmitTimer.stop(); // in case we are called by result() + if (!mPendingCollections.isEmpty()) { + if (!q->error() || mScope.ignoreRetrievalErrors()) { + Q_EMIT q->collectionsReceived(mPendingCollections); + } + mPendingCollections.clear(); + } + } + + void subJobCollectionReceived(const Akonadi::Collection::List &collections) + { + mPendingCollections += collections; + if (!mEmitTimer.isActive()) { + mEmitTimer.start(); + } + } + + QString jobDebuggingString() const override + { + if (mBase.isValid()) { + return QStringLiteral("Collection Id %1").arg(mBase.id()); + } else if (CollectionUtils::hasValidHierarchicalRID(mBase)) { + // return QLatin1String("(") + ProtocolHelper::hierarchicalRidToScope(mBase).hridChain().join(QLatin1String(", ")) + QLatin1Char(')'); + return QStringLiteral("HRID chain"); + } else { + return QStringLiteral("Collection RemoteId %1").arg(mBase.remoteId()); + } + } + + bool jobFailed(KJob *job) + { + Q_Q(CollectionFetchJob); + if (mScope.ignoreRetrievalErrors()) { + int error = job->error(); + if (error && !q->error()) { + q->setError(error); + q->setErrorText(job->errorText()); + } + + return error == Job::ConnectionFailed || error == Job::ProtocolVersionMismatch || error == Job::UserCanceled; + } else { + return job->error(); + } + } +}; + +CollectionFetchJob::CollectionFetchJob(const Collection &collection, Type type, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + d->mBase = collection; + d->mType = type; +} + +CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + Q_ASSERT(!cols.isEmpty()); + if (cols.size() == 1) { + d->mBase = cols.first(); + } else { + d->mBaseList = cols; + } + d->mType = CollectionFetchJob::Base; +} + +CollectionFetchJob::CollectionFetchJob(const Collection::List &cols, Type type, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + Q_ASSERT(!cols.isEmpty()); + if (cols.size() == 1) { + d->mBase = cols.first(); + } else { + d->mBaseList = cols; + } + d->mType = type; +} + +CollectionFetchJob::CollectionFetchJob(const QList &cols, Type type, QObject *parent) + : Job(new CollectionFetchJobPrivate(this), parent) +{ + Q_D(CollectionFetchJob); + d->init(); + + Q_ASSERT(!cols.isEmpty()); + if (cols.size() == 1) { + d->mBase = Collection(cols.first()); + } else { + for (Collection::Id id : cols) { + d->mBaseList.append(Collection(id)); + } + } + d->mType = type; +} + +CollectionFetchJob::~CollectionFetchJob() = default; + +Akonadi::Collection::List CollectionFetchJob::collections() const +{ + Q_D(const CollectionFetchJob); + + return d->mCollections; +} + +void CollectionFetchJob::doStart() +{ + Q_D(CollectionFetchJob); + + if (!d->mBaseList.isEmpty()) { + if (d->mType == Recursive) { + // Because doStart starts several subjobs and @p cols could contain descendants of + // other elements in the list, if type is Recursive, we could end up with duplicates in the result. + // To fix this we require an initial fetch of @p cols with Base and RetrieveAncestors, + // Iterate over that result removing intersections and then perform the Recursive fetch on + // the remainder. + d->mBasePrefetch = true; + // No need to connect to the collectionsReceived signal here. This job is internal. The + // result needs to be filtered through filterDescendants before it is useful. + new CollectionFetchJob(d->mBaseList, NonOverlappingRoots, this); + } else if (d->mType == NonOverlappingRoots) { + for (const Collection &col : std::as_const(d->mBaseList)) { + // No need to connect to the collectionsReceived signal here. This job is internal. The (aggregated) + // result needs to be filtered through filterDescendants before it is useful. + auto subJob = new CollectionFetchJob(col, Base, this); + subJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + } + } else { + for (const Collection &col : std::as_const(d->mBaseList)) { + auto subJob = new CollectionFetchJob(col, d->mType, this); + connect(subJob, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { + d->subJobCollectionReceived(cols); + }); + subJob->setFetchScope(fetchScope()); + } + } + return; + } + + if (!d->mBase.isValid() && d->mBase.remoteId().isEmpty()) { + setError(Unknown); + setErrorText(i18n("Invalid collection given.")); + emitResult(); + return; + } + + const auto cmd = Protocol::FetchCollectionsCommandPtr::create(ProtocolHelper::entityToScope(d->mBase)); + switch (d->mType) { + case Base: + cmd->setDepth(Protocol::FetchCollectionsCommand::BaseCollection); + break; + case Akonadi::CollectionFetchJob::FirstLevel: + cmd->setDepth(Protocol::FetchCollectionsCommand::ParentCollection); + break; + case Akonadi::CollectionFetchJob::Recursive: + cmd->setDepth(Protocol::FetchCollectionsCommand::AllCollections); + break; + default: + Q_ASSERT(false); + } + cmd->setResource(d->mScope.resource()); + cmd->setMimeTypes(d->mScope.contentMimeTypes()); + + switch (d->mScope.listFilter()) { + case CollectionFetchScope::Display: + cmd->setDisplayPref(true); + break; + case CollectionFetchScope::Sync: + cmd->setSyncPref(true); + break; + case CollectionFetchScope::Index: + cmd->setIndexPref(true); + break; + case CollectionFetchScope::Enabled: + cmd->setEnabled(true); + break; + case CollectionFetchScope::NoFilter: + break; + default: + Q_ASSERT(false); + } + + cmd->setFetchStats(d->mScope.includeStatistics()); + switch (d->mScope.ancestorRetrieval()) { + case CollectionFetchScope::None: + cmd->setAncestorsDepth(Protocol::Ancestor::NoAncestor); + break; + case CollectionFetchScope::Parent: + cmd->setAncestorsDepth(Protocol::Ancestor::ParentAncestor); + break; + case CollectionFetchScope::All: + cmd->setAncestorsDepth(Protocol::Ancestor::AllAncestors); + break; + } + if (d->mScope.ancestorRetrieval() != CollectionFetchScope::None) { + cmd->setAncestorsAttributes(d->mScope.ancestorFetchScope().attributes()); + } + + d->sendCommand(cmd); +} + +bool CollectionFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(CollectionFetchJob); + + if (d->mBasePrefetch || d->mType == NonOverlappingRoots) { + return false; + } + + if (!response->isResponse() || response->type() != Protocol::Command::FetchCollections) { + return Job::doHandleResponse(tag, response); + } + + const auto &resp = Protocol::cmdCast(response); + // Invalid response (no ID) means this was the last response + if (resp.id() == -1) { + return true; + } + + Collection collection = ProtocolHelper::parseCollection(resp, true); + if (!collection.isValid()) { + return false; + } + + collection.d_ptr->resetChangeLog(); + d->mCollections.append(collection); + d->mPendingCollections.append(collection); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); + } + + return false; +} + +static Collection::List filterDescendants(const Collection::List &list) +{ + Collection::List result; + + QVector> ids; + ids.reserve(list.count()); + for (const Collection &collection : list) { + QList ancestors; + Collection parent = collection.parentCollection(); + ancestors << parent.id(); + if (parent != Collection::root()) { + while (parent.parentCollection() != Collection::root()) { + parent = parent.parentCollection(); + QList::iterator i = std::lower_bound(ancestors.begin(), ancestors.end(), parent.id()); + ancestors.insert(i, parent.id()); + } + } + ids << ancestors; + } + + QSet excludeList; + for (const Collection &collection : list) { + int i = 0; + for (const QList &ancestors : std::as_const(ids)) { + if (std::binary_search(ancestors.cbegin(), ancestors.cend(), collection.id())) { + excludeList.insert(list.at(i).id()); + } + ++i; + } + } + + for (const Collection &collection : list) { + if (!excludeList.contains(collection.id())) { + result.append(collection); + } + } + + return result; +} + +void CollectionFetchJob::slotResult(KJob *job) +{ + Q_D(CollectionFetchJob); + + auto list = qobject_cast(job); + Q_ASSERT(job); + + if (d->mType == NonOverlappingRoots) { + d->mPrefetchList += list->collections(); + } else if (!d->mBasePrefetch) { + d->mCollections += list->collections(); + } + + if (d_ptr->mCurrentSubJob == job && !d->jobFailed(job)) { + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during CollectionFetchJob: " << job->errorString(); + } + d_ptr->mCurrentSubJob = nullptr; + removeSubjob(job); + QTimer::singleShot(0, this, [d]() { + d->startNext(); + }); + } else { + Job::slotResult(job); + } + + if (d->mBasePrefetch) { + d->mBasePrefetch = false; + const Collection::List roots = list->collections(); + Q_ASSERT(!hasSubjobs()); + if (!job->error()) { + for (const Collection &col : roots) { + auto subJob = new CollectionFetchJob(col, d->mType, this); + connect(subJob, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { + d->subJobCollectionReceived(cols); + }); + subJob->setFetchScope(fetchScope()); + } + } + // No result yet. + } else if (d->mType == NonOverlappingRoots) { + if (!d->jobFailed(job) && !hasSubjobs()) { + const Collection::List result = filterDescendants(d->mPrefetchList); + d->mPendingCollections += result; + d->mCollections = result; + d->delayedEmitResult(); + } + } else { + if (!d->jobFailed(job) && !hasSubjobs()) { + d->delayedEmitResult(); + } + } +} + +void CollectionFetchJob::setFetchScope(const CollectionFetchScope &scope) +{ + Q_D(CollectionFetchJob); + d->mScope = scope; +} + +CollectionFetchScope &CollectionFetchJob::fetchScope() +{ + Q_D(CollectionFetchJob); + return d->mScope; +} + +#include "moc_collectionfetchjob.cpp" diff --git a/src/core/jobs/collectionfetchjob.h b/src/core/jobs/collectionfetchjob.h new file mode 100644 index 0000000..49281e4 --- /dev/null +++ b/src/core/jobs/collectionfetchjob.h @@ -0,0 +1,179 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ +class CollectionFetchScope; +class CollectionFetchJobPrivate; + +/** + * @short Job that fetches collections from the Akonadi storage. + * + * This class can be used to retrieve the complete or partial collection tree + * from the Akonadi storage. This fetches collection data, not item data. + * + * @code + * + * using namespace Akonadi; + * + * // fetching all collections containing emails recursively, starting at the root collection + * CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + * job->fetchScope().setContentMimeTypes(QStringList() << "message/rfc822"); + * connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), + * this, SLOT(myCollectionsReceived(Akonadi::Collection::List))); + * connect(job, SIGNAL(result(KJob*)), this, SLOT(collectionFetchResult(KJob*))); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionFetchJob : public Job +{ + Q_OBJECT + +public: + /** + * Describes the type of fetch depth. + */ + enum Type { + Base, ///< Only fetch the base collection. + FirstLevel, ///< Only list direct sub-collections of the base collection. + Recursive, ///< List all sub-collections. + NonOverlappingRoots ///< List the roots of a list of fetched collections. @since 4.7 + }; + + /** + * Creates a new collection fetch job. If the given base collection + * has a unique identifier, this is used to identify the collection in the + * Akonadi server. If only a remote identifier is available the collection + * is identified using that, provided that a resource search context has + * been specified by calling setResource(). + * + * @internal + * For internal use only, if a remote identifier is set, the resource + * search context can be set globally using ResourceSelectJob. + * @endinternal + * + * @param collection The base collection for the listing. + * @param type The type of fetch depth. + * @param parent The parent object. + */ + explicit CollectionFetchJob(const Collection &collection, Type type = FirstLevel, QObject *parent = nullptr); + + /** + * Creates a new collection fetch job to retrieve a list of collections. + * If a given collection has a unique identifier, this is used to identify + * the collection in the Akonadi server. If only a remote identifier is + * available the collection is identified using that, provided that a + * resource search context has been specified by calling setResource(). + * + * @internal + * For internal use only, if a remote identifier is set, the resource + * search context can be set globally using ResourceSelectJob. + * @endinternal + * + * @param collections A list of collections to fetch. Must not be empty. + * @param parent The parent object. + */ + explicit CollectionFetchJob(const Collection::List &collections, QObject *parent = nullptr); + + /** + * Creates a new collection fetch job to retrieve a list of collections. + * If a given collection has a unique identifier, this is used to identify + * the collection in the Akonadi server. If only a remote identifier is + * available the collection is identified using that, provided that a + * resource search context has been specified by calling setResource(). + * + * @internal + * For internal use only, if a remote identifier is set, the resource + * search context can be set globally using ResourceSelectJob. + * @endinternal + * + * @param collections A list of collections to fetch. Must not be empty. + * @param type The type of fetch depth. + * @param parent The parent object. + * @todo KDE5 merge with ctor above. + * @since 4.7 + */ + CollectionFetchJob(const Collection::List &collections, Type type, QObject *parent = nullptr); + + /** + * Convenience ctor equivalent to CollectionFetchJob(const Collection::List &collections, Type type, QObject *parent = nullptr) + * @since 4.8 + * @param collections list of collection ids + * @param type fetch job type + * @param parent parent object + */ + explicit CollectionFetchJob(const QList &collections, Type type = Base, QObject *parent = nullptr); + + /** + * Destroys the collection fetch job. + */ + ~CollectionFetchJob() override; + + /** + * Returns the list of fetched collection. + */ + Q_REQUIRED_RESULT Collection::List collections() const; + + /** + * Sets the collection fetch scope. + * + * The CollectionFetchScope controls how much of a collection's data is + * fetched from the server as well as a filter to select which collections + * to fetch. + * + * @param fetchScope The new scope for collection fetch operations. + * + * @see fetchScope() + * @since 4.4 + */ + void setFetchScope(const CollectionFetchScope &fetchScope); + + /** + * Returns the collection fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the CollectionFetchScope documentation + * for an example. + * + * @return a reference to the current collection fetch scope + * + * @see setFetchScope() for replacing the current collection fetch scope + * @since 4.4 + */ + Q_REQUIRED_RESULT CollectionFetchScope &fetchScope(); + +Q_SIGNALS: + /** + * This signal is emitted whenever the job has received collections. + * + * @param collections The received collections. + */ + void collectionsReceived(const Akonadi::Collection::List &collections); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +protected Q_SLOTS: + /// @cond PRIVATE + void slotResult(KJob *job) override; + /// @endcond + +private: + Q_DECLARE_PRIVATE(CollectionFetchJob) +}; + +} + diff --git a/src/core/jobs/collectionmodifyjob.cpp b/src/core/jobs/collectionmodifyjob.cpp new file mode 100644 index 0000000..6999cdd --- /dev/null +++ b/src/core/jobs/collectionmodifyjob.cpp @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionmodifyjob.h" + +#include "changemediator_p.h" +#include "collection_p.h" +#include "collectionstatistics.h" +#include "job_p.h" +#include "persistentsearchattribute.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +using namespace Akonadi; + +class Akonadi::CollectionModifyJobPrivate : public JobPrivate +{ +public: + explicit CollectionModifyJobPrivate(CollectionModifyJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const override + { + return QStringLiteral("Collection Id %1").arg(mCollection.id()); + } + + Collection mCollection; +}; + +CollectionModifyJob::CollectionModifyJob(const Collection &collection, QObject *parent) + : Job(new CollectionModifyJobPrivate(this), parent) +{ + Q_D(CollectionModifyJob); + d->mCollection = collection; +} + +CollectionModifyJob::~CollectionModifyJob() +{ +} + +void CollectionModifyJob::doStart() +{ + Q_D(CollectionModifyJob); + + Protocol::ModifyCollectionCommandPtr cmd; + try { + cmd = Protocol::ModifyCollectionCommandPtr::create(ProtocolHelper::entityToScope(d->mCollection)); + } catch (const std::exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } + + if (d->mCollection.d_ptr->contentTypesChanged) { + cmd->setMimeTypes(d->mCollection.contentMimeTypes()); + } + if (d->mCollection.parentCollection().id() >= 0) { + cmd->setParentId(d->mCollection.parentCollection().id()); + } + const QString &collectionName = d->mCollection.name(); + if (!collectionName.isEmpty()) { + cmd->setName(collectionName); + } + if (!d->mCollection.remoteId().isNull()) { + cmd->setRemoteId(d->mCollection.remoteId()); + } + if (!d->mCollection.remoteRevision().isNull()) { + cmd->setRemoteRevision(d->mCollection.remoteRevision()); + } + if (d->mCollection.d_ptr->cachePolicyChanged) { + cmd->setCachePolicy(ProtocolHelper::cachePolicyToProtocol(d->mCollection.cachePolicy())); + } + if (d->mCollection.d_ptr->enabledChanged) { + cmd->setEnabled(d->mCollection.enabled()); + } + if (d->mCollection.d_ptr->listPreferenceChanged) { + cmd->setDisplayPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListDisplay))); + cmd->setSyncPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListSync))); + cmd->setIndexPref(ProtocolHelper::listPreference(d->mCollection.localListPreference(Collection::ListIndex))); + } + if (d->mCollection.d_ptr->mAttributeStorage.hasModifiedAttributes()) { + cmd->setAttributes(ProtocolHelper::attributesToProtocol(d->mCollection.d_ptr->mAttributeStorage.modifiedAttributes())); + } + if (auto attr = d->mCollection.attribute()) { + cmd->setPersistentSearchCollections(attr->queryCollections()); + cmd->setPersistentSearchQuery(attr->queryString()); + cmd->setPersistentSearchRecursive(attr->isRecursive()); + cmd->setPersistentSearchRemote(attr->isRemoteSearchEnabled()); + } + if (!d->mCollection.d_ptr->mAttributeStorage.deletedAttributes().empty()) { + cmd->setRemovedAttributes(d->mCollection.d_ptr->mAttributeStorage.deletedAttributes()); + } + + if (cmd->modifiedParts() == Protocol::ModifyCollectionCommand::None) { + emitResult(); + return; + } + + d->sendCommand(cmd); + + ChangeMediator::invalidateCollection(d->mCollection); +} + +bool CollectionModifyJob::doHandleResponse(qint64 tag, const Akonadi::Protocol::CommandPtr &response) +{ + Q_D(CollectionModifyJob); + + if (!response->isResponse() || response->type() != Protocol::Command::ModifyCollection) { + return Job::doHandleResponse(tag, response); + } + + d->mCollection.d_ptr->resetChangeLog(); + return true; +} + +Collection CollectionModifyJob::collection() const +{ + const Q_D(CollectionModifyJob); + return d->mCollection; +} diff --git a/src/core/jobs/collectionmodifyjob.h b/src/core/jobs/collectionmodifyjob.h new file mode 100644 index 0000000..fde4c55 --- /dev/null +++ b/src/core/jobs/collectionmodifyjob.h @@ -0,0 +1,105 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class CachePolicy; +class Collection; +class CollectionModifyJobPrivate; + +/** + * @short Job that modifies a collection in the Akonadi storage. + * + * This job modifies the properties of an existing collection. + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*)) ); + * + * @endcode + * + * If the collection has attributes, it is recommended only to supply values for + * any attributes whose values are to be updated. This will help to avoid + * potential clashes with other resources or applications which may happen to + * update the collection simultaneously. To avoid supplying attribute values which + * are not needed, create a new instance of the collection and explicitly set + * attributes to be updated, e.g. + * + * @code + * + * // Update the 'MyAttribute' attribute of 'collection'. + * Akonadi::Collection c( collection.id() ); + * MyAttribute *attribute = c.attribute( Collection::AddIfMissing ); + * if ( collection.hasAttribute() ) { + * *attribute = *collection.attribute(); + * } + * // Update the value of 'attribute' ... + * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( c ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*)) ); + * + * @endcode + * + * To update only the collection, and not change any attributes: + * + * @code + * + * // Update the cache policy for 'collection' to 'newPolicy'. + * Akonadi::Collection c( collection.id() ); + * c.setCachePolicy( newPolicy ); + * Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( c ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionModifyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection modify job for the given collection. The collection can be + * identified either by its unique identifier or its remote identifier. Since the remote + * identifier is not necessarily globally unique, identification by remote identifier only + * works inside a resource context (that is from within ResourceBase) and is therefore + * limited to one resource. + * + * @param collection The collection to modify. + * @param parent The parent object. + */ + explicit CollectionModifyJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the collection modify job. + */ + ~CollectionModifyJob() override; + + /** + * Returns the modified collection. + * + * @since 4.4 + */ + Q_REQUIRED_RESULT Collection collection() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(CollectionModifyJob) +}; + +} + diff --git a/src/core/jobs/collectionmovejob.cpp b/src/core/jobs/collectionmovejob.cpp new file mode 100644 index 0000000..b52084f --- /dev/null +++ b/src/core/jobs/collectionmovejob.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionmovejob.h" +#include "changemediator_p.h" +#include "collection.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::CollectionMoveJobPrivate : public JobPrivate +{ +public: + explicit CollectionMoveJobPrivate(CollectionMoveJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const override; + Collection destination; + Collection collection; + + Q_DECLARE_PUBLIC(CollectionMoveJob) +}; + +QString Akonadi::CollectionMoveJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Move collection from %1 to %2").arg(collection.id()).arg(destination.id()); +} + +CollectionMoveJob::CollectionMoveJob(const Collection &collection, const Collection &destination, QObject *parent) + : Job(new CollectionMoveJobPrivate(this), parent) +{ + Q_D(CollectionMoveJob); + d->destination = destination; + d->collection = collection; +} + +void CollectionMoveJob::doStart() +{ + Q_D(CollectionMoveJob); + + if (!d->collection.isValid()) { + setError(Job::Unknown); + setErrorText(i18n("No objects specified for moving")); + emitResult(); + return; + } + + if (!d->destination.isValid() && d->destination.remoteId().isEmpty()) { + setError(Job::Unknown); + setErrorText(i18n("No valid destination specified")); + emitResult(); + return; + } + + const Scope colScope = ProtocolHelper::entitySetToScope(Collection::List() << d->collection); + const Scope destScope = ProtocolHelper::entitySetToScope(Collection::List() << d->destination); + + d->sendCommand(Protocol::MoveCollectionCommandPtr::create(colScope, destScope)); + + ChangeMediator::invalidateCollection(d->collection); +} + +bool CollectionMoveJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::MoveCollection) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/collectionmovejob.h b/src/core/jobs/collectionmovejob.h new file mode 100644 index 0000000..6a2b7d9 --- /dev/null +++ b/src/core/jobs/collectionmovejob.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class CollectionMoveJobPrivate; + +/** + * @short Job that moves a collection in the Akonadi storage to a new parent collection. + * + * This job moves an existing collection to a new parent collection. + * + * @code + * + * const Akonadi::Collection collection = ... + * const Akonadi::Collection newParent = ... + * + * Akonadi::CollectionMoveJob *job = new Akonadi::CollectionMoveJob( collection, newParent ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(moveResult(KJob*)) ); + * + * @endcode + * + * @since 4.4 + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionMoveJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection move job for the given collection and destination + * + * @param collection The collection to move. + * @param destination The destination collection where @p collection should be moved to. + * @param parent The parent object. + */ + CollectionMoveJob(const Collection &collection, const Collection &destination, QObject *parent = nullptr); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(CollectionMoveJob) +}; + +} + diff --git a/src/core/jobs/collectionstatisticsjob.cpp b/src/core/jobs/collectionstatisticsjob.cpp new file mode 100644 index 0000000..1f19b30 --- /dev/null +++ b/src/core/jobs/collectionstatisticsjob.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionstatisticsjob.h" + +#include "collection.h" +#include "collectionstatistics.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +using namespace Akonadi; + +class Akonadi::CollectionStatisticsJobPrivate : public JobPrivate +{ +public: + explicit CollectionStatisticsJobPrivate(CollectionStatisticsJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const override + { + return QStringLiteral("Collection Statistic from collection Id %1").arg(mCollection.id()); + } + + Collection mCollection; + CollectionStatistics mStatistics; +}; + +CollectionStatisticsJob::CollectionStatisticsJob(const Collection &collection, QObject *parent) + : Job(new CollectionStatisticsJobPrivate(this), parent) +{ + Q_D(CollectionStatisticsJob); + + d->mCollection = collection; +} + +CollectionStatisticsJob::~CollectionStatisticsJob() +{ +} + +void CollectionStatisticsJob::doStart() +{ + Q_D(CollectionStatisticsJob); + + try { + d->sendCommand(Protocol::FetchCollectionStatsCommandPtr::create(ProtocolHelper::entityToScope(d->mCollection))); + } catch (const std::exception &e) { + setError(Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool CollectionStatisticsJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(CollectionStatisticsJob); + + if (!response->isResponse() || response->type() != Protocol::Command::FetchCollectionStats) { + return Job::doHandleResponse(tag, response); + } + + d->mStatistics = ProtocolHelper::parseCollectionStatistics(Protocol::cmdCast(response)); + return true; +} + +Collection CollectionStatisticsJob::collection() const +{ + Q_D(const CollectionStatisticsJob); + + return d->mCollection; +} + +CollectionStatistics Akonadi::CollectionStatisticsJob::statistics() const +{ + Q_D(const CollectionStatisticsJob); + + return d->mStatistics; +} diff --git a/src/core/jobs/collectionstatisticsjob.h b/src/core/jobs/collectionstatisticsjob.h new file mode 100644 index 0000000..c750737 --- /dev/null +++ b/src/core/jobs/collectionstatisticsjob.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class CollectionStatistics; +class CollectionStatisticsJobPrivate; + +/** + * @short Job that fetches collection statistics from the Akonadi storage. + * + * This class fetches the CollectionStatistics object for a given collection. + * + * Example: + * + * @code + * + * Akonadi::Collection collection = ... + * + * Akonadi::CollectionStatisticsJob *job = new Akonadi::CollectionStatisticsJob( collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * CollectionStatisticsJob *statisticsJob = qobject_cast( job ); + * + * const Akonadi::CollectionStatistics statistics = statisticsJob->statistics(); + * qDebug() << "Unread items:" << statistics.unreadCount(); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT CollectionStatisticsJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new collection statistics job. + * + * @param collection The collection to fetch the statistics from. + * @param parent The parent object. + */ + explicit CollectionStatisticsJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the collection statistics job. + */ + ~CollectionStatisticsJob() override; + + /** + * Returns the fetched collection statistics. + */ + Q_REQUIRED_RESULT CollectionStatistics statistics() const; + + /** + * Returns the corresponding collection, if the job was executed successfully, + * the collection is already updated. + */ + Q_REQUIRED_RESULT Collection collection() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(CollectionStatisticsJob) +}; + +} + diff --git a/src/core/jobs/invalidatecachejob.cpp b/src/core/jobs/invalidatecachejob.cpp new file mode 100644 index 0000000..6efc93c --- /dev/null +++ b/src/core/jobs/invalidatecachejob.cpp @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionfetchjob.h" +#include "invalidatecachejob_p.h" +#include "itemfetchjob.h" +#include "itemmodifyjob.h" +#include "job_p.h" + +#include + +using namespace Akonadi; + +namespace Akonadi +{ +class InvalidateCacheJobPrivate : JobPrivate +{ +public: + explicit InvalidateCacheJobPrivate(InvalidateCacheJob *qq) + : JobPrivate(qq) + { + } + Collection collection; + + QString jobDebuggingString() const override; + void collectionFetchResult(KJob *job); + void itemFetchResult(KJob *job); + void itemStoreResult(KJob *job); + + Q_DECLARE_PUBLIC(InvalidateCacheJob) +}; + +QString InvalidateCacheJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Invalidate Cache from collection id: %1").arg(collection.id()); +} + +} // namespace Akonadi + +void InvalidateCacheJobPrivate::collectionFetchResult(KJob *job) +{ + Q_Q(InvalidateCacheJob); + if (job->error()) { + return; // handled by KCompositeJob + } + + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + if (fetchJob->collections().size() == 1) { + collection = fetchJob->collections().at(0); + } + + if (!collection.isValid()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid collection.")); + q->emitResult(); + return; + } + + auto itemFetch = new ItemFetchJob(collection, q); + QObject::connect(itemFetch, &ItemFetchJob::result, q, [this](KJob *job) { + itemFetchResult(job); + }); +} + +void InvalidateCacheJobPrivate::itemFetchResult(KJob *job) +{ + Q_Q(InvalidateCacheJob); + if (job->error()) { + return; + } + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + if (fetchJob->items().isEmpty()) { + q->emitResult(); + return; + } + + ItemModifyJob *modJob = nullptr; + const Akonadi::Item::List itemsLst = fetchJob->items(); + for (Item item : itemsLst) { + item.clearPayload(); + modJob = new ItemModifyJob(item, q); + } + QObject::connect(modJob, &KJob::result, q, [this](KJob *job) { + itemStoreResult(job); + }); +} + +void InvalidateCacheJobPrivate::itemStoreResult(KJob *job) +{ + Q_Q(InvalidateCacheJob); + if (job->error()) { + return; + } + q->emitResult(); +} + +InvalidateCacheJob::InvalidateCacheJob(const Collection &collection, QObject *parent) + : Job(new InvalidateCacheJobPrivate(this), parent) +{ + Q_D(InvalidateCacheJob); + d->collection = collection; +} + +void InvalidateCacheJob::doStart() +{ + Q_D(InvalidateCacheJob); + // resolve RID-only collections + auto job = new CollectionFetchJob(d->collection, Akonadi::CollectionFetchJob::Base, this); + connect(job, &KJob::result, this, [d](KJob *job) { + d->collectionFetchResult(job); + }); +} + +#include "moc_invalidatecachejob_p.cpp" diff --git a/src/core/jobs/invalidatecachejob_p.h b/src/core/jobs/invalidatecachejob_p.h new file mode 100644 index 0000000..c9abaf0 --- /dev/null +++ b/src/core/jobs/invalidatecachejob_p.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class InvalidateCacheJobPrivate; + +/** + * Helper job to invalidate item cache for an entire collection. + * @since 4.8 + */ +class AKONADICORE_EXPORT InvalidateCacheJob : public Akonadi::Job +{ + Q_OBJECT +public: + /** + * Create a job to invalidate all cached content in @p collection. + */ + explicit InvalidateCacheJob(const Collection &collection, QObject *parent); + +protected: + void doStart() override; + +private: + Q_DECLARE_PRIVATE(InvalidateCacheJob) +}; + +} + diff --git a/src/core/jobs/itemcopyjob.cpp b/src/core/jobs/itemcopyjob.cpp new file mode 100644 index 0000000..6382037 --- /dev/null +++ b/src/core/jobs/itemcopyjob.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemcopyjob.h" + +#include "collection.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +using namespace Akonadi; + +class Akonadi::ItemCopyJobPrivate : public JobPrivate +{ +public: + explicit ItemCopyJobPrivate(ItemCopyJob *parent) + : JobPrivate(parent) + { + } + QString jobDebuggingString() const override; + + Item::List mItems; + Collection mTarget; +}; + +QString Akonadi::ItemCopyJobPrivate::jobDebuggingString() const +{ + QString str = QStringLiteral("Copy items : "); + const int nbItems = mItems.count(); + for (int i = 0; i < nbItems; ++i) { + if (i != 0) { + str += QLatin1Char(','); + } + str += QString::number(mItems.at(i).id()); + } + return str + QStringLiteral(" to collection %1").arg(mTarget.id()); +} + +ItemCopyJob::ItemCopyJob(const Item &item, const Collection &target, QObject *parent) + : Job(new ItemCopyJobPrivate(this), parent) +{ + Q_D(ItemCopyJob); + + d->mItems << item; + d->mTarget = target; +} + +ItemCopyJob::ItemCopyJob(const Item::List &items, const Collection &target, QObject *parent) + : Job(new ItemCopyJobPrivate(this), parent) +{ + Q_D(ItemCopyJob); + + d->mItems = items; + d->mTarget = target; +} + +ItemCopyJob::~ItemCopyJob() +{ +} + +void ItemCopyJob::doStart() +{ + Q_D(ItemCopyJob); + + try { + d->sendCommand(Protocol::CopyItemsCommandPtr::create(ProtocolHelper::entitySetToScope(d->mItems), ProtocolHelper::entityToScope(d->mTarget))); + } catch (std::exception &e) { + setError(Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + } +} + +bool ItemCopyJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::CopyItems) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/itemcopyjob.h b/src/core/jobs/itemcopyjob.h new file mode 100644 index 0000000..98aae0e --- /dev/null +++ b/src/core/jobs/itemcopyjob.h @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class ItemCopyJobPrivate; + +/** + * @short Job that copies a set of items to a target collection in the Akonadi storage. + * + * The job can be used to copy one or several Item objects to another collection. + * + * Example: + * + * @code + * + * Akonadi::Item::List items = ... + * Akonadi::Collection collection = ... + * + * Akonadi::ItemCopyJob *job = new Akonadi::ItemCopyJob( items, collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Items copied successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemCopyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new item copy job. + * + * @param item The item to copy. + * @param target The target collection. + * @param parent The parent object. + */ + ItemCopyJob(const Item &item, const Collection &target, QObject *parent = nullptr); + + /** + * Creates a new item copy job. + * + * @param items A list of items to copy. + * @param target The target collection. + * @param parent The parent object. + */ + ItemCopyJob(const Item::List &items, const Collection &target, QObject *parent = nullptr); + + /** + * Destroys the item copy job. + */ + ~ItemCopyJob() override; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(ItemCopyJob) +}; + +} + diff --git a/src/core/jobs/itemcreatejob.cpp b/src/core/jobs/itemcreatejob.cpp new file mode 100644 index 0000000..8b821ac --- /dev/null +++ b/src/core/jobs/itemcreatejob.cpp @@ -0,0 +1,236 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + SPDX-FileCopyrightText: 2007 Robert Zwerus + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemcreatejob.h" + +#include "collection.h" +#include "gidextractor_p.h" +#include "item.h" +#include "item_p.h" +#include "itemserializer_p.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include +#include + +#include + +using namespace Akonadi; + +class Akonadi::ItemCreateJobPrivate : public JobPrivate +{ +public: + explicit ItemCreateJobPrivate(ItemCreateJob *parent) + : JobPrivate(parent) + { + } + + Protocol::PartMetaData preparePart(const QByteArray &part); + + QString jobDebuggingString() const override; + Collection mCollection; + Item mItem; + QSet mParts; + QSet mForeignParts; + QDateTime mDatetime; + QByteArray mPendingData; + ItemCreateJob::MergeOptions mMergeOptions = ItemCreateJob::NoMerge; + bool mItemReceived = false; +}; + +QString Akonadi::ItemCreateJobPrivate::jobDebuggingString() const +{ + const QString collectionName = mCollection.name(); + QString str = QStringLiteral("%1 Item %2 from col %3") + .arg(mMergeOptions == ItemCreateJob::NoMerge ? QStringLiteral("Create") : QStringLiteral("Merge")) + .arg(mItem.id()) + .arg(mCollection.id()); + if (!collectionName.isEmpty()) { + str += QStringLiteral(" (%1)").arg(collectionName); + } + return str; +} + +Protocol::PartMetaData ItemCreateJobPrivate::preparePart(const QByteArray &partName) +{ + ProtocolHelper::PartNamespace ns; // dummy + const QByteArray partLabel = ProtocolHelper::decodePartIdentifier(partName, ns); + if (!mParts.remove(partLabel)) { + // ERROR? + return Protocol::PartMetaData(); + } + + int version = 0; + if (mForeignParts.contains(partLabel)) { + mPendingData = mItem.d_ptr->mPayloadPath.toUtf8(); + const auto size = QFile(mItem.d_ptr->mPayloadPath).size(); + return Protocol::PartMetaData(partName, size, version, Protocol::PartMetaData::Foreign); + } else { + mPendingData.clear(); + ItemSerializer::serialize(mItem, partLabel, mPendingData, version); + return Protocol::PartMetaData(partName, mPendingData.size(), version); + } +} + +ItemCreateJob::ItemCreateJob(const Item &item, const Collection &collection, QObject *parent) + : Job(new ItemCreateJobPrivate(this), parent) +{ + Q_D(ItemCreateJob); + + Q_ASSERT(!item.mimeType().isEmpty()); + d->mItem = item; + d->mParts = d->mItem.loadedPayloadParts(); + d->mCollection = collection; + + if (!d->mItem.payloadPath().isEmpty()) { + d->mForeignParts = ItemSerializer::allowedForeignParts(d->mItem); + } +} + +ItemCreateJob::~ItemCreateJob() +{ +} + +void ItemCreateJob::doStart() +{ + Q_D(ItemCreateJob); + + if (!d->mCollection.isValid()) { + setError(Unknown); + setErrorText(i18n("Invalid parent collection")); + emitResult(); + return; + } + + auto cmd = Protocol::CreateItemCommandPtr::create(); + cmd->setMimeType(d->mItem.mimeType()); + cmd->setGid(d->mItem.gid()); + cmd->setRemoteId(d->mItem.remoteId()); + cmd->setRemoteRevision(d->mItem.remoteRevision()); + + Protocol::CreateItemCommand::MergeModes mergeModes = Protocol::CreateItemCommand::None; + if ((d->mMergeOptions & GID) && !d->mItem.gid().isEmpty()) { + mergeModes |= Protocol::CreateItemCommand::GID; + } + if ((d->mMergeOptions & RID) && !d->mItem.remoteId().isEmpty()) { + mergeModes |= Protocol::CreateItemCommand::RemoteID; + } + if ((d->mMergeOptions & Silent)) { + mergeModes |= Protocol::CreateItemCommand::Silent; + } + const bool merge = (mergeModes & Protocol::CreateItemCommand::GID) || (mergeModes & Protocol::CreateItemCommand::RemoteID); + cmd->setMergeModes(mergeModes); + + if (d->mItem.d_ptr->mFlagsOverwritten || !merge) { + cmd->setFlags(d->mItem.flags()); + cmd->setFlagsOverwritten(d->mItem.d_ptr->mFlagsOverwritten); + } else { + const auto addedFlags = ItemChangeLog::instance()->addedFlags(d->mItem.d_ptr); + const auto deletedFlags = ItemChangeLog::instance()->deletedFlags(d->mItem.d_ptr); + cmd->setAddedFlags(addedFlags); + cmd->setRemovedFlags(deletedFlags); + } + + if (d->mItem.d_ptr->mTagsOverwritten || !merge) { + const auto tags = d->mItem.tags(); + if (!tags.isEmpty()) { + cmd->setTags(ProtocolHelper::entitySetToScope(tags)); + } + } else { + const auto addedTags = ItemChangeLog::instance()->addedTags(d->mItem.d_ptr); + if (!addedTags.isEmpty()) { + cmd->setAddedTags(ProtocolHelper::entitySetToScope(addedTags)); + } + const auto deletedTags = ItemChangeLog::instance()->deletedTags(d->mItem.d_ptr); + if (!deletedTags.isEmpty()) { + cmd->setRemovedTags(ProtocolHelper::entitySetToScope(deletedTags)); + } + } + + cmd->setCollection(ProtocolHelper::entityToScope(d->mCollection)); + cmd->setItemSize(d->mItem.size()); + + cmd->setAttributes(ProtocolHelper::attributesToProtocol(d->mItem)); + QSet parts; + parts.reserve(d->mParts.size()); + for (const QByteArray &part : std::as_const(d->mParts)) { + parts.insert(ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, part)); + } + cmd->setParts(parts); + + d->sendCommand(cmd); +} + +bool ItemCreateJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(ItemCreateJob); + + if (!response->isResponse() && response->type() == Protocol::Command::StreamPayload) { + const auto &streamCmd = Protocol::cmdCast(response); + auto streamResp = Protocol::StreamPayloadResponsePtr::create(); + streamResp->setPayloadName(streamCmd.payloadName()); + if (streamCmd.request() == Protocol::StreamPayloadCommand::MetaData) { + streamResp->setMetaData(d->preparePart(streamCmd.payloadName())); + } else { + if (streamCmd.destination().isEmpty()) { + streamResp->setData(d->mPendingData); + } else { + QByteArray error; + if (!ProtocolHelper::streamPayloadToFile(streamCmd.destination(), d->mPendingData, error)) { + // Error? + } + } + } + d->sendCommand(tag, streamResp); + return false; + } + + if (response->isResponse() && response->type() == Protocol::Command::FetchItems) { + const auto &fetchResp = Protocol::cmdCast(response); + Item item = ProtocolHelper::parseItemFetchResult(fetchResp); + if (!item.isValid()) { + // Error, maybe? + return false; + } + d->mItem = item; + return false; + } + + if (response->isResponse() && response->type() == Protocol::Command::CreateItem) { + return true; + } + + return Job::doHandleResponse(tag, response); +} + +void ItemCreateJob::setMerge(ItemCreateJob::MergeOptions options) +{ + Q_D(ItemCreateJob); + + d->mMergeOptions = options; +} + +Item ItemCreateJob::item() const +{ + Q_D(const ItemCreateJob); + + // Parent collection is available only with non-silent merge/create + if (d->mItem.parentCollection().isValid()) { + return d->mItem; + } + + Item item(d->mItem); + item.setRevision(0); + item.setModificationTime(d->mDatetime); + item.setParentCollection(d->mCollection); + item.setStorageCollectionId(d->mCollection.id()); + + return item; +} diff --git a/src/core/jobs/itemcreatejob.h b/src/core/jobs/itemcreatejob.h new file mode 100644 index 0000000..d97f1cb --- /dev/null +++ b/src/core/jobs/itemcreatejob.h @@ -0,0 +1,124 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class Item; +class ItemCreateJobPrivate; + +/** + * @short Job that creates a new item in the Akonadi storage. + * + * This job creates a new item with all the set properties in the + * given target collection. + * + * Note that items can not be created in the root collection (Collection::root()) + * and the collection must have Collection::contentMimeTypes() that match the mimetype + * of the item being created. + * + * Example: + * + * @code + * + * // Create a contact item in the root collection + * + * KContacts::Addressee addr; + * addr.setNameFromString( "Joe Jr. Miller" ); + * + * Akonadi::Item item; + * item.setMimeType( "text/directory" ); + * item.setPayload( addr ); + * + * Akonadi::Collection collection = getCollection(); + * + * Akonadi::ItemCreateJob *job = new Akonadi::ItemCreateJob( item, collection ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Contact item created successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new item create job. + * + * @param item The item to create. + * @note It must have a mime type set. + * @param collection The parent collection where the new item shall be located in. + * @param parent The parent object. + */ + ItemCreateJob(const Item &item, const Collection &collection, QObject *parent = nullptr); + + /** + * Destroys the item create job. + */ + ~ItemCreateJob() override; + + /** + * Returns the created item with the new unique id, or an invalid item if the job failed. + */ + Q_REQUIRED_RESULT Item item() const; + + enum MergeOption { + NoMerge = 0, ///< Don't merge + RID = 1, ///< Merge by remote id + GID = 2, ///< Merge by GID + Silent = 4 ///< Only return the id of the merged/created item. + }; + Q_DECLARE_FLAGS(MergeOptions, MergeOption) + + /** + * Merge this item into an existing one if available. + * + * If an item with same GID and/or remote ID as the created item exists in + * specified collection (depending on the provided options), the new item will + * be merged into the existing one and the merged item will be returned + * (unless the Silent option is used). + * + * If no matching item is found a new item is created. + * + * If the item does not have a GID or RID, this option will be + * ignored and a new item will be created. + * + * By default, merging is disabled. + * + * @param options Merge options. + * @since 4.14 + */ + void setMerge(MergeOptions options); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(ItemCreateJob) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(ItemCreateJob::MergeOptions) + +} + diff --git a/src/core/jobs/itemdeletejob.cpp b/src/core/jobs/itemdeletejob.cpp new file mode 100644 index 0000000..2dfa3c9 --- /dev/null +++ b/src/core/jobs/itemdeletejob.cpp @@ -0,0 +1,115 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemdeletejob.h" + +#include "collection.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +using namespace Akonadi; + +class Akonadi::ItemDeleteJobPrivate : public JobPrivate +{ +public: + explicit ItemDeleteJobPrivate(ItemDeleteJob *parent) + : JobPrivate(parent) + { + } + + Q_DECLARE_PUBLIC(ItemDeleteJob) + QString jobDebuggingString() const override; + + Item::List mItems; + Collection mCollection; + Tag mCurrentTag; +}; + +QString Akonadi::ItemDeleteJobPrivate::jobDebuggingString() const +{ + QString itemStr = QStringLiteral("items id: "); + bool firstItem = true; + for (const Akonadi::Item &item : std::as_const(mItems)) { + if (firstItem) { + firstItem = false; + } else { + itemStr += QStringLiteral(", "); + } + itemStr += QString::number(item.id()); + } + + return QStringLiteral("Remove %1 from collection id %2").arg(itemStr).arg(mCollection.id()); +} + +ItemDeleteJob::ItemDeleteJob(const Item &item, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mItems << item; +} + +ItemDeleteJob::ItemDeleteJob(const Item::List &items, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mItems = items; +} + +ItemDeleteJob::ItemDeleteJob(const Collection &collection, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mCollection = collection; +} + +ItemDeleteJob::ItemDeleteJob(const Tag &tag, QObject *parent) + : Job(new ItemDeleteJobPrivate(this), parent) +{ + Q_D(ItemDeleteJob); + + d->mCurrentTag = tag; +} + +ItemDeleteJob::~ItemDeleteJob() +{ +} + +Item::List ItemDeleteJob::deletedItems() const +{ + Q_D(const ItemDeleteJob); + + return d->mItems; +} + +void ItemDeleteJob::doStart() +{ + Q_D(ItemDeleteJob); + + try { + d->sendCommand(Protocol::DeleteItemsCommandPtr::create(d->mItems.isEmpty() ? Scope() : ProtocolHelper::entitySetToScope(d->mItems), + ProtocolHelper::commandContextToProtocol(d->mCollection, d->mCurrentTag, d->mItems))); + } catch (const Akonadi::Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool ItemDeleteJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::DeleteItems) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +#include "moc_itemdeletejob.cpp" diff --git a/src/core/jobs/itemdeletejob.h b/src/core/jobs/itemdeletejob.h new file mode 100644 index 0000000..7418c22 --- /dev/null +++ b/src/core/jobs/itemdeletejob.h @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class ItemDeleteJobPrivate; + +/** + * @short Job that deletes items from the Akonadi storage. + * + * This job removes the given items from the Akonadi storage. + * + * Example: + * + * @code + * + * const Akonadi::Item item = ... + * + * ItemDeleteJob *job = new ItemDeleteJob(item); + * connect(job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*))); + * + * @endcode + * + * Example: + * + * @code + * + * const Akonadi::Item::List items = ... + * + * ItemDeleteJob *job = new ItemDeleteJob(items); + * connect(job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*))); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemDeleteJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new item delete job that deletes @p item. The item + * needs to have a unique identifier set. + * + * @internal + * For internal use only, the item may have a remote identifier set instead + * of a unique identifier. In this case, a collection or resource context + * needs to be selected using ResourceSelectJob. + * @endinternal + * + * @param item The item to delete. + * @param parent The parent object. + */ + explicit ItemDeleteJob(const Item &item, QObject *parent = nullptr); + + /** + * Creates a new item delete job that deletes all items in the list + * @p items. Each item needs to have a unique identifier set. These items + * can be located in any collection. + * + * @internal + * For internal use only, the items may have remote identifiers set instead + * of unique identifiers. In this case, a collection or resource context + * needs to be selected using ResourceSelectJob. + * @endinternal + * + * @param items The items to delete. + * @param parent The parent object. + * + * @since 4.3 + */ + explicit ItemDeleteJob(const Item::List &items, QObject *parent = nullptr); + + /** + * Creates a new item delete job that deletes all items in the collection + * @p collection. The collection needs to have a unique identifier set. + * + * @internal + * For internal use only, the collection may have a remote identifier set + * instead of a unique identifier. In this case, a resource context needs + * to be selected using ResourceSelectJob. + * @endinternal + * + * @param collection The collection which content should be deleted. + * @param parent The parent object. + * + * @since 4.3 + */ + explicit ItemDeleteJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Creates a new item delete job that deletes all items that have assigned + * the tag @p tag. + * + * @param tag The tag which content should be deleted. + * @param parent The parent object. + * + * @since 4.14 + */ + explicit ItemDeleteJob(const Tag &tag, QObject *parent = nullptr); + + /** + * Destroys the item delete job. + */ + ~ItemDeleteJob() override; + + /** + * Returns the items passed on in the constructor. + * @since 4.4 + */ + Q_REQUIRED_RESULT Item::List deletedItems() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(ItemDeleteJob) + /// @endcond +}; + +} + diff --git a/src/core/jobs/itemfetchjob.cpp b/src/core/jobs/itemfetchjob.cpp new file mode 100644 index 0000000..82bc84c --- /dev/null +++ b/src/core/jobs/itemfetchjob.cpp @@ -0,0 +1,282 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemfetchjob.h" + +#include "attributefactory.h" +#include "collection.h" +#include "itemfetchscope.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" +#include "session_p.h" +#include "tagfetchscope.h" + +#include + +using namespace Akonadi; + +class Akonadi::ItemFetchJobPrivate : public JobPrivate +{ +public: + explicit ItemFetchJobPrivate(ItemFetchJob *parent) + : JobPrivate(parent) + { + mCollection = Collection::root(); + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + } + + ~ItemFetchJobPrivate() override + { + delete mValuePool; + } + + void init() + { + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { + timeout(); + }); + } + void aboutToFinish() override + { + timeout(); + } + + void timeout() + { + Q_Q(ItemFetchJob); + + mEmitTimer.stop(); // in case we are called by result() + if (!mPendingItems.isEmpty()) { + if (!q->error()) { + Q_EMIT q->itemsReceived(mPendingItems); + } + mPendingItems.clear(); + } + } + + QString jobDebuggingString() const override + { + if (mRequestedItems.isEmpty()) { + QString str = QStringLiteral("All items from collection %1").arg(mCollection.id()); + if (mFetchScope.fetchChangedSince().isValid()) { + str += QStringLiteral(" changed since %1").arg(mFetchScope.fetchChangedSince().toString()); + } + return str; + + } else { + try { + QString itemStr = QStringLiteral("items id: "); + bool firstItem = true; + for (const Akonadi::Item &item : std::as_const(mRequestedItems)) { + if (firstItem) { + firstItem = false; + } else { + itemStr += QStringLiteral(", "); + } + itemStr += QString::number(item.id()); + const Akonadi::Collection parentCollection = item.parentCollection(); + if (parentCollection.isValid()) { + itemStr += QStringLiteral(" from collection %1").arg(parentCollection.id()); + } + } + return itemStr; + // return QString(); //QString::fromLatin1(ProtocolHelper::entitySetToScope(mRequestedItems)); + } catch (const Exception &e) { + return QString::fromUtf8(e.what()); + } + } + } + + Q_DECLARE_PUBLIC(ItemFetchJob) + + Collection mCollection; + Tag mCurrentTag; + Item::List mRequestedItems; + Item::List mResultItems; + ItemFetchScope mFetchScope; + Item::List mPendingItems; // items pending for emitting itemsReceived() + QTimer mEmitTimer; + ProtocolHelperValuePool *mValuePool = nullptr; + ItemFetchJob::DeliveryOptions mDeliveryOptions = ItemFetchJob::Default; + int mCount = 0; +}; + +ItemFetchJob::ItemFetchJob(const Collection &collection, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + d->init(); + + d->mCollection = collection; + d->mValuePool = new ProtocolHelperValuePool; // only worth it for lots of results +} + +ItemFetchJob::ItemFetchJob(const Item &item, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + d->init(); + + d->mRequestedItems.append(item); +} + +ItemFetchJob::ItemFetchJob(const Item::List &items, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + d->init(); + + d->mRequestedItems = items; +} + +ItemFetchJob::ItemFetchJob(const QList &items, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + d->init(); + + d->mRequestedItems.reserve(items.size()); + for (auto id : items) { + d->mRequestedItems.append(Item(id)); + } +} + +ItemFetchJob::ItemFetchJob(const QVector &items, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + d->init(); + + d->mRequestedItems.reserve(items.size()); + for (auto id : items) { + d->mRequestedItems.append(Item(id)); + } +} + +ItemFetchJob::ItemFetchJob(const Tag &tag, QObject *parent) + : Job(new ItemFetchJobPrivate(this), parent) +{ + Q_D(ItemFetchJob); + d->init(); + + d->mCurrentTag = tag; + d->mValuePool = new ProtocolHelperValuePool; +} + +ItemFetchJob::~ItemFetchJob() = default; + +void ItemFetchJob::doStart() +{ + Q_D(ItemFetchJob); + + try { + d->sendCommand(Protocol::FetchItemsCommandPtr::create(d->mRequestedItems.isEmpty() ? Scope() : ProtocolHelper::entitySetToScope(d->mRequestedItems), + ProtocolHelper::commandContextToProtocol(d->mCollection, d->mCurrentTag, d->mRequestedItems), + ProtocolHelper::itemFetchScopeToProtocol(d->mFetchScope), + ProtocolHelper::tagFetchScopeToProtocol(d->mFetchScope.tagFetchScope()))); + } catch (const Akonadi::Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool ItemFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(ItemFetchJob); + + if (!response->isResponse() || response->type() != Protocol::Command::FetchItems) { + return Job::doHandleResponse(tag, response); + } + + const auto &resp = Protocol::cmdCast(response); + // Invalid ID marks the last part of the response + if (resp.id() < 0) { + return true; + } + + const Item item = ProtocolHelper::parseItemFetchResult(resp, nullptr, d->mValuePool); + if (!item.isValid()) { + return false; + } + + d->mCount++; + + if (d->mDeliveryOptions & ItemGetter) { + d->mResultItems.append(item); + } + + if (d->mDeliveryOptions & EmitItemsInBatches) { + d->mPendingItems.append(item); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); + } + } else if (d->mDeliveryOptions & EmitItemsIndividually) { + Q_EMIT itemsReceived(Item::List() << item); + } + + return false; +} + +Item::List ItemFetchJob::items() const +{ + Q_D(const ItemFetchJob); + + return d->mResultItems; +} + +void ItemFetchJob::clearItems() +{ + Q_D(ItemFetchJob); + + d->mResultItems.clear(); +} + +void ItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(ItemFetchJob); + + d->mFetchScope = fetchScope; +} + +ItemFetchScope &ItemFetchJob::fetchScope() +{ + Q_D(ItemFetchJob); + + return d->mFetchScope; +} + +void ItemFetchJob::setCollection(const Akonadi::Collection &collection) +{ + Q_D(ItemFetchJob); + + d->mCollection = collection; +} + +void ItemFetchJob::setDeliveryOption(DeliveryOptions options) +{ + Q_D(ItemFetchJob); + + d->mDeliveryOptions = options; +} + +ItemFetchJob::DeliveryOptions ItemFetchJob::deliveryOptions() const +{ + Q_D(const ItemFetchJob); + + return d->mDeliveryOptions; +} + +int ItemFetchJob::count() const +{ + Q_D(const ItemFetchJob); + + return d->mCount; +} +#include "moc_itemfetchjob.cpp" diff --git a/src/core/jobs/itemfetchjob.h b/src/core/jobs/itemfetchjob.h new file mode 100644 index 0000000..7e21b12 --- /dev/null +++ b/src/core/jobs/itemfetchjob.h @@ -0,0 +1,251 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class ItemFetchJobPrivate; +class ItemFetchScope; + +/** + * @short Job that fetches items from the Akonadi storage. + * + * This class is used to fetch items from the Akonadi storage. + * Which parts of the items (e.g. headers only, attachments or all) + * can be specified by the ItemFetchScope. + * + * Note that ItemFetchJob does not refresh the Akonadi storage from the + * backend; this is unnecessary due to the fact that backend updates + * automatically trigger an update to the Akonadi database whenever they occur + * (unless the resource is offline). + * + * Note that items can not be created in the root collection (Collection::root()) + * and therefore can not be fetched from there either. That is - an item fetch in + * the root collection will yield an empty list. + * + * + * Example: + * + * @code + * + * // Fetch all items with full payload from a collection + * + * const Collection collection = getCollection(); + * + * Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(collection); + * connect(job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*))); + * job->fetchScope().fetchFullPayload(); + * + * ... + * + * MyClass::jobFinished(KJob *job) + * { + * if (job->error()) { + * qDebug() << "Error occurred"; + * return; + * } + * + * Akonadi::ItemFetchJob *fetchJob = qobject_cast(job); + * + * const Akonadi::Item::List items = fetchJob->items(); + * for (const Akonadi::Item &item : items) { + * qDebug() << "Item ID:" << item.id(); + * } + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemFetchJob : public Job +{ + Q_OBJECT + Q_FLAGS(DeliveryOptions) +public: + /** + * Creates a new item fetch job that retrieves all items inside the given collection. + * + * @param collection The parent collection to fetch all items from. + * @param parent The parent object. + */ + explicit ItemFetchJob(const Collection &collection, QObject *parent = nullptr); + + /** + * Creates a new item fetch job that retrieves the specified item. + * If the item has a uid set, this is used to identify the item on the Akonadi + * server. If only a remote identifier is available, that is used. + * However, as remote identifiers are not necessarily globally unique, you + * need to specify the collection to search in in that case, using + * setCollection(). + * + * @internal + * For internal use only when using remote identifiers, the resource search + * context can be set globally by ResourceSelectJob. + * @endinternal + * + * @param item The item to fetch. + * @param parent The parent object. + */ + explicit ItemFetchJob(const Item &item, QObject *parent = nullptr); + + /** + * Creates a new item fetch job that retrieves the specified items. + * If the items have a uid set, this is used to identify the item on the Akonadi + * server. If only a remote identifier is available, that is used. + * However, as remote identifiers are not necessarily globally unique, you + * need to specify the collection to search in in that case, using + * setCollection(). + * + * @internal + * For internal use only when using remote identifiers, the resource search + * context can be set globally by ResourceSelectJob. + * @endinternal + * + * @param items The items to fetch. + * @param parent The parent object. + * @since 4.4 + */ + explicit ItemFetchJob(const Item::List &items, QObject *parent = nullptr); + + /** + * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = nullptr) + * @since 4.8 + */ + explicit ItemFetchJob(const QList &items, QObject *parent = nullptr); + + /** + * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = nullptr) + * @since 5.4 + */ + explicit ItemFetchJob(const QVector &items, QObject *parent = nullptr); + + /** + * Creates a new item fetch job that retrieves all items tagged with specified @p tag. + * + * @param tag The tag to fetch all items from. + * @param parent The parent object. + * + * @since 4.14 + */ + explicit ItemFetchJob(const Tag &tag, QObject *parent = nullptr); + + /** + * Destroys the item fetch job. + */ + ~ItemFetchJob() override; + + /** + * Returns the fetched items. + * + * This returns an empty list when not using the ItemGetter DeliveryOption. + * + * @note The items are invalid before the result(KJob*) + * signal has been emitted or if an error occurred. + */ + Q_REQUIRED_RESULT Item::List items() const; + + /** + * Save memory by clearing the fetched items. + * @since 4.12 + */ + void clearItems(); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + * @since 4.4 + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + + /** + * Specifies the collection the item is in. + * This is only required when retrieving an item based on its remote id + * which might not be unique globally. + * + * @internal + * @see ResourceSelectJob (for internal use only) + * @endinternal + */ + void setCollection(const Collection &collection); + + enum DeliveryOption { + ItemGetter = 0x1, ///< items available through items() + EmitItemsIndividually = 0x2, ///< emitted via signal upon reception + EmitItemsInBatches = 0x4, ///< emitted via signal in bulk (collected and emitted delayed via timer) + Default = ItemGetter | EmitItemsInBatches + }; + Q_DECLARE_FLAGS(DeliveryOptions, DeliveryOption) + + /** + * Sets the mechanisms by which the items should be fetched + * @since 4.13 + */ + void setDeliveryOption(DeliveryOptions options); + + /** + * Returns the delivery options + * @since 4.13 + */ + DeliveryOptions deliveryOptions() const; + + /** + * Returns the total number of retrieved items. + * This works also without the ItemGetter DeliveryOption. + * @since 4.14 + */ + int count() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new items have been fetched completely. + * + * @note This is an optimization; instead of waiting for the end of the job + * and calling items(), you can connect to this signal and get the items + * incrementally. + * + * @param items The fetched items. + */ + void itemsReceived(const Akonadi::Item::List &items); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(ItemFetchJob) +}; + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::ItemFetchJob::DeliveryOptions) + diff --git a/src/core/jobs/itemmodifyjob.cpp b/src/core/jobs/itemmodifyjob.cpp new file mode 100644 index 0000000..ea39cdc --- /dev/null +++ b/src/core/jobs/itemmodifyjob.cpp @@ -0,0 +1,414 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemmodifyjob.h" +#include "akonadicore_debug.h" +#include "itemmodifyjob_p.h" + +#include "changemediator_p.h" +#include "collection.h" +#include "conflicthandler_p.h" +#include "item_p.h" +#include "itemserializer_p.h" +#include "job_p.h" + +#include "gidextractor_p.h" +#include "protocolhelper_p.h" + +#include + +#include + +using namespace Akonadi; + +ItemModifyJobPrivate::ItemModifyJobPrivate(ItemModifyJob *parent) + : JobPrivate(parent) +{ +} + +void ItemModifyJobPrivate::setClean() +{ + mOperations.insert(Dirty); +} + +Protocol::PartMetaData ItemModifyJobPrivate::preparePart(const QByteArray &partName) +{ + ProtocolHelper::PartNamespace ns; // dummy + const QByteArray partLabel = ProtocolHelper::decodePartIdentifier(partName, ns); + if (!mParts.contains(partLabel)) { + // Error? + return Protocol::PartMetaData(); + } + + mPendingData.clear(); + int version = 0; + const auto item = mItems.first(); + if (mForeignParts.contains(partLabel)) { + mPendingData = item.d_ptr->mPayloadPath.toUtf8(); + const auto size = QFile(item.d_ptr->mPayloadPath).size(); + return Protocol::PartMetaData(partName, size, version, Protocol::PartMetaData::Foreign); + } else { + ItemSerializer::serialize(mItems.first(), partLabel, mPendingData, version); + return Protocol::PartMetaData(partName, mPendingData.size(), version); + } +} + +void ItemModifyJobPrivate::conflictResolved() +{ + Q_Q(ItemModifyJob); + + q->setError(KJob::NoError); + q->setErrorText(QString()); + q->emitResult(); +} + +void ItemModifyJobPrivate::conflictResolveError(const QString &message) +{ + Q_Q(ItemModifyJob); + + q->setErrorText(q->errorText() + message); + q->emitResult(); +} + +void ItemModifyJobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + auto it = std::find_if(mItems.begin(), mItems.end(), [&itemId](const Item &item) -> bool { + return item.id() == itemId; + }); + if (it != mItems.end() && (*it).revision() == oldRevision) { + (*it).setRevision(newRevision); + } +} + +QString ItemModifyJobPrivate::jobDebuggingString() const +{ + try { + return Protocol::debugString(fullCommand()); + } catch (const Exception &e) { + return QString::fromUtf8(e.what()); + } +} + +void ItemModifyJobPrivate::setSilent(bool silent) +{ + mSilent = silent; +} + +ItemModifyJob::ItemModifyJob(const Item &item, QObject *parent) + : Job(new ItemModifyJobPrivate(this), parent) +{ + Q_D(ItemModifyJob); + + d->mItems.append(item); + d->mParts = item.loadedPayloadParts(); + + d->mOperations.insert(ItemModifyJobPrivate::RemoteId); + d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision); + + if (!item.payloadPath().isEmpty()) { + d->mForeignParts = ItemSerializer::allowedForeignParts(item); + } +} + +ItemModifyJob::ItemModifyJob(const Akonadi::Item::List &items, QObject *parent) + : Job(new ItemModifyJobPrivate(this), parent) +{ + Q_ASSERT(!items.isEmpty()); + Q_D(ItemModifyJob); + d->mItems = items; + + // same as single item ctor + if (d->mItems.size() == 1) { + d->mParts = items.first().loadedPayloadParts(); + d->mOperations.insert(ItemModifyJobPrivate::RemoteId); + d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision); + } else { + d->mIgnorePayload = true; + d->mRevCheck = false; + } +} + +ItemModifyJob::~ItemModifyJob() +{ +} + +Protocol::ModifyItemsCommandPtr ItemModifyJobPrivate::fullCommand() const +{ + auto cmd = Protocol::ModifyItemsCommandPtr::create(); + + const Akonadi::Item item = mItems.first(); + for (int op : std::as_const(mOperations)) { + switch (op) { + case ItemModifyJobPrivate::RemoteId: + if (!item.remoteId().isNull()) { + cmd->setRemoteId(item.remoteId()); + } + break; + case ItemModifyJobPrivate::Gid: { + const QString gid = GidExtractor::getGid(item); + if (!gid.isNull()) { + cmd->setGid(gid); + } + break; + } + case ItemModifyJobPrivate::RemoteRevision: + if (!item.remoteRevision().isNull()) { + cmd->setRemoteRevision(item.remoteRevision()); + } + break; + case ItemModifyJobPrivate::Dirty: + cmd->setDirty(false); + break; + } + } + + if (item.d_ptr->mClearPayload) { + cmd->setInvalidateCache(true); + } + if (mSilent) { + cmd->setNotify(true); + } + + if (item.d_ptr->mFlagsOverwritten) { + cmd->setFlags(item.flags()); + } else { + const auto addedFlags = ItemChangeLog::instance()->addedFlags(item.d_ptr); + if (!addedFlags.isEmpty()) { + cmd->setAddedFlags(addedFlags); + } + const auto deletedFlags = ItemChangeLog::instance()->deletedFlags(item.d_ptr); + if (!deletedFlags.isEmpty()) { + cmd->setRemovedFlags(deletedFlags); + } + } + + if (item.d_ptr->mTagsOverwritten) { + const auto tags = item.tags(); + if (!tags.isEmpty()) { + cmd->setTags(ProtocolHelper::entitySetToScope(tags)); + } + } else { + const auto addedTags = ItemChangeLog::instance()->addedTags(item.d_ptr); + if (!addedTags.isEmpty()) { + cmd->setAddedTags(ProtocolHelper::entitySetToScope(addedTags)); + } + const auto deletedTags = ItemChangeLog::instance()->deletedTags(item.d_ptr); + if (!deletedTags.isEmpty()) { + cmd->setRemovedTags(ProtocolHelper::entitySetToScope(deletedTags)); + } + } + + if (!mParts.isEmpty()) { + QSet parts; + parts.reserve(mParts.size()); + for (const QByteArray &part : std::as_const(mParts)) { + parts.insert(ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, part)); + } + cmd->setParts(parts); + } + + const AttributeStorage &attributeStorage = ItemChangeLog::instance()->attributeStorage(item.d_ptr); + const QSet deletedAttributes = attributeStorage.deletedAttributes(); + if (!deletedAttributes.isEmpty()) { + QSet removedParts; + removedParts.reserve(deletedAttributes.size()); + for (const QByteArray &part : deletedAttributes) { + removedParts.insert("ATR:" + part); + } + cmd->setRemovedParts(removedParts); + } + if (attributeStorage.hasModifiedAttributes()) { + cmd->setAttributes(ProtocolHelper::attributesToProtocol(attributeStorage.modifiedAttributes())); + } + + // nothing to do + if (cmd->modifiedParts() == Protocol::ModifyItemsCommand::None && mParts.isEmpty() && !cmd->invalidateCache()) { + return cmd; + } + + cmd->setItems(ProtocolHelper::entitySetToScope(mItems)); + if (mRevCheck && item.revision() >= 0) { + cmd->setOldRevision(item.revision()); + } + + if (item.d_ptr->mSizeChanged) { + cmd->setItemSize(item.size()); + } + + return cmd; +} + +void ItemModifyJob::doStart() +{ + Q_D(ItemModifyJob); + + Protocol::ModifyItemsCommandPtr command; + try { + command = d->fullCommand(); + } catch (const Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } + + if (command->modifiedParts() == Protocol::ModifyItemsCommand::None) { + emitResult(); + return; + } + + d->sendCommand(command); +} + +bool ItemModifyJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(ItemModifyJob); + + if (!response->isResponse() && response->type() == Protocol::Command::StreamPayload) { + const auto &streamCmd = Protocol::cmdCast(response); + auto streamResp = Protocol::StreamPayloadResponsePtr::create(); + if (streamCmd.request() == Protocol::StreamPayloadCommand::MetaData) { + streamResp->setMetaData(d->preparePart(streamCmd.payloadName())); + } else { + if (streamCmd.destination().isEmpty()) { + streamResp->setData(d->mPendingData); + } else { + QByteArray error; + if (!ProtocolHelper::streamPayloadToFile(streamCmd.destination(), d->mPendingData, error)) { + // TODO: Error? + } + } + } + d->sendCommand(tag, streamResp); + return false; + } + + if (response->isResponse() && response->type() == Protocol::Command::ModifyItems) { + const auto &resp = Protocol::cmdCast(response); + if (resp.errorCode()) { + setError(Unknown); + setErrorText(resp.errorMessage()); + return true; + } + + if (resp.errorMessage().contains(QLatin1String("[LLCONFLICT]"))) { + if (d->mAutomaticConflictHandlingEnabled) { + auto handler = new ConflictHandler(ConflictHandler::LocalLocalConflict, this); + handler->setConflictingItems(d->mItems.first(), d->mItems.first()); + connect(handler, &ConflictHandler::conflictResolved, this, [d]() { + d->conflictResolved(); + }); + connect(handler, &ConflictHandler::error, this, [d](const QString &str) { + d->conflictResolveError(str); + }); + QMetaObject::invokeMethod(handler, &ConflictHandler::start, Qt::QueuedConnection); + return true; + } + } + + if (resp.modificationDateTime().isValid()) { + Item &item = d->mItems.first(); + item.setModificationTime(resp.modificationDateTime()); + item.d_ptr->resetChangeLog(); + } else if (resp.id() > -1) { + auto it = std::find_if(d->mItems.begin(), d->mItems.end(), [&resp](const Item &item) -> bool { + return item.id() == resp.id(); + }); + if (it == d->mItems.end()) { + qCDebug(AKONADICORE_LOG) << "Received STORE response for an item we did not modify: " << tag << Protocol::debugString(response); + return true; + } + + const int newRev = resp.newRevision(); + const int oldRev = (*it).revision(); + if (newRev >= oldRev && newRev >= 0) { + d->itemRevisionChanged((*it).id(), oldRev, newRev); + (*it).setRevision(newRev); + } + // There will be more responses, either for other modified items, + // or the final response with invalid ID, but with modification datetime + return false; + } + + for (const Item &item : std::as_const(d->mItems)) { + ChangeMediator::invalidateItem(item); + } + + return true; + } + + return Job::doHandleResponse(tag, response); +} + +void ItemModifyJob::setIgnorePayload(bool ignore) +{ + Q_D(ItemModifyJob); + + if (d->mIgnorePayload == ignore) { + return; + } + + d->mIgnorePayload = ignore; + if (d->mIgnorePayload) { + d->mParts = QSet(); + } else { + Q_ASSERT(!d->mItems.first().mimeType().isEmpty()); + d->mParts = d->mItems.first().loadedPayloadParts(); + } +} + +bool ItemModifyJob::ignorePayload() const +{ + Q_D(const ItemModifyJob); + + return d->mIgnorePayload; +} + +void ItemModifyJob::setUpdateGid(bool update) +{ + Q_D(ItemModifyJob); + if (update && !updateGid()) { + d->mOperations.insert(ItemModifyJobPrivate::Gid); + } else { + d->mOperations.remove(ItemModifyJobPrivate::Gid); + } +} + +bool ItemModifyJob::updateGid() const +{ + Q_D(const ItemModifyJob); + return d->mOperations.contains(ItemModifyJobPrivate::Gid); +} + +void ItemModifyJob::disableRevisionCheck() +{ + Q_D(ItemModifyJob); + + d->mRevCheck = false; +} + +void ItemModifyJob::disableAutomaticConflictHandling() +{ + Q_D(ItemModifyJob); + + d->mAutomaticConflictHandlingEnabled = false; +} + +Item ItemModifyJob::item() const +{ + Q_D(const ItemModifyJob); + Q_ASSERT(d->mItems.size() == 1); + + return d->mItems.first(); +} + +Item::List ItemModifyJob::items() const +{ + Q_D(const ItemModifyJob); + return d->mItems; +} + +#include "moc_itemmodifyjob.cpp" diff --git a/src/core/jobs/itemmodifyjob.h b/src/core/jobs/itemmodifyjob.h new file mode 100644 index 0000000..c3a3be0 --- /dev/null +++ b/src/core/jobs/itemmodifyjob.h @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class ItemModifyJobPrivate; + +/** + * @short Job that modifies an existing item in the Akonadi storage. + * + * This job is used to writing back items to the Akonadi storage, after + * the user has changed them in any way. + * For performance reasons either the full item (including the full payload) + * can written back or only the meta data of the item. + * + * Example: + * + * @code + * + * // Fetch item with unique id 125 + * Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob( Akonadi::Item( 125 ) ); + * connect( fetchJob, SIGNAL(result(KJob*)), SLOT(fetchFinished(KJob*)) ); + * + * ... + * + * MyClass::fetchFinished( KJob *job ) + * { + * if ( job->error() ) + * return; + * + * Akonadi::ItemFetchJob *fetchJob = qobject_cast( job ); + * + * Akonadi::Item item = fetchJob->items().at(0); + * + * // Set a custom flag + * item.setFlag( "\GotIt" ); + * + * // Store back modified item + * Akonadi::ItemModifyJob *modifyJob = new Akonadi::ItemModifyJob( item ); + * connect( modifyJob, SIGNAL(result(KJob*)), SLOT(modifyFinished(KJob*)) ); + * } + * + * MyClass::modifyFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Item modified successfully"; + * } + * + * @endcode + * + *

Conflict Resolution

+ + * When the job is executed, a check is made to ensure that the Item contained + * in the job is not older than the version of the Item already held in the + * Akonadi database. If it is older, a conflict resolution dialog is displayed + * for the user to choose which version of the Item to use, unless + * disableAutomaticConflictHandling() has been called to disable the dialog, or + * disableRevisionCheck() has been called to disable version checking + * altogether. + * + * The item version is checked by comparing the Item::revision() values in the + * job and in the database. To ensure that two successive ItemModifyJobs for + * the same Item work correctly, the revision number of the Item supplied to + * the second ItemModifyJob should be set equal to the Item's revision number + * on completion of the first ItemModifyJob. This can be obtained by, for + * example, calling item().revision() in the job's result slot. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemModifyJob : public Job +{ + friend class ResourceBase; + + Q_OBJECT + +public: + /** + * Creates a new item modify job. + * + * @param item The modified item object to store. + * @param parent The parent object. + */ + explicit ItemModifyJob(const Item &item, QObject *parent = nullptr); + + /** + * Creates a new item modify job for bulk modifications. + * + * Using this is different from running a modification job per item. + * Use this when applying the same change to a set of items, such as a + * mass-change of item flags, not if you just want to store a bunch of + * randomly modified items. + * + * Currently the following modifications are supported: + * - flag changes + * + * @note Since this does not do payload modifications, it implies + * setIgnorePayload( true ) and disableRevisionCheck(). + * @param items The list of items to modify, must not be empty. + * @since 4.6 + */ + explicit ItemModifyJob(const Item::List &items, QObject *parent = nullptr); + + /** + * Destroys the item modify job. + */ + ~ItemModifyJob() override; + + /** + * Sets whether the payload of the modified item shall be + * omitted from transmission to the Akonadi storage. + * The default is @c false, however it can be set for + * performance reasons. + * @param ignore ignores payload if set as @c true + */ + void setIgnorePayload(bool ignore); + + /** + * Returns whether the payload of the modified item shall be + * omitted from transmission to the Akonadi storage. + */ + Q_REQUIRED_RESULT bool ignorePayload() const; + + /** + * Sets whether the GID shall be updated either from the gid parameter or + * by extracting it from the payload. + * The default is @c false to avoid unnecessarily update the GID, + * as it should never change once set, and the ItemCreateJob already sets it. + * @param update update the GID if set as @c true + * + * @note If disabled the GID will not be updated, but still be used for identification of the item. + * @since 4.12 + */ + void setUpdateGid(bool update); + + /** + * Returns whether the GID should be updated. + * @since 4.12 + */ + Q_REQUIRED_RESULT bool updateGid() const; + + /** + * Disables the check of the revision number. + * + * @note If disabled, no conflict detection is available. + */ + void disableRevisionCheck(); + + /** + * Returns the modified and stored item including the changed revision number. + * + * @note Use this method only when using the single item constructor. + */ + Q_REQUIRED_RESULT Item item() const; + + /** + * Returns the modified and stored items including the changed revision number. + * + * @since 4.6 + */ + Q_REQUIRED_RESULT Item::List items() const; + + /** + * Disables the automatic handling of conflicts. + * + * By default the item modify job will bring up a dialog to resolve + * a conflict that might happen when modifying an item. + * Calling this method will avoid that and the job returns with an + * error in case of a conflict. + * + * @since 4.6 + */ + void disableAutomaticConflictHandling(); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(ItemModifyJob) + /// @endcond +}; + +} + diff --git a/src/core/jobs/itemmodifyjob_p.h b/src/core/jobs/itemmodifyjob_p.h new file mode 100644 index 0000000..333e07b --- /dev/null +++ b/src/core/jobs/itemmodifyjob_p.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job_p.h" + +namespace Akonadi +{ +namespace Protocol +{ +class PartMetaData; +class Command; +class ModifyItemsCommand; +using ModifyItemsCommandPtr = QSharedPointer; +} + +/** + * @internal + */ +class AKONADICORE_EXPORT ItemModifyJobPrivate : public JobPrivate +{ +public: + enum Operation { + RemoteId, + RemoteRevision, + Gid, + Dirty, + }; + + explicit ItemModifyJobPrivate(ItemModifyJob *parent); + + void setClean(); + Protocol::PartMetaData preparePart(const QByteArray &partName); + + void conflictResolved(); + void conflictResolveError(const QString &message); + + void doUpdateItemRevision(Item::Id id, int oldRevision, int newRevision) override; + + QString jobDebuggingString() const override; + Protocol::ModifyItemsCommandPtr fullCommand() const; + + void setSilent(bool silent); + + Q_DECLARE_PUBLIC(ItemModifyJob) + + QSet mOperations; + QByteArray mTag; + Item::List mItems; + bool mRevCheck = true; + QSet mParts; + QSet mForeignParts; + QByteArray mPendingData; + bool mIgnorePayload = false; + bool mAutomaticConflictHandlingEnabled = true; + bool mSilent = false; +}; + +} + diff --git a/src/core/jobs/itemmovejob.cpp b/src/core/jobs/itemmovejob.cpp new file mode 100644 index 0000000..859fb62 --- /dev/null +++ b/src/core/jobs/itemmovejob.cpp @@ -0,0 +1,133 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemmovejob.h" + +#include "collection.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::ItemMoveJobPrivate : public Akonadi::JobPrivate +{ +public: + explicit ItemMoveJobPrivate(ItemMoveJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const override + { + QString str = QStringLiteral("Move item"); + if (source.isValid()) { + str += QStringLiteral("from collection %1").arg(source.id()); + } + str += QStringLiteral(" to collection %1. ").arg(destination.id()); + if (items.isEmpty()) { + str += QStringLiteral("No Items defined."); + } else { + str += QStringLiteral("Items: "); + const int nbItems = items.count(); + for (int i = 0; i < nbItems; ++i) { + if (i != 0) { + str += QStringLiteral(", "); + } + str += QString::number(items.at(i).id()); + } + } + return str; + } + + Item::List items; + Collection destination; + Collection source; + + Q_DECLARE_PUBLIC(ItemMoveJob) +}; + +ItemMoveJob::ItemMoveJob(const Item &item, const Collection &destination, QObject *parent) + : Job(new ItemMoveJobPrivate(this), parent) +{ + Q_D(ItemMoveJob); + d->destination = destination; + d->items.append(item); +} + +ItemMoveJob::ItemMoveJob(const Item::List &items, const Collection &destination, QObject *parent) + : Job(new ItemMoveJobPrivate(this), parent) +{ + Q_D(ItemMoveJob); + d->destination = destination; + d->items = items; +} + +ItemMoveJob::ItemMoveJob(const Item::List &items, const Collection &source, const Collection &destination, QObject *parent) + : Job(new ItemMoveJobPrivate(this), parent) +{ + Q_D(ItemMoveJob); + d->source = source; + d->destination = destination; + d->items = items; +} + +ItemMoveJob::~ItemMoveJob() +{ +} + +void ItemMoveJob::doStart() +{ + Q_D(ItemMoveJob); + + if (d->items.isEmpty()) { + setError(Job::Unknown); + setErrorText(i18n("No objects specified for moving")); + emitResult(); + return; + } + + if (!d->destination.isValid() && d->destination.remoteId().isEmpty()) { + setError(Job::Unknown); + setErrorText(i18n("No valid destination specified")); + emitResult(); + return; + } + + try { + d->sendCommand(Protocol::MoveItemsCommandPtr::create(ProtocolHelper::entitySetToScope(d->items), + ProtocolHelper::commandContextToProtocol(d->source, Tag(), d->items), + ProtocolHelper::entityToScope(d->destination))); + } catch (const Akonadi::Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } +} + +bool ItemMoveJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::MoveItems) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Collection ItemMoveJob::destinationCollection() const +{ + Q_D(const ItemMoveJob); + return d->destination; +} + +Item::List ItemMoveJob::items() const +{ + Q_D(const ItemMoveJob); + return d->items; +} diff --git a/src/core/jobs/itemmovejob.h b/src/core/jobs/itemmovejob.h new file mode 100644 index 0000000..3b1c1fd --- /dev/null +++ b/src/core/jobs/itemmovejob.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" +namespace Akonadi +{ +class Collection; +class ItemMoveJobPrivate; + +/** + * @short Job that moves an item into a different collection in the Akonadi storage. + * + * This job takes an item and moves it to a collection in the Akonadi storage. + * + * @code + * + * Akonadi::Item item = ... + * Akonadi::Collection collection = ... + * + * Akonadi::ItemMoveJob *job = new Akonadi::ItemMoveJob( item, collection ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(moveResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ItemMoveJob : public Job +{ + Q_OBJECT + +public: + /** + * Move the given item into the given collection. + * + * @param item The item to move. + * @param destination The destination collection. + * @param parent The parent object. + */ + ItemMoveJob(const Item &item, const Collection &destination, QObject *parent = nullptr); + + /** + * Move the given items into @p destination. + * + * @param items A list of items to move. + * @param destination The destination collection. + * @param parent The parent object. + */ + ItemMoveJob(const Item::List &items, const Collection &destination, QObject *parent = nullptr); + + /** + * Move the given items from @p source to @p destination. + * + * @internal If the items are identified only by RID, then you MUST use this + * constructor to specify the source collection, otherwise the job will fail. + * RID-based moves are only allowed to resources. + * + * @since 4.14 + */ + ItemMoveJob(const Item::List &items, const Collection &source, const Collection &destination, QObject *parent = nullptr); + + /** + * Destroys the item move job. + */ + ~ItemMoveJob() override; + + /** + * Returns the destination collection. + * + * @since 4.7 + */ + Q_REQUIRED_RESULT Collection destinationCollection() const; + + /** + * Returns the list of items that where passed in the constructor. + * + * @since 4.7 + */ + Q_REQUIRED_RESULT Akonadi::Item::List items() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(ItemMoveJob) +}; + +} + diff --git a/src/core/jobs/itemsearchjob.cpp b/src/core/jobs/itemsearchjob.cpp new file mode 100644 index 0000000..682920a --- /dev/null +++ b/src/core/jobs/itemsearchjob.cpp @@ -0,0 +1,269 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemsearchjob.h" + +#include "itemfetchscope.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" +#include "searchquery.h" +#include "tagfetchscope.h" + +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::ItemSearchJobPrivate : public JobPrivate +{ +public: + ItemSearchJobPrivate(ItemSearchJob *parent, const SearchQuery &query) + : JobPrivate(parent) + , mQuery(query) + { + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + } + + void init() + { + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { + timeout(); + }); + } + + void aboutToFinish() override + { + timeout(); + } + + void timeout() + { + Q_Q(Akonadi::ItemSearchJob); + + mEmitTimer.stop(); // in case we are called by result() + if (!mPendingItems.isEmpty()) { + if (!q->error()) { + Q_EMIT q->itemsReceived(mPendingItems); + } + mPendingItems.clear(); + } + } + QString jobDebuggingString() const override + { + QStringList flags; + if (mRecursive) { + flags.append(QStringLiteral("recursive")); + } + if (mRemote) { + flags.append(QStringLiteral("remote")); + } + if (mCollections.isEmpty()) { + flags.append(QStringLiteral("all collections")); + } else { + flags.append(QStringLiteral("%1 collections").arg(mCollections.count())); + } + return QStringLiteral("%1,json=%2").arg(flags.join(QLatin1Char(',')), QString::fromUtf8(mQuery.toJSON())); + } + + Q_DECLARE_PUBLIC(ItemSearchJob) + + SearchQuery mQuery; + Collection::List mCollections; + QStringList mMimeTypes; + bool mRecursive = false; + bool mRemote = false; + ItemFetchScope mItemFetchScope; + TagFetchScope mTagFetchScope; + + Item::List mItems; + Item::List mPendingItems; // items pending for emitting itemsReceived() + + QTimer mEmitTimer; +}; + +QThreadStorage instances; + +static void cleanupDefaultSearchSession() +{ + instances.setLocalData(nullptr); +} + +static Session *defaultSearchSession() +{ + if (!instances.hasLocalData()) { + const QByteArray sessionName = Session::defaultSession()->sessionId() + "-SearchSession"; + instances.setLocalData(new Session(sessionName)); + qAddPostRoutine(cleanupDefaultSearchSession); + } + return instances.localData(); +} + +static QObject *sessionForJob(QObject *parent) +{ + if (qobject_cast(parent) || qobject_cast(parent)) { + return parent; + } + return defaultSearchSession(); +} + +ItemSearchJob::ItemSearchJob(QObject *parent) + : Job(new ItemSearchJobPrivate(this, SearchQuery()), sessionForJob(parent)) +{ + Q_D(ItemSearchJob); + d->init(); +} + +ItemSearchJob::ItemSearchJob(const SearchQuery &query, QObject *parent) + : Job(new ItemSearchJobPrivate(this, query), sessionForJob(parent)) +{ + Q_D(ItemSearchJob); + d->init(); +} + +ItemSearchJob::~ItemSearchJob() = default; + +void ItemSearchJob::setQuery(const SearchQuery &query) +{ + Q_D(ItemSearchJob); + + d->mQuery = query; +} + +void ItemSearchJob::setFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(ItemSearchJob); + + d->mItemFetchScope = fetchScope; +} + +ItemFetchScope &ItemSearchJob::fetchScope() +{ + Q_D(ItemSearchJob); + + return d->mItemFetchScope; +} + +void ItemSearchJob::setTagFetchScope(const TagFetchScope &fetchScope) +{ + Q_D(ItemSearchJob); + + d->mTagFetchScope = fetchScope; +} + +TagFetchScope &ItemSearchJob::tagFetchScope() +{ + Q_D(ItemSearchJob); + + return d->mTagFetchScope; +} + +void ItemSearchJob::setSearchCollections(const Collection::List &collections) +{ + Q_D(ItemSearchJob); + + d->mCollections = collections; +} + +Collection::List ItemSearchJob::searchCollections() const +{ + return d_func()->mCollections; +} + +void ItemSearchJob::setMimeTypes(const QStringList &mimeTypes) +{ + Q_D(ItemSearchJob); + + d->mMimeTypes = mimeTypes; +} + +QStringList ItemSearchJob::mimeTypes() const +{ + return d_func()->mMimeTypes; +} + +void ItemSearchJob::setRecursive(bool recursive) +{ + Q_D(ItemSearchJob); + + d->mRecursive = recursive; +} + +bool ItemSearchJob::isRecursive() const +{ + return d_func()->mRecursive; +} + +void ItemSearchJob::setRemoteSearchEnabled(bool enabled) +{ + Q_D(ItemSearchJob); + + d->mRemote = enabled; +} + +bool ItemSearchJob::isRemoteSearchEnabled() const +{ + return d_func()->mRemote; +} + +void ItemSearchJob::doStart() +{ + Q_D(ItemSearchJob); + + auto cmd = Protocol::SearchCommandPtr::create(); + cmd->setMimeTypes(d->mMimeTypes); + if (!d->mCollections.isEmpty()) { + QVector ids; + ids.reserve(d->mCollections.size()); + for (const Collection &col : std::as_const(d->mCollections)) { + ids << col.id(); + } + cmd->setCollections(ids); + } + cmd->setRecursive(d->mRecursive); + cmd->setRemote(d->mRemote); + cmd->setQuery(QString::fromUtf8(d->mQuery.toJSON())); + cmd->setItemFetchScope(ProtocolHelper::itemFetchScopeToProtocol(d->mItemFetchScope)); + cmd->setTagFetchScope(ProtocolHelper::tagFetchScopeToProtocol(d->mTagFetchScope)); + + d->sendCommand(cmd); +} + +bool ItemSearchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(ItemSearchJob); + + if (response->isResponse() && response->type() == Protocol::Command::FetchItems) { + const Item item = ProtocolHelper::parseItemFetchResult(Protocol::cmdCast(response)); + if (!item.isValid()) { + return false; + } + d->mItems.append(item); + d->mPendingItems.append(item); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); + } + + return false; + } + + if (response->isResponse() && response->type() == Protocol::Command::Search) { + return true; + } + + return Job::doHandleResponse(tag, response); +} + +Item::List ItemSearchJob::items() const +{ + Q_D(const ItemSearchJob); + + return d->mItems; +} + +#include "moc_itemsearchjob.cpp" diff --git a/src/core/jobs/itemsearchjob.h b/src/core/jobs/itemsearchjob.h new file mode 100644 index 0000000..6d9b45c --- /dev/null +++ b/src/core/jobs/itemsearchjob.h @@ -0,0 +1,248 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class TagFetchScope; +class ItemFetchScope; +class ItemSearchJobPrivate; +class SearchQuery; + +/** + * @short Job that searches for items in the Akonadi storage. + * + * This job searches for items that match a given search query and returns + * the list of matching item. + * + * @code + * + * SearchQuery query; + * query.addTerm( SearchTerm( "From", "user1@domain.example", SearchTerm::CondEqual ) ); + * query.addTerm( SearchTerm( "Date", QDateTime( QDate( 2014, 01, 27 ), QTime( 00, 00, 00 ) ), SearchTerm::CondGreaterThan ); + * + * Akonadi::ItemSearchJob *job = new Akonadi::ItemSearchJob( query ); + * job->fetchScope().fetchFullPayload(); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(searchResult(KJob*)) ); + * + * ... + * + * MyClass::searchResult( KJob *job ) + * { + * Akonadi::ItemSearchJob *searchJob = qobject_cast( job ); + * const Akonadi::Item::List items = searchJob->items(); + * for ( const Akonadi::Item &item : items ) { + * // extract the payload and do further stuff + * } + * } + * + * @endcode + * + * @author Tobias Koenig + * @since 4.4 + */ +class AKONADICORE_EXPORT ItemSearchJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates an invalid search job. + * + * @param parent The parent object. + * @since 5.1 + */ + explicit ItemSearchJob(QObject *parent = nullptr); + + /** + * Creates an item search job. + * + * @param query The search query. + * @param parent The parent object. + * @since 4.13 + */ + explicit ItemSearchJob(const SearchQuery &query, QObject *parent = nullptr); + + /** + * Destroys the item search job. + */ + ~ItemSearchJob() override; + + /** + * Sets the search @p query. + * + * @since 4.13 + */ + void setQuery(const SearchQuery &query); + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an matching item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &fetchScope(); + + /** + * Sets the tag fetch scope. + * + * The tag fetch scope affects what scope of tags for each Item will be + * retrieved. + */ + void setTagFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify + * the current scope in-place. + */ + TagFetchScope &tagFetchScope(); + + /** + * Returns the items that matched the search query. + */ + Q_REQUIRED_RESULT Item::List items() const; + + /** + * Search only for items of given mime types. + * + * @since 4.13 + */ + void setMimeTypes(const QStringList &mimeTypes); + + /** + * Returns list of mime types to search in + * + * @since 4.13 + */ + Q_REQUIRED_RESULT QStringList mimeTypes() const; + + /** + * Search only in given collections. + * + * When recursive search is enabled, all child collections of each specified + * collection will be searched too + * + * By default all collections are be searched. + * + * @param collections Collections to search + * @since 4.13 + */ + void setSearchCollections(const Collection::List &collections); + + /** + * Returns list of collections to search. + * + * This list does not include child collections that will be searched when + * recursive search is enabled + * + * @since 4.13 + */ + Q_REQUIRED_RESULT Collection::List searchCollections() const; + + /** + * Sets whether the search should recurse into collections + * + * When set to true, all child collections of the specific collections will + * be search recursively. + * + * @param recursive Whether to search recursively + * @since 4.13 + */ + void setRecursive(bool recursive); + + /** + * Returns whether the search is recursive + * + * @since 4.13 + */ + bool isRecursive() const; + + /** + * Sets whether resources should be queried too. + * + * When set to true, Akonadi will search local indexed items and will also + * query resources that support server-side search, to forward the query + * to remote storage (for example using SEARCH feature on IMAP servers) and + * merge their results with results from local index. + * + * This is useful especially when searching resources, that don't fetch full + * payload by default, for example the IMAP resource, which only fetches headers + * by default and the body is fetched on demand, which means that emails that + * were not yet fully fetched cannot be indexed in local index, and thus cannot + * be searched. With remote search, even those emails can be included in search + * results. + * + * This feature is disabled by default. + * + * Results are streamed back to client as they are received from queried sources, + * so this job can take some time to finish, but will deliver initial results + * from local index fairly quickly. + * + * @param enabled Whether remote search is enabled + * @since 4.13 + */ + void setRemoteSearchEnabled(bool enabled); + + /** + * Returns whether remote search is enabled. + * + * @since 4.13 + */ + Q_REQUIRED_RESULT bool isRemoteSearchEnabled() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new matching items have been fetched completely. + * + * @note This is an optimization, instead of waiting for the end of the job + * and calling items(), you can connect to this signal and get the items + * incrementally. + * + * @param items The matching items. + */ + void itemsReceived(const Akonadi::Item::List &items); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(ItemSearchJob) + + Q_PRIVATE_SLOT(d_func(), void timeout()) + /// @endcond +}; + +} + diff --git a/src/core/jobs/job.cpp b/src/core/jobs/job.cpp new file mode 100644 index 0000000..4e97676 --- /dev/null +++ b/src/core/jobs/job.cpp @@ -0,0 +1,417 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + 2006 Marc Mutz + 2006 - 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "job.h" +#include "akonadicore_debug.h" +#include "job_p.h" +#include "private/instance_p.h" +#include "private/protocol_p.h" +#include "session.h" +#include "session_p.h" +#include +#include + +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +static QDBusAbstractInterface *s_jobtracker = nullptr; + +/// @cond PRIVATE +void JobPrivate::handleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_Q(Job); + + if (mCurrentSubJob) { + mCurrentSubJob->d_ptr->handleResponse(tag, response); + return; + } + + if (tag == mTag) { + if (response->isResponse()) { + const auto &resp = Protocol::cmdCast(response); + if (resp.isError()) { + q->setError(Job::Unknown); + q->setErrorText(resp.errorMessage()); + q->emitResult(); + return; + } + } + } + + if (mTag != tag) { + qCWarning(AKONADICORE_LOG) << "Received response with a different tag!"; + qCDebug(AKONADICORE_LOG) << "Response tag:" << tag << ", response type:" << response->type(); + qCDebug(AKONADICORE_LOG) << "Job tag:" << mTag << ", job:" << q; + return; + } + + if (mStarted) { + if (mReadingFinished) { + qCWarning(AKONADICORE_LOG) << "Received response for a job that does not expect any more data, ignoring"; + qCDebug(AKONADICORE_LOG) << "Response tag:" << tag << ", response type:" << response->type(); + qCDebug(AKONADICORE_LOG) << "Job tag:" << mTag << ", job:" << q; + Q_ASSERT(!mReadingFinished); + return; + } + + if (q->doHandleResponse(tag, response)) { + mReadingFinished = true; + QTimer::singleShot(0, q, [this]() { + delayedEmitResult(); + }); + } + } +} + +void JobPrivate::init(QObject *parent) +{ + Q_Q(Job); + + mParentJob = qobject_cast(parent); + mSession = qobject_cast(parent); + + if (!mSession) { + if (!mParentJob) { + mSession = Session::defaultSession(); + } else { + mSession = mParentJob->d_ptr->mSession; + } + } + + if (!mParentJob) { + mSession->d->addJob(q); + } else { + mParentJob->addSubjob(q); + } + publishJob(); +} + +void JobPrivate::publishJob() +{ + Q_Q(Job); + // if there's a job tracker running, tell it about the new job + if (!s_jobtracker) { + // Let's only check for the debugging console every 3 seconds, otherwise every single job + // makes a dbus call to the dbus daemon, doesn't help performance. + static QElapsedTimer s_lastTime; + if (!s_lastTime.isValid() || s_lastTime.elapsed() > 3000) { + if (!s_lastTime.isValid()) { + s_lastTime.start(); + } + const QString suffix = Akonadi::Instance::identifier().isEmpty() ? QString() : QLatin1Char('-') + Akonadi::Instance::identifier(); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.akonadiconsole") + suffix)) { + s_jobtracker = new QDBusInterface(QLatin1String("org.kde.akonadiconsole") + suffix, + QStringLiteral("/jobtracker"), + QStringLiteral("org.freedesktop.Akonadi.JobTracker"), + QDBusConnection::sessionBus(), + nullptr); + mSession->d->publishOtherJobs(q); + } else { + s_lastTime.restart(); + } + } + // Note: we never reset s_jobtracker to 0 when a call fails; but if we did + // then we should restart s_lastTime. + } + QMetaObject::invokeMethod(q, "signalCreationToJobTracker", Qt::QueuedConnection); +} + +void JobPrivate::signalCreationToJobTracker() +{ + Q_Q(Job); + if (s_jobtracker) { + // We do these dbus calls manually, so as to avoid having to install (or copy) the console's + // xml interface document. Since this is purely a debugging aid, that seems preferable to + // publishing something not intended for public consumption. + // WARNING: for any signature change here, apply it to resourcescheduler.cpp too + const QList argumentList = QList() << QLatin1String(mSession->sessionId()) << QString::number(reinterpret_cast(q), 16) + << (mParentJob ? QString::number(reinterpret_cast(mParentJob), 16) : QString()) + << QString::fromLatin1(q->metaObject()->className()) << jobDebuggingString(); + QDBusPendingCall call = s_jobtracker->asyncCallWithArgumentList(QStringLiteral("jobCreated"), argumentList); + + auto watcher = new QDBusPendingCallWatcher(call, s_jobtracker); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, s_jobtracker, [](QDBusPendingCallWatcher *w) { + QDBusPendingReply reply = *w; + if (reply.isError() && s_jobtracker) { + qDebug() << reply.error().name() << reply.error().message(); + s_jobtracker->deleteLater(); + s_jobtracker = nullptr; + } + w->deleteLater(); + }); + } +} + +void JobPrivate::signalStartedToJobTracker() +{ + Q_Q(Job); + if (s_jobtracker) { + // if there's a job tracker running, tell it a job started + const QList argumentList = {QString::number(reinterpret_cast(q), 16)}; + s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobStarted"), argumentList); + } +} + +void JobPrivate::aboutToFinish() +{ + // Dummy +} + +void JobPrivate::delayedEmitResult() +{ + Q_Q(Job); + if (q->hasSubjobs()) { + // We still have subjobs, wait for them to finish + mFinishPending = true; + } else { + aboutToFinish(); + q->emitResult(); + } +} + +void JobPrivate::startQueued() +{ + Q_Q(Job); + mStarted = true; + + Q_EMIT q->aboutToStart(q); + q->doStart(); + QTimer::singleShot(0, q, [this]() { + startNext(); + }); + QMetaObject::invokeMethod(q, "signalStartedToJobTracker", Qt::QueuedConnection); +} + +void JobPrivate::lostConnection() +{ + Q_Q(Job); + + if (mCurrentSubJob) { + mCurrentSubJob->d_ptr->lostConnection(); + } else { + q->setError(Job::ConnectionFailed); + q->emitResult(); + } +} + +void JobPrivate::slotSubJobAboutToStart(Job *job) +{ + Q_ASSERT(mCurrentSubJob == nullptr); + mCurrentSubJob = job; +} + +void JobPrivate::startNext() +{ + Q_Q(Job); + + if (mStarted && !mCurrentSubJob && q->hasSubjobs()) { + Job *job = qobject_cast(q->subjobs().at(0)); + Q_ASSERT(job); + job->d_ptr->startQueued(); + } else if (mFinishPending && !q->hasSubjobs()) { + // The last subjob we've been waiting for has finished, emitResult() finally + QTimer::singleShot(0, q, [this]() { + delayedEmitResult(); + }); + } +} + +qint64 JobPrivate::newTag() +{ + if (mParentJob) { + mTag = mParentJob->d_ptr->newTag(); + } else { + mTag = mSession->d->nextTag(); + } + return mTag; +} + +qint64 JobPrivate::tag() const +{ + return mTag; +} + +void JobPrivate::sendCommand(qint64 tag, const Protocol::CommandPtr &cmd) +{ + if (mParentJob) { + mParentJob->d_ptr->sendCommand(tag, cmd); + } else { + mSession->d->sendCommand(tag, cmd); + } +} + +void JobPrivate::sendCommand(const Protocol::CommandPtr &cmd) +{ + sendCommand(newTag(), cmd); +} + +void JobPrivate::itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + mSession->d->itemRevisionChanged(itemId, oldRevision, newRevision); +} + +void JobPrivate::updateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + Q_Q(Job); + const auto &subjobs = q->subjobs(); + for (KJob *j : subjobs) { + auto job = qobject_cast(j); + if (job) { + job->d_ptr->updateItemRevision(itemId, oldRevision, newRevision); + } + } + doUpdateItemRevision(itemId, oldRevision, newRevision); +} + +void JobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + Q_UNUSED(itemId) + Q_UNUSED(oldRevision) + Q_UNUSED(newRevision) +} + +int JobPrivate::protocolVersion() const +{ + return mSession->d->protocolVersion; +} +/// @endcond + +Job::Job(QObject *parent) + : KCompositeJob(parent) + , d_ptr(new JobPrivate(this)) +{ + d_ptr->init(parent); +} + +Job::Job(JobPrivate *dd, QObject *parent) + : KCompositeJob(parent) + , d_ptr(dd) +{ + d_ptr->init(parent); +} + +Job::~Job() +{ + // if there is a job tracer listening, tell it the job is done now + if (s_jobtracker) { + const QList argumentList = {QString::number(reinterpret_cast(this), 16), errorString()}; + s_jobtracker->callWithArgumentList(QDBus::NoBlock, QStringLiteral("jobEnded"), argumentList); + } + + delete d_ptr; +} + +void Job::start() +{ +} + +bool Job::doKill() +{ + Q_D(Job); + if (d->mStarted) { + // the only way to cancel an already started job is reconnecting to the server + d->mSession->d->forceReconnect(); + } + d->mStarted = false; + return true; +} + +QString Job::errorString() const +{ + QString str; + switch (error()) { + case NoError: + break; + case ConnectionFailed: + str = i18n("Cannot connect to the Akonadi service."); + break; + case ProtocolVersionMismatch: + str = i18n("The protocol version of the Akonadi server is incompatible. Make sure you have a compatible version installed."); + break; + case UserCanceled: + str = i18n("User canceled operation."); + break; + case Unknown: + return errorText(); + case UserError: + str = i18n("Unknown error."); + break; + } + if (!errorText().isEmpty()) { + str += QStringLiteral(" (%1)").arg(errorText()); + } + return str; +} + +bool Job::addSubjob(KJob *job) +{ + bool rv = KCompositeJob::addSubjob(job); + if (rv) { + connect(qobject_cast(job), &Job::aboutToStart, this, [this](Job *job) { + d_ptr->slotSubJobAboutToStart(job); + }); + QTimer::singleShot(0, this, [this]() { + d_ptr->startNext(); + }); + } + return rv; +} + +bool Job::removeSubjob(KJob *job) +{ + bool rv = KCompositeJob::removeSubjob(job); + if (job == d_ptr->mCurrentSubJob) { + d_ptr->mCurrentSubJob = nullptr; + QTimer::singleShot(0, this, [this]() { + d_ptr->startNext(); + }); + } + return rv; +} + +bool Akonadi::Job::doHandleResponse(qint64 tag, const Akonadi::Protocol::CommandPtr &response) +{ + qCDebug(AKONADICORE_LOG) << this << "Unhandled response: " << tag << Protocol::debugString(response); + setError(Unknown); + setErrorText(i18n("Unexpected response")); + emitResult(); + return true; +} + +void Job::slotResult(KJob *job) +{ + if (d_ptr->mCurrentSubJob == job) { + // current job finished, start the next one + d_ptr->mCurrentSubJob = nullptr; + KCompositeJob::slotResult(job); + if (!job->error()) { + QTimer::singleShot(0, this, [this]() { + d_ptr->startNext(); + }); + } + } else { + // job that was still waiting for execution finished, probably canceled, + // so just remove it from the queue and move on without caring about + // its error code + KCompositeJob::removeSubjob(job); + } +} + +void Job::emitWriteFinished() +{ + d_ptr->mWriteFinished = true; + Q_EMIT writeFinished(this); +} + +#include "moc_job.cpp" diff --git a/src/core/jobs/job.h b/src/core/jobs/job.h new file mode 100644 index 0000000..1ad124e --- /dev/null +++ b/src/core/jobs/job.h @@ -0,0 +1,220 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + 2006 Marc Mutz + 2006 - 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +class QString; + +namespace Akonadi +{ +namespace Protocol +{ +class Command; +using CommandPtr = QSharedPointer; +} + +class JobPrivate; +class Session; +class SessionPrivate; + +/** + * @short Base class for all actions in the Akonadi storage. + * + * This class encapsulates a request to the pim storage service, + * the code looks like + * + * @code + * + * Akonadi::Job *job = new Akonadi::SomeJob( some parameter ); + * connect( job, SIGNAL(result(KJob*)), + * this, SLOT(slotResult(KJob*)) ); + * + * @endcode + * + * The job is queued for execution as soon as the event loop is entered + * again. + * + * And the slotResult is usually at least: + * + * @code + * + * if ( job->error() ) { + * // handle error... + * } + * + * @endcode + * + * With the synchronous interface the code looks like + * + * @code + * Akonadi::SomeJob *job = new Akonadi::SomeJob( some parameter ); + * if ( !job->exec() ) { + * qDebug() << "Error:" << job->errorString(); + * } else { + * // do something + * } + * @endcode + * + * @warning Using the synchronous method is error prone, use this only + * if the asynchronous access is not possible. See the documentation of + * KJob::exec() for more details. + * + * Subclasses must reimplement doStart(). + * + * @note KJob-derived objects delete itself, it is thus not possible + * to create job objects on the stack! + * + * @author Volker Krause , Tobias Koenig , Marc Mutz + */ +class AKONADICORE_EXPORT Job : public KCompositeJob +{ + Q_OBJECT + + friend class Session; + friend class SessionPrivate; + +public: + /** + * Describes a list of jobs. + */ + using List = QList; + + /** + * Describes the error codes that can be emitted by this class. + * Subclasses can provide additional codes, starting from UserError + * onwards + */ + enum Error { + ConnectionFailed = UserDefinedError, ///< The connection to the Akonadi server failed. + ProtocolVersionMismatch, ///< The server protocol version is too old or too new. + UserCanceled, ///< The user canceled this job. + Unknown, ///< Unknown error. + UserError = UserDefinedError + 42 ///< Starting point for error codes defined by sub-classes. + }; + + /** + * Creates a new job. + * + * If the parent object is a Job object, the new job will be a subjob of @p parent. + * If the parent object is a Session object, it will be used for server communication + * instead of the default session. + * + * @param parent The parent object, job or session. + */ + explicit Job(QObject *parent = nullptr); + + /** + * Destroys the job. + */ + ~Job() override; + + /** + * Jobs are started automatically once entering the event loop again, no need + * to explicitly call this. + */ + void start() override; + + /** + * Returns the error string, if there has been an error, an empty + * string otherwise. + */ + Q_REQUIRED_RESULT QString errorString() const final; + +Q_SIGNALS: + /** + * This signal is emitted directly before the job will be started. + * + * @param job The started job. + */ + void aboutToStart(Akonadi::Job *job); + + /** + * This signal is emitted if the job has finished all write operations, ie. + * if this signal is emitted, the job guarantees to not call writeData() again. + * Do not emit this signal directly, call emitWriteFinished() instead. + * + * @param job This job. + * @see emitWriteFinished() + */ + void writeFinished(Akonadi::Job *job); + +protected: + /** + * This method must be reimplemented in the concrete jobs. It will be called + * after the job has been started and a connection to the Akonadi backend has + * been established. + */ + virtual void doStart() = 0; + + /** + * This method should be reimplemented in the concrete jobs in case you want + * to handle incoming data. It will be called on received data from the backend. + * The default implementation does nothing. + * + * @param tag The tag of the corresponding command, empty if this is an untagged response. + * @param response The received response + * + * @return Implementations should return true if the last response was processed and + * the job can emit result. Return false if more responses from server are expected. + */ + virtual bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response); + + /** + * Adds the given job as a subjob to this job. This method is automatically called + * if you construct a job using another job as parent object. + * The base implementation does the necessary setup to share the network connection + * with the backend. + * + * @param job The new subjob. + */ + bool addSubjob(KJob *job) override; + + /** + * Removes the given subjob of this job. + * + * @param job The subjob to remove. + */ + bool removeSubjob(KJob *job) override; + + /** + * Kills the execution of the job. + */ + bool doKill() override; + + /** + * Call this method to indicate that this job will not call writeData() again. + * @see writeFinished() + */ + void emitWriteFinished(); + +protected Q_SLOTS: + void slotResult(KJob *job) override; + +protected: + /// @cond PRIVATE + Job(JobPrivate *dd, QObject *parent); + JobPrivate *const d_ptr; + /// @endcond + +private: + Q_DECLARE_PRIVATE(Job) + + /// @cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void startNext()) + Q_PRIVATE_SLOT(d_func(), void signalCreationToJobTracker()) + Q_PRIVATE_SLOT(d_func(), void signalStartedToJobTracker()) + Q_PRIVATE_SLOT(d_func(), void delayedEmitResult()) + /// @endcond +}; + +} + diff --git a/src/core/jobs/job_p.h b/src/core/jobs/job_p.h new file mode 100644 index 0000000..61aa656 --- /dev/null +++ b/src/core/jobs/job_p.h @@ -0,0 +1,118 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "item.h" +#include "session.h" + +namespace Akonadi +{ +namespace Protocol +{ +class Command; +} + +/** + * @internal + */ +class JobPrivate +{ +public: + explicit JobPrivate(Job *parent) + : q_ptr(parent) + { + } + + virtual ~JobPrivate() = default; + + void init(QObject *parent); + + void handleResponse(qint64 tag, const Protocol::CommandPtr &response); + void startQueued(); + void lostConnection(); + void slotSubJobAboutToStart(Akonadi::Job *job); + void startNext(); + void signalCreationToJobTracker(); + void signalStartedToJobTracker(); + void delayedEmitResult(); + void publishJob(); + /* + Returns a string to display in akonadi console's job tracker. E.g. item ID. + */ + virtual QString jobDebuggingString() const + { + return QString(); + } + /** + Returns a new unique command tag for communication with the backend. + */ + Q_REQUIRED_RESULT qint64 newTag(); + + /** + Return the tag used for the request. + */ + Q_REQUIRED_RESULT qint64 tag() const; + + /** + Sends the @p command to the backend + */ + void sendCommand(qint64 tag, const Protocol::CommandPtr &command); + + /** + * Same as calling JobPrivate::sendCommand(newTag(), command) + */ + void sendCommand(const Protocol::CommandPtr &command); + + /** + * Notify following jobs about item revision changes. + * This is used to avoid phantom conflicts between pipelined modify jobs on the same item. + * @param itemId the id of the item which has changed + * @param oldRevision the old item revision + * @param newRevision the new item revision + */ + void itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision); + + /** + * Propagate item revision changes to this job and its sub-jobs. + */ + void updateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision); + + /** + * Overwrite this if your job does operations with conflict detection and update + * the item revisions if your items are affected. The default implementation does nothing. + */ + virtual void doUpdateItemRevision(Akonadi::Item::Id, int oldRevision, int newRevision); + + /** + * This method is called right before result() and finished() signals are emitted. + * Overwrite this method in your job if you need to emit some signals or process + * some data before the job finishes. + * + * Default implementation does nothing. + */ + virtual void aboutToFinish(); + + Q_REQUIRED_RESULT int protocolVersion() const; + + Job *q_ptr; + Q_DECLARE_PUBLIC(Job) + + Job *mParentJob = nullptr; + Job *mCurrentSubJob = nullptr; + qint64 mTag = -1; + Session *mSession = nullptr; + bool mWriteFinished = false; + bool mReadingFinished = false; + bool mStarted = false; + bool mFinishPending = false; + +private: + Q_DISABLE_COPY_MOVE(JobPrivate) +}; + +} + diff --git a/src/core/jobs/kjobprivatebase.cpp b/src/core/jobs/kjobprivatebase.cpp new file mode 100644 index 0000000..4a8db81 --- /dev/null +++ b/src/core/jobs/kjobprivatebase.cpp @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "kjobprivatebase_p.h" + +using namespace Akonadi; + +void KJobPrivateBase::start() +{ + const ServerManager::State serverState = ServerManager::state(); + + if (serverState == ServerManager::Running) { + doStart(); + return; + } + + connect(ServerManager::self(), &ServerManager::stateChanged, this, &KJobPrivateBase::serverStateChanged); + + if (serverState == ServerManager::NotRunning) { + ServerManager::start(); + } +} + +void KJobPrivateBase::serverStateChanged(Akonadi::ServerManager::State state) +{ + if (state == ServerManager::Running) { + disconnect(ServerManager::self(), &ServerManager::stateChanged, this, &KJobPrivateBase::serverStateChanged); + doStart(); + } +} + +#include "moc_kjobprivatebase_p.cpp" diff --git a/src/core/jobs/kjobprivatebase_p.h b/src/core/jobs/kjobprivatebase_p.h new file mode 100644 index 0000000..2a8e033 --- /dev/null +++ b/src/core/jobs/kjobprivatebase_p.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "servermanager.h" + +namespace Akonadi +{ +/** + * Base class for the private class of KJob but not Akonadi::Job based jobs that + * require the Akonadi server to be operational. + * Delays job execution until that is the case. + * @internal + */ +class KJobPrivateBase : public QObject +{ + Q_OBJECT + +public: + /** Call from KJob::start() reimplementation. */ + void start(); + + /** Reimplement and put here what was in KJob::start() before. */ + virtual void doStart() = 0; + +private Q_SLOTS: + void serverStateChanged(Akonadi::ServerManager::State state); +}; + +} + diff --git a/src/core/jobs/linkjob.cpp b/src/core/jobs/linkjob.cpp new file mode 100644 index 0000000..79089cd --- /dev/null +++ b/src/core/jobs/linkjob.cpp @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "linkjob.h" + +#include "collection.h" +#include "job_p.h" +#include "linkjobimpl_p.h" + +using namespace Akonadi; + +class Akonadi::LinkJobPrivate : public LinkJobImpl +{ +public: + explicit LinkJobPrivate(LinkJob *parent) + : LinkJobImpl(parent) + { + } +}; + +LinkJob::LinkJob(const Collection &collection, const Item::List &items, QObject *parent) + : Job(new LinkJobPrivate(this), parent) +{ + Q_D(LinkJob); + d->destination = collection; + d->objectsToLink = items; +} + +LinkJob::~LinkJob() +{ +} + +void LinkJob::doStart() +{ + Q_D(LinkJob); + d->sendCommand(Protocol::LinkItemsCommand::Link); +} + +bool LinkJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + return d_func()->handleResponse(tag, response); +} diff --git a/src/core/jobs/linkjob.h b/src/core/jobs/linkjob.h new file mode 100644 index 0000000..d556bea --- /dev/null +++ b/src/core/jobs/linkjob.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class LinkJobPrivate; + +/** + * @short Job that links items inside the Akonadi storage. + * + * This job allows you to create references to a set of items in a virtual + * collection. + * + * Example: + * + * @code + * + * // Links the given items to the given virtual collection + * const Akonadi::Collection virtualCollection = ... + * const Akonadi::Item::List items = ... + * + * Akonadi::LinkJob *job = new Akonadi::LinkJob( virtualCollection, items ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Linked items successfully"; + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.2 + * @see UnlinkJob + */ +class AKONADICORE_EXPORT LinkJob : public Job +{ + Q_OBJECT +public: + /** + * Creates the link job. + * + * The job will create references to the given items in the given collection. + * + * @param collection The collection in which the references should be created. + * @param items The items of which the references should be created. + * @param parent The parent object. + */ + LinkJob(const Collection &collection, const Item::List &items, QObject *parent = nullptr); + + /** + * Destroys the link job. + */ + ~LinkJob() override; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(LinkJob) + template friend class LinkJobImpl; +}; + +} + diff --git a/src/core/jobs/linkjobimpl_p.h b/src/core/jobs/linkjobimpl_p.h new file mode 100644 index 0000000..a22a8dd --- /dev/null +++ b/src/core/jobs/linkjobimpl_p.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2008, 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collection.h" +#include "item.h" +#include "job.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +#include + +namespace Akonadi +{ +/** Shared implementation details between item and collection move jobs. */ +template class LinkJobImpl : public JobPrivate +{ +public: + LinkJobImpl(Job *parent) + : JobPrivate(parent) + { + } + + inline void sendCommand(Protocol::LinkItemsCommand::Action action) + { + auto q = static_cast(q_func()); // Job would be enough already, but then we don't have access to the non-public stuff... + if (objectsToLink.isEmpty()) { + q->emitResult(); + return; + } + if (!destination.isValid() && destination.remoteId().isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("No valid destination specified")); + q->emitResult(); + return; + } + + try { + JobPrivate::sendCommand( + Protocol::LinkItemsCommandPtr::create(action, ProtocolHelper::entitySetToScope(objectsToLink), ProtocolHelper::entityToScope(destination))); + } catch (const std::exception &e) { + q->setError(Job::Unknown); + q->setErrorText(QString::fromUtf8(e.what())); + q->emitResult(); + return; + } + } + + inline bool handleResponse(qint64 tag, const Protocol::CommandPtr &response) + { + auto q = static_cast(q_func()); + if (!response->isResponse() || response->type() != Protocol::Command::LinkItems) { + return q->Job::doHandleResponse(tag, response); + } + + return true; + } + + Item::List objectsToLink; + Collection destination; +}; + +} + diff --git a/src/core/jobs/recursiveitemfetchjob.cpp b/src/core/jobs/recursiveitemfetchjob.cpp new file mode 100644 index 0000000..ef63359 --- /dev/null +++ b/src/core/jobs/recursiveitemfetchjob.cpp @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "recursiveitemfetchjob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN RecursiveItemFetchJob::Private +{ +public: + Private(const Collection &collection, const QStringList &mimeTypes, RecursiveItemFetchJob *parent) + : mParent(parent) + , mCollection(collection) + , mMimeTypes(mimeTypes) + { + } + + void collectionFetchResult(KJob *job) + { + if (job->error()) { + mParent->emitResult(); + return; + } + + const CollectionFetchJob *fetchJob = qobject_cast(job); + + Collection::List collections = fetchJob->collections(); + collections.prepend(mCollection); + + for (const Collection &collection : std::as_const(collections)) { + auto itemFetchJob = new ItemFetchJob(collection, mParent); + itemFetchJob->setFetchScope(mFetchScope); + mParent->connect(itemFetchJob, &KJob::result, mParent, [this](KJob *job) { + itemFetchResult(job); + }); + + mFetchCount++; + } + } + + void itemFetchResult(KJob *job) + { + if (!job->error()) { + const ItemFetchJob *fetchJob = qobject_cast(job); + + if (!mMimeTypes.isEmpty()) { + const Akonadi::Item::List lstItems = fetchJob->items(); + for (const Item &item : lstItems) { + if (mMimeTypes.contains(item.mimeType())) { + mItems << item; + } + } + } else { + mItems << fetchJob->items(); + } + } + + mFetchCount--; + + if (mFetchCount == 0) { + mParent->emitResult(); + } + } + + RecursiveItemFetchJob *const mParent; + const Collection mCollection; + Item::List mItems; + ItemFetchScope mFetchScope; + const QStringList mMimeTypes; + + int mFetchCount = 0; +}; + +RecursiveItemFetchJob::RecursiveItemFetchJob(const Collection &collection, const QStringList &mimeTypes, QObject *parent) + : KJob(parent) + , d(new Private(collection, mimeTypes, this)) +{ +} + +RecursiveItemFetchJob::~RecursiveItemFetchJob() +{ + delete d; +} + +void RecursiveItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope) +{ + d->mFetchScope = fetchScope; +} + +ItemFetchScope &RecursiveItemFetchJob::fetchScope() +{ + return d->mFetchScope; +} + +void RecursiveItemFetchJob::start() +{ + auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Recursive, this); + + if (!d->mMimeTypes.isEmpty()) { + job->fetchScope().setContentMimeTypes(d->mMimeTypes); + } + + connect(job, &CollectionFetchJob::result, this, [this](KJob *job) { + d->collectionFetchResult(job); + }); +} + +Akonadi::Item::List RecursiveItemFetchJob::items() const +{ + return d->mItems; +} + +#include "moc_recursiveitemfetchjob.cpp" diff --git a/src/core/jobs/recursiveitemfetchjob.h b/src/core/jobs/recursiveitemfetchjob.h new file mode 100644 index 0000000..ffe13c5 --- /dev/null +++ b/src/core/jobs/recursiveitemfetchjob.h @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" + +#include + +namespace Akonadi +{ +class Collection; +class ItemFetchScope; + +/** + * @short Job that fetches all items of a collection recursive. + * + * This job takes a collection as argument and returns a list of + * all items that are part of the passed collection and its child + * collections. The items to fetch can be filtered by mime types and + * the parts of the items that shall be fetched can + * be specified via an ItemFetchScope. + * + * Example: + * + * @code + * + * // Assume the following Akonadi storage tree structure: + * // + * // Root Collection + * // | + * // +- Contacts + * // | | + * // | +- Private + * // | | + * // | `- Business + * // | + * // `- Events + * // + * // Collection 'Contacts' has the ID 15, then the following code + * // returns all contact items from 'Contacts', 'Private' and 'Business'. + * + * const Akonadi::Collection contactsCollection( 15 ); + * const QStringList mimeTypes = QStringList() << KContacts::Addressee::mimeType(); + * + * Akonadi::RecursiveItemFetchJob *job = new Akonadi::RecursiveItemFetchJob( contactsCollection, mimeTypes ); + * job->fetchScope().fetchFullPayload(); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*)) ); + * + * job->start(); + * + * ... + * + * MyClass::fetchResult( KJob *job ) + * { + * Akonadi::RecursiveItemFetchJob *fetchJob = qobject_cast( job ); + * const Akonadi::Item::List items = fetchJob->items(); + * // do something with the items + * } + * + * @endcode + * + * @author Tobias Koenig + * @since 4.6 + */ +class AKONADICORE_EXPORT RecursiveItemFetchJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new recursive item fetch job. + * + * @param collection The collection that shall be fetched recursive. + * @param mimeTypes The list of mime types that will be used for filtering. + * @param parent The parent object. + */ + explicit RecursiveItemFetchJob(const Akonadi::Collection &collection, const QStringList &mimeTypes, QObject *parent = nullptr); + + /** + * Destroys the recursive item fetch job. + */ + ~RecursiveItemFetchJob() override; + + /** + * Sets the item fetch scope. + * + * The ItemFetchScope controls how much of an item's data is fetched + * from the server, e.g. whether to fetch the full item payload or + * only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see fetchScope() + */ + void setFetchScope(const Akonadi::ItemFetchScope &fetchScope); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setFetchScope() for replacing the current item fetch scope + */ + Akonadi::ItemFetchScope &fetchScope(); + + /** + * Returns the list of fetched items. + */ + Q_REQUIRED_RESULT Akonadi::Item::List items() const; + + /** + * Starts the recursive item fetch job. + */ + void start() override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/jobs/relationcreatejob.cpp b/src/core/jobs/relationcreatejob.cpp new file mode 100644 index 0000000..c59e3e3 --- /dev/null +++ b/src/core/jobs/relationcreatejob.cpp @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "relationcreatejob.h" +#include "akonadicore_debug.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" +#include "relation.h" +#include + +using namespace Akonadi; + +class Akonadi::RelationCreateJobPrivate : public JobPrivate +{ +public: + explicit RelationCreateJobPrivate(RelationCreateJob *parent) + : JobPrivate(parent) + { + } + + Relation mRelation; +}; + +RelationCreateJob::RelationCreateJob(const Akonadi::Relation &relation, QObject *parent) + : Job(new RelationCreateJobPrivate(this), parent) +{ + Q_D(RelationCreateJob); + d->mRelation = relation; +} + +void RelationCreateJob::doStart() +{ + Q_D(RelationCreateJob); + + if (!d->mRelation.isValid()) { + qCWarning(AKONADICORE_LOG) << "The relation is invalid"; + setError(Job::Unknown); + setErrorText(i18n("Failed to create relation.")); + emitResult(); + return; + } + + d->sendCommand( + Protocol::ModifyRelationCommandPtr::create(d->mRelation.left().id(), d->mRelation.right().id(), d->mRelation.type(), d->mRelation.remoteId())); +} + +bool RelationCreateJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::ModifyRelation) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Relation RelationCreateJob::relation() const +{ + Q_D(const RelationCreateJob); + return d->mRelation; +} diff --git a/src/core/jobs/relationcreatejob.h b/src/core/jobs/relationcreatejob.h new file mode 100644 index 0000000..3f758ee --- /dev/null +++ b/src/core/jobs/relationcreatejob.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "job.h" + +namespace Akonadi +{ +class Relation; +class RelationCreateJobPrivate; + +/** + * @short Job that creates a new relation in the Akonadi storage. + * @since 4.15 + */ +class AKONADICORE_EXPORT RelationCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new relation create job. + * + * @param relation The relation to create. + * @param parent The parent object. + */ + explicit RelationCreateJob(const Relation &relation, QObject *parent = nullptr); + + /** + * Returns the relation. + */ + Q_REQUIRED_RESULT Relation relation() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(RelationCreateJob) +}; + +} + diff --git a/src/core/jobs/relationdeletejob.cpp b/src/core/jobs/relationdeletejob.cpp new file mode 100644 index 0000000..dbadb5a --- /dev/null +++ b/src/core/jobs/relationdeletejob.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "relationdeletejob.h" +#include "akonadicore_debug.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" +#include "relation.h" +#include + +using namespace Akonadi; + +class Akonadi::RelationDeleteJobPrivate : public JobPrivate +{ +public: + explicit RelationDeleteJobPrivate(RelationDeleteJob *parent) + : JobPrivate(parent) + { + } + + Relation mRelation; +}; + +RelationDeleteJob::RelationDeleteJob(const Akonadi::Relation &relation, QObject *parent) + : Job(new RelationDeleteJobPrivate(this), parent) +{ + Q_D(RelationDeleteJob); + d->mRelation = relation; +} + +void RelationDeleteJob::doStart() +{ + Q_D(RelationDeleteJob); + + if (!d->mRelation.isValid()) { + qCWarning(AKONADICORE_LOG) << "The relation is invalid"; + setError(Job::Unknown); + setErrorText(i18n("Failed to create relation.")); + emitResult(); + return; + } + + d->sendCommand(Protocol::RemoveRelationsCommandPtr::create(d->mRelation.left().id(), d->mRelation.right().id(), d->mRelation.type())); +} + +bool RelationDeleteJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::RemoveRelations) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Relation RelationDeleteJob::relation() const +{ + Q_D(const RelationDeleteJob); + return d->mRelation; +} diff --git a/src/core/jobs/relationdeletejob.h b/src/core/jobs/relationdeletejob.h new file mode 100644 index 0000000..b7c6297 --- /dev/null +++ b/src/core/jobs/relationdeletejob.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "job.h" + +namespace Akonadi +{ +class Relation; +class RelationDeleteJobPrivate; + +/** + * @short Job that deletes a relation in the Akonadi storage. + * @since 4.15 + */ +class AKONADICORE_EXPORT RelationDeleteJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new relation delete job. + * + * @param relation The relation to delete. + * @param parent The parent object. + */ + explicit RelationDeleteJob(const Relation &relation, QObject *parent = nullptr); + + /** + * Returns the relation. + */ + Q_REQUIRED_RESULT Relation relation() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(RelationDeleteJob) +}; + +} + diff --git a/src/core/jobs/relationfetchjob.cpp b/src/core/jobs/relationfetchjob.cpp new file mode 100644 index 0000000..5dd5596 --- /dev/null +++ b/src/core/jobs/relationfetchjob.cpp @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "relationfetchjob.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "protocolhelper_p.h" +#include + +using namespace Akonadi; + +class Akonadi::RelationFetchJobPrivate : public JobPrivate +{ +public: + explicit RelationFetchJobPrivate(RelationFetchJob *parent) + : JobPrivate(parent) + { + mEmitTimer.setSingleShot(true); + mEmitTimer.setInterval(std::chrono::milliseconds{100}); + } + + void init() + { + QObject::connect(&mEmitTimer, &QTimer::timeout, q_ptr, [this]() { + timeout(); + }); + } + + void aboutToFinish() override + { + timeout(); + } + + void timeout() + { + Q_Q(RelationFetchJob); + mEmitTimer.stop(); // in case we are called by result() + if (!mPendingRelations.isEmpty()) { + if (!q->error()) { + Q_EMIT q->relationsReceived(mPendingRelations); + } + mPendingRelations.clear(); + } + } + + Q_DECLARE_PUBLIC(RelationFetchJob) + + Relation::List mResultRelations; + Relation::List mPendingRelations; // relation pending for emitting itemsReceived() + QTimer mEmitTimer; + QVector mTypes; + QString mResource; + Relation mRequestedRelation; +}; + +RelationFetchJob::RelationFetchJob(const Relation &relation, QObject *parent) + : Job(new RelationFetchJobPrivate(this), parent) +{ + Q_D(RelationFetchJob); + d->init(); + d->mRequestedRelation = relation; +} + +RelationFetchJob::RelationFetchJob(const QVector &types, QObject *parent) + : Job(new RelationFetchJobPrivate(this), parent) +{ + Q_D(RelationFetchJob); + d->init(); + d->mTypes = types; +} + +void RelationFetchJob::doStart() +{ + Q_D(RelationFetchJob); + + d->sendCommand(Protocol::FetchRelationsCommandPtr::create( + d->mRequestedRelation.left().id(), + d->mRequestedRelation.right().id(), + (d->mTypes.isEmpty() && !d->mRequestedRelation.type().isEmpty()) ? QVector() << d->mRequestedRelation.type() : d->mTypes, + d->mResource)); +} + +bool RelationFetchJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(RelationFetchJob); + + if (!response->isResponse() || response->type() != Protocol::Command::FetchRelations) { + return Job::doHandleResponse(tag, response); + } + + const Relation rel = ProtocolHelper::parseRelationFetchResult(Protocol::cmdCast(response)); + // Invalid response means there will be no more responses + if (!rel.isValid()) { + return true; + } + + d->mResultRelations.append(rel); + d->mPendingRelations.append(rel); + if (!d->mEmitTimer.isActive()) { + d->mEmitTimer.start(); + } + return false; +} + +Relation::List RelationFetchJob::relations() const +{ + Q_D(const RelationFetchJob); + return d->mResultRelations; +} + +void RelationFetchJob::setResource(const QString &identifier) +{ + Q_D(RelationFetchJob); + d->mResource = identifier; +} + +#include "moc_relationfetchjob.cpp" diff --git a/src/core/jobs/relationfetchjob.h b/src/core/jobs/relationfetchjob.h new file mode 100644 index 0000000..d806711 --- /dev/null +++ b/src/core/jobs/relationfetchjob.h @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "job.h" +#include "relation.h" + +namespace Akonadi +{ +class Relation; +class RelationFetchJobPrivate; + +/** + * @short Job that to fetch relations from Akonadi storage. + * @since 4.15 + */ +class AKONADICORE_EXPORT RelationFetchJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new relation fetch job. + * + * @param relation The relation to fetch. + * @param parent The parent object. + */ + explicit RelationFetchJob(const Relation &relation, QObject *parent = nullptr); + + explicit RelationFetchJob(const QVector &types, QObject *parent = nullptr); + + void setResource(const QString &identifier); + + /** + * Returns the relations. + */ + Q_REQUIRED_RESULT Relation::List relations() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new relations have been fetched completely. + * + * @param relations The fetched relations. + */ + void relationsReceived(const Akonadi::Relation::List &relations); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(RelationFetchJob) +}; + +} + diff --git a/src/core/jobs/resourceselectjob.cpp b/src/core/jobs/resourceselectjob.cpp new file mode 100644 index 0000000..846495d --- /dev/null +++ b/src/core/jobs/resourceselectjob.cpp @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resourceselectjob_p.h" + +#include "job_p.h" +#include "private/imapparser_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::ResourceSelectJobPrivate : public JobPrivate +{ +public: + explicit ResourceSelectJobPrivate(ResourceSelectJob *parent) + : JobPrivate(parent) + { + } + + QString resourceId; + QString jobDebuggingString() const override; +}; + +QString Akonadi::ResourceSelectJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Select Resource %1").arg(resourceId); +} + +ResourceSelectJob::ResourceSelectJob(const QString &identifier, QObject *parent) + : Job(new ResourceSelectJobPrivate(this), parent) +{ + Q_D(ResourceSelectJob); + d->resourceId = identifier; +} + +void ResourceSelectJob::doStart() +{ + Q_D(ResourceSelectJob); + + d->sendCommand(Protocol::SelectResourceCommandPtr::create(d->resourceId)); +} + +bool ResourceSelectJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::SelectResource) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +#include "moc_resourceselectjob_p.cpp" diff --git a/src/core/jobs/resourceselectjob_p.h b/src/core/jobs/resourceselectjob_p.h new file mode 100644 index 0000000..75baa2e --- /dev/null +++ b/src/core/jobs/resourceselectjob_p.h @@ -0,0 +1,92 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class ResourceSelectJobPrivate; + +/** + * @internal + * + * @short Job that selects a resource context for remote identifier based operations. + * + * This job selects a resource context that is used whenever remote identifier + * based operations ( e.g. fetch items or collections by remote identifier ) are + * executed. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * // Find out the akonadi id of the item with the remote id 'd1627013c6d5a2e7bb58c12560c27047' + * // that is stored in the resource with identifier 'my_mail_resource' + * + * Session *m_resourceSession = new Session( "resourceSession" ); + * + * ResourceSelectJob *job = new ResourceSelectJob( "my_mail_resource", resourceSession ); + * + * connect( job, SIGNAL(result(KJob*)), SLOT(resourceSelected(KJob*)) ); + * ... + * + * void resourceSelected( KJob *job ) + * { + * if ( job->error() ) + * return; + * + * Item item; + * item.setRemoteIdentifier( "d1627013c6d5a2e7bb58c12560c27047" ); + * + * ItemFetchJob *fetchJob = new ItemFetchJob( item, m_resourceSession ); + * connect( fetchJob, SIGNAL(result(KJob*)), SLOT(itemFetched(KJob*)) ); + * } + * + * void itemFetched( KJob *job ) + * { + * if ( job->error() ) + * return; + * + * const Item item = job->items().at(0); + * + * qDebug() << "Remote id" << item.remoteId() << "has akonadi id" << item.id(); + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT ResourceSelectJob : public Job +{ + Q_OBJECT +public: + /** + * Selects the specified resource for all following remote identifier + * based operations in the same session. + * + * @param identifier The resource identifier, or any empty string to reset + * the selection. + * @param parent The parent object. + */ + explicit ResourceSelectJob(const QString &identifier, QObject *parent = nullptr); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(ResourceSelectJob) + /// @endcond +}; + +} + diff --git a/src/core/jobs/resourcesynchronizationjob.cpp b/src/core/jobs/resourcesynchronizationjob.cpp new file mode 100644 index 0000000..72db7ce --- /dev/null +++ b/src/core/jobs/resourcesynchronizationjob.cpp @@ -0,0 +1,165 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "resourcesynchronizationjob.h" +#include "agentinstance.h" +#include "agentmanager.h" +#include "akonadicore_debug.h" +#include "kjobprivatebase_p.h" +#include "resourceinterface.h" +#include "servermanager.h" +#include + +#include + +#include +#include + +namespace Akonadi +{ +class ResourceSynchronizationJobPrivate : public KJobPrivateBase +{ + Q_OBJECT + +public: + explicit ResourceSynchronizationJobPrivate(ResourceSynchronizationJob *parent) + : q(parent) + { + connect(&safetyTimer, &QTimer::timeout, this, &ResourceSynchronizationJobPrivate::slotTimeout); + safetyTimer.setInterval(std::chrono::seconds{30}); + safetyTimer.setSingleShot(true); + } + + void doStart() override; + + ResourceSynchronizationJob *const q; + AgentInstance instance; + std::unique_ptr interface; + QTimer safetyTimer; + int timeoutCount = 60; + bool collectionTreeOnly = false; + int timeoutCountLimit = 0; + + void slotSynchronized(); + void slotTimeout(); +}; + +ResourceSynchronizationJob::ResourceSynchronizationJob(const AgentInstance &instance, QObject *parent) + : KJob(parent) + , d(new ResourceSynchronizationJobPrivate(this)) +{ + d->instance = instance; +} + +ResourceSynchronizationJob::~ResourceSynchronizationJob() +{ + delete d; +} + +void ResourceSynchronizationJob::start() +{ + d->start(); +} + +void ResourceSynchronizationJob::setTimeoutCountLimit(int count) +{ + d->timeoutCountLimit = count; +} + +int ResourceSynchronizationJob::timeoutCountLimit() const +{ + return d->timeoutCountLimit; +} + +bool ResourceSynchronizationJob::collectionTreeOnly() const +{ + return d->collectionTreeOnly; +} + +void ResourceSynchronizationJob::setCollectionTreeOnly(bool b) +{ + d->collectionTreeOnly = b; +} + +void ResourceSynchronizationJobPrivate::doStart() +{ + if (!instance.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Invalid resource instance.")); + q->emitResult(); + return; + } + + using ResourceIface = org::freedesktop::Akonadi::Resource; + interface = std::make_unique(ServerManager::agentServiceName(ServerManager::Resource, instance.identifier()), + QStringLiteral("/"), + QDBusConnection::sessionBus()); + if (collectionTreeOnly) { + connect(interface.get(), &ResourceIface::collectionTreeSynchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); + } else { + connect(interface.get(), &ResourceIface::synchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); + } + + if (interface->isValid()) { + if (collectionTreeOnly) { + instance.synchronizeCollectionTree(); + } else { + instance.synchronize(); + } + + safetyTimer.start(); + } else { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to obtain D-Bus interface for resource '%1'", instance.identifier())); + q->emitResult(); + return; + } +} + +void ResourceSynchronizationJobPrivate::slotSynchronized() +{ + using ResourceIface = org::freedesktop::Akonadi::Resource; + if (collectionTreeOnly) { + disconnect(interface.get(), &ResourceIface::collectionTreeSynchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); + } else { + disconnect(interface.get(), &ResourceIface::synchronized, this, &ResourceSynchronizationJobPrivate::slotSynchronized); + } + safetyTimer.stop(); + q->emitResult(); +} + +void ResourceSynchronizationJobPrivate::slotTimeout() +{ + instance = AgentManager::self()->instance(instance.identifier()); + timeoutCount++; + + if (timeoutCount > timeoutCountLimit) { + safetyTimer.stop(); + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Resource synchronization timed out.")); + q->emitResult(); + return; + } + + if (instance.status() == AgentInstance::Idle) { + // try again, we might have lost the synchronized()/synchronizedCollectionTree() signal + qCDebug(AKONADICORE_LOG) << "trying again to sync resource" << instance.identifier(); + if (collectionTreeOnly) { + instance.synchronizeCollectionTree(); + } else { + instance.synchronize(); + } + } +} + +AgentInstance ResourceSynchronizationJob::resource() const +{ + return d->instance; +} + +} // namespace Akonadi + +#include "resourcesynchronizationjob.moc" diff --git a/src/core/jobs/resourcesynchronizationjob.h b/src/core/jobs/resourcesynchronizationjob.h new file mode 100644 index 0000000..89e7890 --- /dev/null +++ b/src/core/jobs/resourcesynchronizationjob.h @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +class AgentInstance; +class ResourceSynchronizationJobPrivate; + +/** + * @short Job that synchronizes a resource. + * + * This job will trigger a resource to synchronize the backend it is + * responsible for (e.g. a local file or a groupware server) with the + * Akonadi storage. + * + * If you only want to trigger the synchronization without being + * interested in the result, using Akonadi::AgentInstance::synchronize() is enough. + * If you want to wait until it's finished, use this class. + * + * Example: + * + * @code + * using namespace Akonadi; + * + * const AgentInstance resource = AgentManager::self()->instance( "myresourceidentifier" ); + * + * ResourceSynchronizationJob *job = new ResourceSynchronizationJob( resource ); + * connect( job, SIGNAL(result(KJob*)), SLOT(synchronizationFinished(KJob*)) ); + * job->start(); + * + * @endcode + * + * @note This is a KJob, not an Akonadi::Job, so it won't auto-start! + * + * @author Volker Krause + * @since 4.4 + */ +class AKONADICORE_EXPORT ResourceSynchronizationJob : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new synchronization job for the given resource. + * + * @param instance The resource instance to synchronize. + */ + explicit ResourceSynchronizationJob(const AgentInstance &instance, QObject *parent = nullptr); + + /** + * Destroys the synchronization job. + */ + ~ResourceSynchronizationJob() override; + + /** + * Returns whether a full synchronization will be done, or just the collection tree (without items). + * The default is @c false, i.e. a full sync will be requested. + * + * @since 4.8 + */ + Q_REQUIRED_RESULT bool collectionTreeOnly() const; + + /** + * Sets the collectionTreeOnly property. + * + * @param collectionTreeOnly If set, only the collection tree will be synchronized. + * @since 4.8 + */ + void setCollectionTreeOnly(bool collectionTreeOnly); + + /** + * Returns the resource that has been synchronized. + */ + Q_REQUIRED_RESULT AgentInstance resource() const; + + /* reimpl */ + void start() override; + + /* + * @since 5.1 + */ + void setTimeoutCountLimit(int count); + Q_REQUIRED_RESULT int timeoutCountLimit() const; + +private: + /// @cond PRIVATE + ResourceSynchronizationJobPrivate *const d; + friend class ResourceSynchronizationJobPrivate; + /// @endcond +}; + +} + diff --git a/src/core/jobs/searchcreatejob.cpp b/src/core/jobs/searchcreatejob.cpp new file mode 100644 index 0000000..0175794 --- /dev/null +++ b/src/core/jobs/searchcreatejob.cpp @@ -0,0 +1,152 @@ + +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "searchcreatejob.h" + +#include "job_p.h" +#include "protocolhelper_p.h" +#include "searchquery.h" + +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::SearchCreateJobPrivate : public JobPrivate +{ +public: + SearchCreateJobPrivate(const QString &name, const SearchQuery &query, SearchCreateJob *parent) + : JobPrivate(parent) + , mName(name) + , mQuery(query) + { + } + + const QString mName; + const SearchQuery mQuery; + QStringList mMimeTypes; + QVector mCollections; + bool mRecursive = false; + bool mRemote = false; + Collection mCreatedCollection; + + // JobPrivate interface +public: + QString jobDebuggingString() const override; +}; + +QString Akonadi::SearchCreateJobPrivate::jobDebuggingString() const +{ + QString str = QStringLiteral("Name :%1 ").arg(mName); + if (mRecursive) { + str += QStringLiteral("Recursive "); + } + if (mRemote) { + str += QStringLiteral("Remote"); + } + return str; +} + +SearchCreateJob::SearchCreateJob(const QString &name, const SearchQuery &searchQuery, QObject *parent) + : Job(new SearchCreateJobPrivate(name, searchQuery, this), parent) +{ +} + +SearchCreateJob::~SearchCreateJob() +{ +} + +void SearchCreateJob::setSearchCollections(const QVector &collections) +{ + Q_D(SearchCreateJob); + + d->mCollections = collections; +} + +QVector SearchCreateJob::searchCollections() const +{ + return d_func()->mCollections; +} + +void SearchCreateJob::setSearchMimeTypes(const QStringList &mimeTypes) +{ + Q_D(SearchCreateJob); + + d->mMimeTypes = mimeTypes; +} + +QStringList SearchCreateJob::searchMimeTypes() const +{ + return d_func()->mMimeTypes; +} + +void SearchCreateJob::setRecursive(bool recursive) +{ + Q_D(SearchCreateJob); + + d->mRecursive = recursive; +} + +bool SearchCreateJob::isRecursive() const +{ + return d_func()->mRecursive; +} + +void SearchCreateJob::setRemoteSearchEnabled(bool enabled) +{ + Q_D(SearchCreateJob); + + d->mRemote = enabled; +} + +bool SearchCreateJob::isRemoteSearchEnabled() const +{ + return d_func()->mRemote; +} + +void SearchCreateJob::doStart() +{ + Q_D(SearchCreateJob); + + auto cmd = Protocol::StoreSearchCommandPtr::create(); + cmd->setName(d->mName); + cmd->setQuery(QString::fromUtf8(d->mQuery.toJSON())); + cmd->setMimeTypes(d->mMimeTypes); + cmd->setRecursive(d->mRecursive); + cmd->setRemote(d->mRemote); + if (!d->mCollections.isEmpty()) { + QVector ids; + ids.reserve(d->mCollections.size()); + for (const Collection &col : std::as_const(d->mCollections)) { + ids << col.id(); + } + cmd->setQueryCollections(ids); + } + + d->sendCommand(cmd); +} + +Akonadi::Collection SearchCreateJob::createdCollection() const +{ + Q_D(const SearchCreateJob); + return d->mCreatedCollection; +} + +bool SearchCreateJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(SearchCreateJob); + if (response->isResponse() && response->type() == Protocol::Command::FetchCollections) { + d->mCreatedCollection = ProtocolHelper::parseCollection(Protocol::cmdCast(response)); + return false; + } + + if (response->isResponse() && response->type() == Protocol::Command::StoreSearch) { + return true; + } + + return Job::doHandleResponse(tag, response); +} diff --git a/src/core/jobs/searchcreatejob.h b/src/core/jobs/searchcreatejob.h new file mode 100644 index 0000000..8b26030 --- /dev/null +++ b/src/core/jobs/searchcreatejob.h @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class SearchQuery; +class SearchCreateJobPrivate; + +/** + * @short Job that creates a virtual/search collection in the Akonadi storage. + * + * This job creates so called virtual or search collections, which don't contain + * real data, but references to items that match a given search query. + * + * @code + * + * const QString name = "My search folder"; + * const QString query = "..."; + * + * Akonadi::SearchCreateJob *job = new Akonadi::SearchCreateJob( name, query ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << "Error occurred"; + * return; + * } + * + * qDebug() << "Created search folder successfully"; + * const Collection searchCollection = job->createdCollection(); + * ... + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT SearchCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a search create job + * + * @param name The name of the search collection + * @param searchQuery The search query + * @param parent The parent object + * @since 4.13 + */ + SearchCreateJob(const QString &name, const SearchQuery &searchQuery, QObject *parent = nullptr); + + /** + * Sets list of mime types of items that search results can contain + * + * @param mimeTypes Mime types of items to include in search + * @since 4.13 + */ + void setSearchMimeTypes(const QStringList &mimeTypes); + + /** + * Returns list of mime types that search results can contain + * + * @since 4.13 + */ + Q_REQUIRED_RESULT QStringList searchMimeTypes() const; + + /** + * Sets list of collections to search in. + * + * When an empty list is set (default value), the search will contain + * results from all collections that contain given mime types. + * + * @param collections Collections to search in, or an empty list to search all + * @since 4.13 + */ + void setSearchCollections(const QVector &collections); + + /** + * Returns list of collections to search in + * + * @since 4.13 + */ + Q_REQUIRED_RESULT QVector searchCollections() const; + + /** + * Sets whether resources should be queried too. + * + * When set to true, Akonadi will search local indexed items and will also + * query resources that support server-side search, to forward the query + * to remote storage (for example using SEARCH feature on IMAP servers) and + * merge their results with results from local index. + * + * This is useful especially when searching resources, that don't fetch full + * payload by default, for example the IMAP resource, which only fetches headers + * by default and the body is fetched on demand, which means that emails that + * were not yet fully fetched cannot be indexed in local index, and thus cannot + * be searched. With remote search, even those emails can be included in search + * results. + * + * This feature is disabled by default. + * + * @param enabled Whether remote search is enabled + * @since 4.13 + */ + void setRemoteSearchEnabled(bool enabled); + + /** + * Returns whether remote search is enabled. + * + * @since 4.13 + */ + Q_REQUIRED_RESULT bool isRemoteSearchEnabled() const; + + /** + * Sets whether the search should recurse into collections + * + * When set to true, all child collections of the specific collections will + * be search recursively. + * + * @param recursive Whether to search recursively + * @since 4.13 + */ + void setRecursive(bool recursive); + + /** + * Returns whether the search is recursive + * + * @since 4.13 + */ + Q_REQUIRED_RESULT bool isRecursive() const; + + /** + * Destroys the search create job. + */ + ~SearchCreateJob() override; + + /** + * Returns the newly created search collection once the job finished successfully. Returns an invalid + * collection if the job has not yet finished or failed. + * + * @since 4.4 + */ + Q_REQUIRED_RESULT Collection createdCollection() const; + +protected: + /** + * Reimplemented from Akonadi::Job + */ + void doStart() override; + + /** + * Reimplemented from Akonadi::Job + */ + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(SearchCreateJob) +}; + +} + diff --git a/src/core/jobs/searchresultjob.cpp b/src/core/jobs/searchresultjob.cpp new file mode 100644 index 0000000..821f86f --- /dev/null +++ b/src/core/jobs/searchresultjob.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "job_p.h" +#include "searchresultjob_p.h" + +#include "private/protocol_p.h" +#include "protocolhelper_p.h" + +namespace Akonadi +{ +class SearchResultJobPrivate : public Akonadi::JobPrivate +{ +public: + explicit SearchResultJobPrivate(SearchResultJob *parent); + + QVector rid; + QByteArray searchId; + Collection collection; + ImapSet uid; + + // JobPrivate interface +public: + QString jobDebuggingString() const override; +}; + +QString SearchResultJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Collection id: %1 Search id: %2").arg(collection.id()).arg(QString::fromLatin1(searchId)); +} + +SearchResultJobPrivate::SearchResultJobPrivate(SearchResultJob *parent) + : JobPrivate(parent) +{ +} + +} // namespace Akonadi + +using namespace Akonadi; + +SearchResultJob::SearchResultJob(const QByteArray &searchId, const Collection &collection, QObject *parent) + : Job(new SearchResultJobPrivate(this), parent) +{ + Q_D(SearchResultJob); + Q_ASSERT(collection.isValid()); + + d->searchId = searchId; + d->collection = collection; +} + +SearchResultJob::~SearchResultJob() +{ +} + +void SearchResultJob::setSearchId(const QByteArray &searchId) +{ + Q_D(SearchResultJob); + d->searchId = searchId; +} + +QByteArray SearchResultJob::searchId() const +{ + return d_func()->searchId; +} + +void SearchResultJob::setResult(const ImapSet &set) +{ + Q_D(SearchResultJob); + d->rid.clear(); + d->uid = set; +} + +void SearchResultJob::setResult(const QVector &ids) +{ + Q_D(SearchResultJob); + d->rid.clear(); + d->uid = ImapSet(); + d->uid.add(ids); +} + +void SearchResultJob::setResult(const QVector &remoteIds) +{ + Q_D(SearchResultJob); + d->uid = ImapSet(); + d->rid = remoteIds; +} + +void SearchResultJob::doStart() +{ + Q_D(SearchResultJob); + + Scope scope; + if (!d->rid.isEmpty()) { + QStringList ridSet; + ridSet.reserve(d->rid.size()); + for (const QByteArray &rid : std::as_const(d->rid)) { + ridSet << QString::fromUtf8(rid); + } + scope.setRidSet(ridSet); + } else { + scope.setUidSet(d->uid); + } + d->sendCommand(Protocol::SearchResultCommandPtr::create(d->searchId, d->collection.id(), scope)); +} + +bool SearchResultJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::SearchResult) { + return Job::doHandleResponse(tag, response); + } + + return true; +} diff --git a/src/core/jobs/searchresultjob_p.h b/src/core/jobs/searchresultjob_p.h new file mode 100644 index 0000000..0573d67 --- /dev/null +++ b/src/core/jobs/searchresultjob_p.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class SearchResultJobPrivate; +class ImapSet; +class Collection; + +class AKONADICORE_EXPORT SearchResultJob : public Akonadi::Job +{ + Q_OBJECT +public: + explicit SearchResultJob(const QByteArray &searchId, const Collection &collection, QObject *parent = nullptr); + ~SearchResultJob() override; + + void setSearchId(const QByteArray &searchId); + Q_REQUIRED_RESULT QByteArray searchId() const; + + void setResult(const ImapSet &set); + void setResult(const QVector &remoteIds); + void setResult(const QVector &ids); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(SearchResultJob) +}; +} + diff --git a/src/core/jobs/specialcollectionsdiscoveryjob.cpp b/src/core/jobs/specialcollectionsdiscoveryjob.cpp new file mode 100644 index 0000000..f3f5ee6 --- /dev/null +++ b/src/core/jobs/specialcollectionsdiscoveryjob.cpp @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2013 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "specialcollectionsdiscoveryjob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "specialcollectionattribute.h" +#include + +#include "akonadicore_debug.h" + +using namespace Akonadi; + +/** + @internal +*/ +class Akonadi::SpecialCollectionsDiscoveryJobPrivate +{ +public: + SpecialCollectionsDiscoveryJobPrivate(SpecialCollections *collections, const QStringList &mimeTypes) + : mSpecialCollections(collections) + , mMimeTypes(mimeTypes) + { + } + + SpecialCollections *const mSpecialCollections; + const QStringList mMimeTypes; +}; + +Akonadi::SpecialCollectionsDiscoveryJob::SpecialCollectionsDiscoveryJob(SpecialCollections *collections, const QStringList &mimeTypes, QObject *parent) + : KCompositeJob(parent) + , d(new SpecialCollectionsDiscoveryJobPrivate(collections, mimeTypes)) +{ +} + +Akonadi::SpecialCollectionsDiscoveryJob::~SpecialCollectionsDiscoveryJob() +{ + delete d; +} + +void Akonadi::SpecialCollectionsDiscoveryJob::start() +{ + auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + job->fetchScope().setContentMimeTypes(d->mMimeTypes); + addSubjob(job); +} + +void Akonadi::SpecialCollectionsDiscoveryJob::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + auto fetchJob = qobject_cast(job); + const Akonadi::Collection::List lstCollections = fetchJob->collections(); + for (const Akonadi::Collection &collection : lstCollections) { + if (collection.hasAttribute()) { + d->mSpecialCollections->registerCollection(collection.attribute()->collectionType(), collection); + } + } + emitResult(); +} diff --git a/src/core/jobs/specialcollectionsdiscoveryjob.h b/src/core/jobs/specialcollectionsdiscoveryjob.h new file mode 100644 index 0000000..e6458db --- /dev/null +++ b/src/core/jobs/specialcollectionsdiscoveryjob.h @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2013 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "specialcollections.h" +#include + +namespace Akonadi +{ +class SpecialCollectionsDiscoveryJobPrivate; + +/** + * @short A job to discover all SpecialCollections. + * + * The collections get registered into SpecialCollections. + * + * This class is not meant to be used directly but as a base class for type + * specific special collection request jobs. + * + * @author David Faure + * @since 4.11 + */ +class AKONADICORE_EXPORT SpecialCollectionsDiscoveryJob : public KCompositeJob +{ + Q_OBJECT + +public: + /** + * Destroys the special collections request job. + */ + ~SpecialCollectionsDiscoveryJob() override; + + void start() override; + +protected: + /** + * Creates a new special collections request job. + * + * @param collections The SpecialCollections object that shall be used. + * @param parent The parent object. + */ + explicit SpecialCollectionsDiscoveryJob(SpecialCollections *collections, const QStringList &mimeTypes, QObject *parent = nullptr); + + /* reimpl */ + void slotResult(KJob *job) override; + +private: + /// @cond PRIVATE + SpecialCollectionsDiscoveryJobPrivate *const d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/jobs/specialcollectionshelperjobs.cpp b/src/core/jobs/specialcollectionshelperjobs.cpp new file mode 100644 index 0000000..f8a30e7 --- /dev/null +++ b/src/core/jobs/specialcollectionshelperjobs.cpp @@ -0,0 +1,652 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "specialcollectionshelperjobs_p.h" + +#include "servermanager.h" +#include "specialcollectionattribute.h" +#include "specialcollections.h" +#include + +#include "agentinstance.h" +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "entitydisplayattribute.h" +#include "resourcesynchronizationjob.h" + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#define LOCK_WAIT_TIMEOUT_SECONDS 30 + +using namespace Akonadi; + +// convenient methods to get/set the default resource id +static void setDefaultResourceId(KCoreConfigSkeleton *settings, const QString &value) +{ + KConfigSkeletonItem *item = settings->findItem(QStringLiteral("DefaultResourceId")); + Q_ASSERT(item); + item->setProperty(value); +} + +static QString defaultResourceId(KCoreConfigSkeleton *settings) +{ + const KConfigSkeletonItem *item = settings->findItem(QStringLiteral("DefaultResourceId")); + Q_ASSERT(item); + return item->property().toString(); +} + +static QString dbusServiceName() +{ + QString service = QStringLiteral("org.kde.pim.SpecialCollections"); + if (ServerManager::hasInstanceIdentifier()) { + return service + ServerManager::instanceIdentifier(); + } + return service; +} + +static QVariant::Type argumentType(const QMetaObject *mo, const QString &method) +{ + QMetaMethod m; + for (int i = 0; i < mo->methodCount(); ++i) { + const QString signature = QString::fromLatin1(mo->method(i).methodSignature()); + if (signature.startsWith(method)) { + m = mo->method(i); + } + } + + if (m.methodSignature().isEmpty()) { + return QVariant::Invalid; + } + + const QList argTypes = m.parameterTypes(); + if (argTypes.count() != 1) { + return QVariant::Invalid; + } + + return QVariant::nameToType(argTypes.first().constData()); +} + +// ===================== ResourceScanJob ============================ + +/** + @internal +*/ +class Q_DECL_HIDDEN Akonadi::ResourceScanJob::Private +{ +public: + Private(KCoreConfigSkeleton *settings, ResourceScanJob *qq); + + void fetchResult(KJob *job); // slot + + ResourceScanJob *const q; + + // Input: + QString mResourceId; + KCoreConfigSkeleton *mSettings = nullptr; + + // Output: + Collection mRootCollection; + Collection::List mSpecialCollections; +}; + +ResourceScanJob::Private::Private(KCoreConfigSkeleton *settings, ResourceScanJob *qq) + : q(qq) + , mSettings(settings) +{ +} + +void ResourceScanJob::Private::fetchResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + return; + } + + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + Q_ASSERT(!mRootCollection.isValid()); + Q_ASSERT(mSpecialCollections.isEmpty()); + const Akonadi::Collection::List lstCols = fetchJob->collections(); + for (const Collection &collection : lstCols) { + if (collection.parentCollection() == Collection::root()) { + if (mRootCollection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Resource has more than one root collection. I don't know what to do."; + } else { + mRootCollection = collection; + } + } + + if (collection.hasAttribute()) { + mSpecialCollections.append(collection); + } + } + + qCDebug(AKONADICORE_LOG) << "Fetched root collection" << mRootCollection.id() << "and" << mSpecialCollections.count() << "local folders" + << "(total" << fetchJob->collections().count() << "collections)."; + + if (!mRootCollection.isValid()) { + q->setError(Unknown); + q->setErrorText(i18n("Could not fetch root collection of resource %1.", mResourceId)); + q->emitResult(); + return; + } + + // We are done! + q->emitResult(); +} + +ResourceScanJob::ResourceScanJob(const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent) + : Job(parent) + , d(new Private(settings, this)) +{ + setResourceId(resourceId); +} + +ResourceScanJob::~ResourceScanJob() +{ + delete d; +} + +QString ResourceScanJob::resourceId() const +{ + return d->mResourceId; +} + +void ResourceScanJob::setResourceId(const QString &resourceId) +{ + d->mResourceId = resourceId; +} + +Akonadi::Collection ResourceScanJob::rootResourceCollection() const +{ + return d->mRootCollection; +} + +Akonadi::Collection::List ResourceScanJob::specialCollections() const +{ + return d->mSpecialCollections; +} + +void ResourceScanJob::doStart() +{ + if (d->mResourceId.isEmpty()) { + if (!qobject_cast(this)) { + qCCritical(AKONADICORE_LOG) << "No resource ID given."; + setError(Job::Unknown); + setErrorText(i18n("No resource ID given.")); + } + emitResult(); + return; + } + + auto fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this); + fetchJob->fetchScope().setResource(d->mResourceId); + fetchJob->fetchScope().setIncludeStatistics(true); + fetchJob->fetchScope().setListFilter(CollectionFetchScope::Display); + connect(fetchJob, &CollectionFetchJob::result, this, [this](KJob *job) { + d->fetchResult(job); + }); +} + +// ===================== DefaultResourceJob ============================ + +/** + @internal +*/ +class Akonadi::DefaultResourceJobPrivate +{ +public: + DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq); + + void tryFetchResource(); + void resourceCreateResult(KJob *job); // slot + void resourceSyncResult(KJob *job); // slot + void collectionFetchResult(KJob *job); // slot + void collectionModifyResult(KJob *job); // slot + + DefaultResourceJob *const q; + KCoreConfigSkeleton *mSettings = nullptr; + QVariantMap mDefaultResourceOptions; + QList mKnownTypes; + QMap mNameForTypeMap; + QMap mIconForTypeMap; + QString mDefaultResourceType; + int mPendingModifyJobs = 0; + bool mResourceWasPreexisting = true; +}; + +DefaultResourceJobPrivate::DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq) + : q(qq) + , mSettings(settings) + , mPendingModifyJobs(0) + , mResourceWasPreexisting(true /* for safety, so as not to accidentally delete data */) +{ +} + +void DefaultResourceJobPrivate::tryFetchResource() +{ + // Get the resourceId from config. Another instance might have changed it in the meantime. + mSettings->load(); + + const QString resourceId = defaultResourceId(mSettings); + + qCDebug(AKONADICORE_LOG) << "Read defaultResourceId" << resourceId << "from config."; + + const AgentInstance resource = AgentManager::self()->instance(resourceId); + if (resource.isValid()) { + // The resource exists; scan it. + mResourceWasPreexisting = true; + qCDebug(AKONADICORE_LOG) << "Found resource" << resourceId; + q->setResourceId(resourceId); + + auto fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q); + fetchJob->fetchScope().setResource(resourceId); + fetchJob->fetchScope().setIncludeStatistics(true); + q->connect(fetchJob, &CollectionFetchJob::result, q, [this](KJob *job) { + collectionFetchResult(job); + }); + } else { + // Try harder: maybe the default resource has been removed and another one added + // without updating the config file, in this case search for a resource + // of the same type and the default name + const AgentInstance::List resources = AgentManager::self()->instances(); + for (const AgentInstance &resource : resources) { + if (resource.type().identifier() == mDefaultResourceType) { + if (resource.name() == mDefaultResourceOptions.value(QStringLiteral("Name")).toString()) { + // found a matching one... + setDefaultResourceId(mSettings, resource.identifier()); + mSettings->save(); + mResourceWasPreexisting = true; + qCDebug(AKONADICORE_LOG) << "Found resource" << resource.identifier(); + q->setResourceId(resource.identifier()); + q->ResourceScanJob::doStart(); + return; + } + } + } + + // Create the resource. + mResourceWasPreexisting = false; + qCDebug(AKONADICORE_LOG) << "Creating maildir resource."; + const AgentType type = AgentManager::self()->type(mDefaultResourceType); + auto job = new AgentInstanceCreateJob(type, q); + QObject::connect(job, &AgentInstanceCreateJob::result, q, [this](KJob *job) { + resourceCreateResult(job); + }); + job->start(); // non-Akonadi::Job + } +} + +void DefaultResourceJobPrivate::resourceCreateResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + // fail( i18n( "Failed to create the default resource (%1).", job->errorString() ) ); + q->setError(job->error()); + q->setErrorText(job->errorText()); + q->emitResult(); + return; + } + + AgentInstance agent; + + // Get the resource instance. + { + auto createJob = qobject_cast(job); + Q_ASSERT(createJob); + agent = createJob->instance(); + setDefaultResourceId(mSettings, agent.identifier()); + qCDebug(AKONADICORE_LOG) << "Created maildir resource with id" << defaultResourceId(mSettings); + } + + const QString defaultId = defaultResourceId(mSettings); + + // Configure the resource. + { + agent.setName(mDefaultResourceOptions.value(QStringLiteral("Name")).toString()); + + const auto service = ServerManager::agentServiceName(ServerManager::Resource, defaultId); + QDBusInterface conf(service, QStringLiteral("/Settings"), QString()); + + if (!conf.isValid()) { + q->setError(-1); + q->setErrorText(i18n("Invalid resource identifier '%1'", defaultId)); + q->emitResult(); + return; + } + + QMap::const_iterator it = mDefaultResourceOptions.cbegin(); + const QMap::const_iterator itEnd = mDefaultResourceOptions.cend(); + for (; it != itEnd; ++it) { + if (it.key() == QLatin1String("Name")) { + continue; + } + + const QString methodName = QStringLiteral("set%1").arg(it.key()); + const QVariant::Type argType = argumentType(conf.metaObject(), methodName); + if (argType == QVariant::Invalid) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Failed to configure default resource via D-Bus.")); + q->emitResult(); + return; + } + + QDBusReply reply = conf.call(methodName, it.value()); + if (!reply.isValid()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Failed to configure default resource via D-Bus.")); + q->emitResult(); + return; + } + } + + conf.call(QStringLiteral("save")); + + agent.reconfigure(); + } + + // Sync the resource. + { + auto syncJob = new ResourceSynchronizationJob(agent, q); + QObject::connect(syncJob, &ResourceSynchronizationJob::result, q, [this](KJob *job) { + resourceSyncResult(job); + }); + syncJob->start(); // non-Akonadi + } +} + +void DefaultResourceJobPrivate::resourceSyncResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + // fail( i18n( "ResourceSynchronizationJob failed (%1).", job->errorString() ) ); + return; + } + + // Fetch the collections of the resource. + qCDebug(AKONADICORE_LOG) << "Fetching maildir collections."; + auto fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q); + fetchJob->fetchScope().setResource(defaultResourceId(mSettings)); + QObject::connect(fetchJob, &CollectionFetchJob::result, q, [this](KJob *job) { + collectionFetchResult(job); + }); +} + +void DefaultResourceJobPrivate::collectionFetchResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + // fail( i18n( "Failed to fetch the root maildir collection (%1).", job->errorString() ) ); + return; + } + + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + const Collection::List collections = fetchJob->collections(); + qCDebug(AKONADICORE_LOG) << "Fetched" << collections.count() << "collections."; + + // Find the root maildir collection. + Collection::List toRecover; + Collection resourceCollection; + for (const Collection &collection : collections) { + if (collection.parentCollection() == Collection::root()) { + resourceCollection = collection; + toRecover.append(collection); + break; + } + } + + if (!resourceCollection.isValid()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Failed to fetch the resource collection.")); + q->emitResult(); + return; + } + + // Find all children of the resource collection. + for (const Collection &collection : std::as_const(collections)) { + if (collection.parentCollection() == resourceCollection) { + toRecover.append(collection); + } + } + + QHash typeForName; + for (const QByteArray &type : std::as_const(mKnownTypes)) { + const QString displayName = mNameForTypeMap.value(type); + typeForName[displayName] = type; + } + + // These collections have been created by the maildir resource, when it + // found the folders on disk. So give them the necessary attributes now. + Q_ASSERT(mPendingModifyJobs == 0); + for (Collection collection : std::as_const(toRecover)) { + if (collection.hasAttribute()) { + continue; + } + + // Find the type for the collection. + const QString name = collection.displayName(); + const QByteArray type = typeForName.value(name); + + if (!type.isEmpty()) { + qCDebug(AKONADICORE_LOG) << "Recovering collection" << name; + setCollectionAttributes(collection, type, mNameForTypeMap, mIconForTypeMap); + + auto modifyJob = new CollectionModifyJob(collection, q); + QObject::connect(modifyJob, &CollectionModifyJob::result, q, [this](KJob *job) { + collectionModifyResult(job); + }); + mPendingModifyJobs++; + } else { + qCDebug(AKONADICORE_LOG) << "Searching for names: " << typeForName.keys(); + qCDebug(AKONADICORE_LOG) << "Unknown collection name" << name << "-- not recovering."; + } + } + + if (mPendingModifyJobs == 0) { + // Scan the resource. + q->setResourceId(defaultResourceId(mSettings)); + q->ResourceScanJob::doStart(); + } +} + +void DefaultResourceJobPrivate::collectionModifyResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + // fail( i18n( "Failed to modify the root maildir collection (%1).", job->errorString() ) ); + return; + } + + Q_ASSERT(mPendingModifyJobs > 0); + mPendingModifyJobs--; + qCDebug(AKONADICORE_LOG) << "pendingModifyJobs now" << mPendingModifyJobs; + if (mPendingModifyJobs == 0) { + // Write the updated config. + qCDebug(AKONADICORE_LOG) << "Writing defaultResourceId" << defaultResourceId(mSettings) << "to config."; + mSettings->save(); + + // Scan the resource. + q->setResourceId(defaultResourceId(mSettings)); + q->ResourceScanJob::doStart(); + } +} + +DefaultResourceJob::DefaultResourceJob(KCoreConfigSkeleton *settings, QObject *parent) + : ResourceScanJob(QString(), settings, parent) + , d(new DefaultResourceJobPrivate(settings, this)) +{ +} + +DefaultResourceJob::~DefaultResourceJob() +{ + delete d; +} + +void DefaultResourceJob::setDefaultResourceType(const QString &type) +{ + d->mDefaultResourceType = type; +} + +void DefaultResourceJob::setDefaultResourceOptions(const QVariantMap &options) +{ + d->mDefaultResourceOptions = options; +} + +void DefaultResourceJob::setTypes(const QList &types) +{ + d->mKnownTypes = types; +} + +void DefaultResourceJob::setNameForTypeMap(const QMap &map) +{ + d->mNameForTypeMap = map; +} + +void DefaultResourceJob::setIconForTypeMap(const QMap &map) +{ + d->mIconForTypeMap = map; +} + +void DefaultResourceJob::doStart() +{ + d->tryFetchResource(); +} + +void DefaultResourceJob::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorText(); + // Do some cleanup. + if (!d->mResourceWasPreexisting) { + // We only removed the resource instance if we have created it. + // Otherwise we might lose the user's data. + const AgentInstance resource = AgentManager::self()->instance(defaultResourceId(d->mSettings)); + qCDebug(AKONADICORE_LOG) << "Removing resource" << resource.identifier(); + AgentManager::self()->removeInstance(resource); + } + } + + Job::slotResult(job); +} + +// ===================== GetLockJob ============================ + +class Q_DECL_HIDDEN Akonadi::GetLockJob::Private +{ +public: + explicit Private(GetLockJob *qq); + + void doStart(); // slot + void timeout(); // slot + + GetLockJob *const q; + QTimer *mSafetyTimer = nullptr; +}; + +GetLockJob::Private::Private(GetLockJob *qq) + : q(qq) + , mSafetyTimer(nullptr) +{ +} + +void GetLockJob::Private::doStart() +{ + // Just doing registerService() and checking its return value is not sufficient, + // since we may *already* own the name, and then registerService() returns true. + + QDBusConnection bus = QDBusConnection::sessionBus(); + const bool alreadyLocked = bus.interface()->isServiceRegistered(dbusServiceName()); + const bool gotIt = bus.registerService(dbusServiceName()); + + if (gotIt && !alreadyLocked) { + // qCDebug(AKONADICORE_LOG) << "Got lock immediately."; + q->emitResult(); + } else { + auto watcher = new QDBusServiceWatcher(dbusServiceName(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration, q); + connect(watcher, &QDBusServiceWatcher::serviceUnregistered, q, [this]() { + if (QDBusConnection::sessionBus().registerService(dbusServiceName())) { + mSafetyTimer->stop(); + q->emitResult(); + } + }); + + mSafetyTimer = new QTimer(q); + mSafetyTimer->setSingleShot(true); + mSafetyTimer->setInterval(LOCK_WAIT_TIMEOUT_SECONDS * 1000); + mSafetyTimer->start(); + connect(mSafetyTimer, &QTimer::timeout, q, [this]() { + timeout(); + }); + } +} + +void GetLockJob::Private::timeout() +{ + qCWarning(AKONADICORE_LOG) << "Timeout trying to get lock. Check who has acquired the name" << dbusServiceName() << "on DBus, using qdbus or qdbusviewer."; + q->setError(Job::Unknown); + q->setErrorText(i18n("Timeout trying to get lock.")); + q->emitResult(); +} + +GetLockJob::GetLockJob(QObject *parent) + : KJob(parent) + , d(new Private(this)) +{ +} + +GetLockJob::~GetLockJob() +{ + delete d; +} + +void GetLockJob::start() +{ + QTimer::singleShot(0, this, [this]() { + d->doStart(); + }); +} + +void Akonadi::setCollectionAttributes(Akonadi::Collection &collection, + const QByteArray &type, + const QMap &nameForType, + const QMap &iconForType) +{ + { + auto attr = new EntityDisplayAttribute; + attr->setIconName(iconForType.value(type)); + attr->setDisplayName(nameForType.value(type)); + collection.addAttribute(attr); + } + + { + auto attr = new SpecialCollectionAttribute; + attr->setCollectionType(type); + collection.addAttribute(attr); + } +} + +bool Akonadi::releaseLock() +{ + return QDBusConnection::sessionBus().unregisterService(dbusServiceName()); +} + +#include "moc_specialcollectionshelperjobs_p.cpp" diff --git a/src/core/jobs/specialcollectionshelperjobs_p.h b/src/core/jobs/specialcollectionshelperjobs_p.h new file mode 100644 index 0000000..dcbd405 --- /dev/null +++ b/src/core/jobs/specialcollectionshelperjobs_p.h @@ -0,0 +1,221 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditests_export.h" +#include "collection.h" +#include "specialcollections.h" +#include "transactionsequence.h" + +#include + +namespace Akonadi +{ +// ===================== ResourceScanJob ============================ + +/** + @internal + Helper job for SpecialCollectionsRequestJob. + + A Job that fetches all the collections of a resource, and returns only + those that have a SpecialCollectionAttribute. + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADI_TESTS_EXPORT ResourceScanJob : public Job +{ + Q_OBJECT + +public: + /** + Creates a new ResourceScanJob. + */ + explicit ResourceScanJob(const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent = nullptr); + + /** + Destroys this ResourceScanJob. + */ + ~ResourceScanJob() override; + + /** + Returns the resource ID of the resource being scanned. + */ + Q_REQUIRED_RESULT QString resourceId() const; + + /** + Sets the resource ID of the resource to scan. + */ + void setResourceId(const QString &resourceId); + + /** + Returns the root collection of the resource being scanned. + This function relies on there being a single top-level collection owned + by this resource. + */ + Q_REQUIRED_RESULT Akonadi::Collection rootResourceCollection() const; + + /** + Returns all the collections of this resource which have a + SpecialCollectionAttribute. These might include the root resource collection. + */ + Q_REQUIRED_RESULT Akonadi::Collection::List specialCollections() const; + +protected: + /* reimpl */ + void doStart() override; + +private: + class Private; + friend class Private; + Private *const d; +}; + +// ===================== DefaultResourceJob ============================ + +class DefaultResourceJobPrivate; + +/** + @internal + Helper job for SpecialCollectionsRequestJob. + + A custom ResourceScanJob for the default local folders resource. This is a + maildir resource stored in ~/.local/share/local-mail. + + This job does two things that a regular ResourceScanJob does not do: + 1) It creates and syncs the resource if it is missing. The resource ID is + stored in a config file named specialcollectionsrc. + 2) If the resource had to be recreated, but the folders existed on disk + before that, it recovers the folders based on name. For instance, it will + give a folder named outbox a SpecialCollectionAttribute of type Outbox. + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADI_TESTS_EXPORT DefaultResourceJob : public ResourceScanJob +{ + Q_OBJECT + +public: + /** + * Creates a new DefaultResourceJob. + */ + explicit DefaultResourceJob(KCoreConfigSkeleton *settings, QObject *parent = nullptr); + + /** + * Destroys the DefaultResourceJob. + */ + ~DefaultResourceJob() override; + + /** + * Sets the @p type of the resource that shall be created if the requested + * special collection does not exist yet. + */ + void setDefaultResourceType(const QString &type); + + /** + * Sets the configuration @p options that shall be applied to the new resource + * that is created if the requested special collection does not exist yet. + */ + void setDefaultResourceOptions(const QVariantMap &options); + + /** + * Sets the list of well known special collection @p types. + */ + void setTypes(const QList &types); + + /** + * Sets the @p map of special collection types to display names. + */ + void setNameForTypeMap(const QMap &map); + + /** + * Sets the @p map of special collection types to icon names. + */ + void setIconForTypeMap(const QMap &map); + +protected: + /* reimpl */ + void doStart() override; + /* reimpl */ + void slotResult(KJob *job) override; + +private: + friend class DefaultResourceJobPrivate; + DefaultResourceJobPrivate *const d; +}; + +// ===================== GetLockJob ============================ + +/** + @internal + Helper job for SpecialCollectionsRequestJob. + + If SpecialCollectionsRequestJob needs to create a collection, it sets a lock so + that other instances do not interfere. This lock is an + org.kde.pim.SpecialCollections name registered on D-Bus. This job is used to get + that lock. + This job will give the lock immediately if possible, or wait up to three + seconds for the lock to be released. If the lock is not released during + that time, this job fails. (This is based on the assumption that + SpecialCollectionsRequestJob operations should not take long.) + + Use the releaseLock() function to release the lock. + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADI_TESTS_EXPORT GetLockJob : public KJob +{ + Q_OBJECT + +public: + /** + Creates a new GetLockJob. + */ + explicit GetLockJob(QObject *parent = nullptr); + + /** + Destroys the GetLockJob. + */ + ~GetLockJob() override; + + /* reimpl */ + void start() override; + +private: + class Private; + friend class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void doStart()) +}; + +// ===================== helper functions ============================ + +/** + * Sets on @p col the required attributes of SpecialCollection type @p type + * These are a SpecialCollectionAttribute and an EntityDisplayAttribute. + * @param col collection + * @param type collection type + * @param nameForType collection name for type + * @param iconForType collection icon for type + */ +void setCollectionAttributes(Akonadi::Collection &col, + const QByteArray &type, + const QMap &nameForType, + const QMap &iconForType); + +/** + Releases the SpecialCollectionsRequestJob lock that was obtained through + GetLockJob. + @return Whether the lock was released successfully. +*/ +bool AKONADI_TESTS_EXPORT releaseLock(); + +} // namespace Akonadi + diff --git a/src/core/jobs/specialcollectionsrequestjob.cpp b/src/core/jobs/specialcollectionsrequestjob.cpp new file mode 100644 index 0000000..06185fe --- /dev/null +++ b/src/core/jobs/specialcollectionsrequestjob.cpp @@ -0,0 +1,347 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "specialcollectionsrequestjob.h" + +#include "specialcollectionattribute.h" +#include "specialcollections_p.h" +#include "specialcollectionshelperjobs_p.h" + +#include "agentmanager.h" +#include "collectioncreatejob.h" +#include "entitydisplayattribute.h" + +#include "akonadicore_debug.h" + +using namespace Akonadi; + +/** + @internal +*/ +class Akonadi::SpecialCollectionsRequestJobPrivate +{ +public: + SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, SpecialCollectionsRequestJob *qq); + + bool isEverythingReady() const; + void lockResult(KJob *job); // slot + void releaseLock(); // slot + void nextResource(); + void resourceScanResult(KJob *job); // slot + void createRequestedFolders(ResourceScanJob *job, QHash &requestedFolders); + void collectionCreateResult(KJob *job); // slot + + SpecialCollectionsRequestJob *const q; + SpecialCollections *mSpecialCollections = nullptr; + int mPendingCreateJobs; + + QByteArray mRequestedType; + AgentInstance mRequestedResource; + + // Input: + QHash mDefaultFolders; + bool mRequestingDefaultFolders; + QHash> mFoldersForResource; + QString mDefaultResourceType; + QVariantMap mDefaultResourceOptions; + QList mKnownTypes; + QMap mNameForTypeMap; + QMap mIconForTypeMap; + + // Output: + QStringList mToForget; + QVector> mToRegister; +}; + +SpecialCollectionsRequestJobPrivate::SpecialCollectionsRequestJobPrivate(SpecialCollections *collections, SpecialCollectionsRequestJob *qq) + : q(qq) + , mSpecialCollections(collections) + , mPendingCreateJobs(0) + , mRequestingDefaultFolders(false) +{ +} + +bool SpecialCollectionsRequestJobPrivate::isEverythingReady() const +{ + // check if all requested folders are known already + if (mRequestingDefaultFolders) { + for (auto it = mDefaultFolders.cbegin(), end = mDefaultFolders.cend(); it != end; ++it) { + if (it.value() && !mSpecialCollections->hasDefaultCollection(it.key())) { + return false; + } + } + } + + for (auto resourceIt = mFoldersForResource.cbegin(), end = mFoldersForResource.cend(); resourceIt != end; ++resourceIt) { + const QHash &requested = resourceIt.value(); + for (auto it = requested.cbegin(), end = requested.cend(); it != end; ++it) { + if (it.value() && !mSpecialCollections->hasCollection(it.key(), AgentManager::self()->instance(resourceIt.key()))) { + return false; + } + } + } + + return true; +} + +void SpecialCollectionsRequestJobPrivate::lockResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Failed to get lock:" << job->errorString(); + q->setError(job->error()); + q->setErrorText(job->errorString()); + q->emitResult(); + return; + } + + if (mRequestingDefaultFolders) { + // If default folders are requested, deal with that first. + auto resjob = new DefaultResourceJob(mSpecialCollections->d->mSettings, q); + resjob->setDefaultResourceType(mDefaultResourceType); + resjob->setDefaultResourceOptions(mDefaultResourceOptions); + resjob->setTypes(mKnownTypes); + resjob->setNameForTypeMap(mNameForTypeMap); + resjob->setIconForTypeMap(mIconForTypeMap); + QObject::connect(resjob, &KJob::result, q, [this](KJob *job) { + resourceScanResult(job); + }); + } else { + // If no default folders are requested, go straight to the next step. + nextResource(); + } +} + +void SpecialCollectionsRequestJobPrivate::releaseLock() +{ + const bool ok = Akonadi::releaseLock(); + if (!ok) { + qCWarning(AKONADICORE_LOG) << "WTF, can't release lock."; + } +} + +void SpecialCollectionsRequestJobPrivate::nextResource() +{ + if (mFoldersForResource.isEmpty()) { + qCDebug(AKONADICORE_LOG) << "All done! Committing."; + + mSpecialCollections->d->beginBatchRegister(); + + // Forget everything we knew before about these resources. + for (const QString &resourceId : std::as_const(mToForget)) { + mSpecialCollections->d->forgetFoldersForResource(resourceId); + } + + // Register all the collections that we fetched / created. + using RegisterPair = QPair; + for (const RegisterPair &pair : std::as_const(mToRegister)) { + const bool ok = mSpecialCollections->registerCollection(pair.second, pair.first); + Q_ASSERT(ok); + Q_UNUSED(ok) + } + + mSpecialCollections->d->endBatchRegister(); + + // Release the lock once the transaction has been committed. + QObject::connect(q, &KJob::result, q, [this]() { + releaseLock(); + }); + + // We are done! + q->commit(); + + } else { + const QString resourceId = mFoldersForResource.cbegin().key(); + qCDebug(AKONADICORE_LOG) << "A resource is done," << mFoldersForResource.count() << "more to do. Now doing resource" << resourceId; + auto resjob = new ResourceScanJob(resourceId, mSpecialCollections->d->mSettings, q); + QObject::connect(resjob, &KJob::result, q, [this](KJob *job) { + resourceScanResult(job); + }); + } +} + +void SpecialCollectionsRequestJobPrivate::resourceScanResult(KJob *job) +{ + auto resjob = qobject_cast(job); + Q_ASSERT(resjob); + + const QString resourceId = resjob->resourceId(); + qCDebug(AKONADICORE_LOG) << "resourceId" << resourceId; + + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Failed to request resource" << resourceId << ":" << job->errorString(); + return; + } + + if (qobject_cast(job)) { + // This is the default resource. + if (resourceId != mSpecialCollections->d->defaultResourceId()) { + qCWarning(AKONADICORE_LOG) << "Resource id's don't match: " << resourceId << mSpecialCollections->d->defaultResourceId(); + Q_ASSERT(false); + } + // mToForget.append( mSpecialCollections->defaultResourceId() ); + createRequestedFolders(resjob, mDefaultFolders); + } else { + // This is not the default resource. + QHash requestedFolders = mFoldersForResource[resourceId]; + mFoldersForResource.remove(resourceId); + createRequestedFolders(resjob, requestedFolders); + } +} + +void SpecialCollectionsRequestJobPrivate::createRequestedFolders(ResourceScanJob *scanJob, QHash &requestedFolders) +{ + // Remove from the request list the folders which already exist. + const Akonadi::Collection::List lstSpecialCols = scanJob->specialCollections(); + for (const Collection &collection : lstSpecialCols) { + Q_ASSERT(collection.hasAttribute()); + const auto attr = collection.attribute(); + const QByteArray type = attr->collectionType(); + + if (!type.isEmpty()) { + mToRegister.append(qMakePair(collection, type)); + requestedFolders.insert(type, false); + } + } + mToForget.append(scanJob->resourceId()); + + // Folders left in the request list must be created. + Q_ASSERT(mPendingCreateJobs == 0); + Q_ASSERT(scanJob->rootResourceCollection().isValid()); + + QHashIterator it(requestedFolders); + while (it.hasNext()) { + it.next(); + + if (it.value()) { + Collection collection; + collection.setParentCollection(scanJob->rootResourceCollection()); + collection.setName(mNameForTypeMap.value(it.key())); + + setCollectionAttributes(collection, it.key(), mNameForTypeMap, mIconForTypeMap); + + auto createJob = new CollectionCreateJob(collection, q); + createJob->setProperty("type", it.key()); + QObject::connect(createJob, &KJob::result, q, [this](KJob *job) { + collectionCreateResult(job); + }); + + mPendingCreateJobs++; + } + } + + if (mPendingCreateJobs == 0) { + nextResource(); + } +} + +void SpecialCollectionsRequestJobPrivate::collectionCreateResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Failed CollectionCreateJob." << job->errorString(); + return; + } + + auto createJob = qobject_cast(job); + Q_ASSERT(createJob); + + const Collection collection = createJob->collection(); + mToRegister.append(qMakePair(collection, createJob->property("type").toByteArray())); + + Q_ASSERT(mPendingCreateJobs > 0); + mPendingCreateJobs--; + qCDebug(AKONADICORE_LOG) << "mPendingCreateJobs now" << mPendingCreateJobs; + + if (mPendingCreateJobs == 0) { + nextResource(); + } +} + +// TODO KDE5: do not inherit from TransactionSequence +SpecialCollectionsRequestJob::SpecialCollectionsRequestJob(SpecialCollections *collections, QObject *parent) + : TransactionSequence(parent) + , d(new SpecialCollectionsRequestJobPrivate(collections, this)) +{ + setProperty("transactionsDisabled", true); +} + +SpecialCollectionsRequestJob::~SpecialCollectionsRequestJob() +{ + delete d; +} + +void SpecialCollectionsRequestJob::requestDefaultCollection(const QByteArray &type) +{ + d->mDefaultFolders[type] = true; + d->mRequestingDefaultFolders = true; + d->mRequestedType = type; +} + +void SpecialCollectionsRequestJob::requestCollection(const QByteArray &type, const AgentInstance &instance) +{ + d->mFoldersForResource[instance.identifier()][type] = true; + d->mRequestedType = type; + d->mRequestedResource = instance; +} + +Akonadi::Collection SpecialCollectionsRequestJob::collection() const +{ + if (d->mRequestedResource.isValid()) { + return d->mSpecialCollections->collection(d->mRequestedType, d->mRequestedResource); + } else { + return d->mSpecialCollections->defaultCollection(d->mRequestedType); + } +} + +void SpecialCollectionsRequestJob::setDefaultResourceType(const QString &type) +{ + d->mDefaultResourceType = type; +} + +void SpecialCollectionsRequestJob::setDefaultResourceOptions(const QVariantMap &options) +{ + d->mDefaultResourceOptions = options; +} + +void SpecialCollectionsRequestJob::setTypes(const QList &types) +{ + d->mKnownTypes = types; +} + +void SpecialCollectionsRequestJob::setNameForTypeMap(const QMap &map) +{ + d->mNameForTypeMap = map; +} + +void SpecialCollectionsRequestJob::setIconForTypeMap(const QMap &map) +{ + d->mIconForTypeMap = map; +} + +void SpecialCollectionsRequestJob::doStart() +{ + if (d->isEverythingReady()) { + emitResult(); + } else { + auto lockJob = new GetLockJob(this); + connect(lockJob, &GetLockJob::result, this, [this](KJob *job) { + d->lockResult(job); + }); + lockJob->start(); + } +} + +void SpecialCollectionsRequestJob::slotResult(KJob *job) +{ + if (job->error()) { + // If we failed, let others try. + qCWarning(AKONADICORE_LOG) << "Failed SpecialCollectionsRequestJob::slotResult" << job->errorString(); + + d->releaseLock(); + } + TransactionSequence::slotResult(job); +} + +#include "moc_specialcollectionsrequestjob.cpp" diff --git a/src/core/jobs/specialcollectionsrequestjob.h b/src/core/jobs/specialcollectionsrequestjob.h new file mode 100644 index 0000000..4773fc3 --- /dev/null +++ b/src/core/jobs/specialcollectionsrequestjob.h @@ -0,0 +1,119 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "specialcollections.h" +#include "transactionsequence.h" + +#include + +namespace Akonadi +{ +class SpecialCollectionsRequestJobPrivate; + +/** + * @short A job to request SpecialCollections. + * + * Use this job to request the SpecialCollections you need. You can request both + * default SpecialCollections and SpecialCollections in a given resource. The default + * SpecialCollections resource is created when the first default SpecialCollection is + * requested, but if a SpecialCollection in a custom resource is requested, this + * job expects that resource to exist already. + * + * If the folders you requested already exist, this job simply succeeds. + * Otherwise, it creates the required collections and registers them with + * SpecialCollections. + * + * This class is not meant to be used directly but as a base class for type + * specific special collection request jobs. + * + * @author Constantin Berzan + * @since 4.4 + */ +class AKONADICORE_EXPORT SpecialCollectionsRequestJob : public TransactionSequence +{ + Q_OBJECT + +public: + /** + * Destroys the special collections request job. + */ + ~SpecialCollectionsRequestJob() override; + + /** + * Requests a special collection of the given @p type in the default resource. + */ + void requestDefaultCollection(const QByteArray &type); + + /** + * Requests a special collection of the given @p type in the given resource @p instance. + */ + void requestCollection(const QByteArray &type, const AgentInstance &instance); + + /** + * Returns the requested collection. + */ + Q_REQUIRED_RESULT Collection collection() const; + +protected: + /** + * Creates a new special collections request job. + * + * @param collections The SpecialCollections object that shall be used. + * @param parent The parent object. + */ + explicit SpecialCollectionsRequestJob(SpecialCollections *collections, QObject *parent = nullptr); + + /** + * Sets the @p type of the resource that shall be created if the requested + * special collection does not exist yet. + */ + void setDefaultResourceType(const QString &type); + + /** + * Sets the configuration @p options that shall be applied to the new resource + * that is created if the requested special collection does not exist yet. + */ + void setDefaultResourceOptions(const QVariantMap &options); + + /** + * Sets the list of well known special collection @p types. + */ + void setTypes(const QList &types); + + /** + * Sets the @p map of special collection types to display names. + */ + void setNameForTypeMap(const QMap &map); + + /** + * Sets the @p map of special collection types to icon names. + */ + void setIconForTypeMap(const QMap &map); + + /* reimpl */ + void doStart() override; + /* reimpl */ + void slotResult(KJob *job) override; + +private: + /// @cond PRIVATE + friend class SpecialCollectionsRequestJobPrivate; + friend class DefaultResourceJobPrivate; + + SpecialCollectionsRequestJobPrivate *const d; + + Q_PRIVATE_SLOT(d, void releaseLock()) + Q_PRIVATE_SLOT(d, void resourceScanResult(KJob *)) + Q_PRIVATE_SLOT(d, void collectionCreateResult(KJob *)) + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/jobs/subscriptionjob.cpp b/src/core/jobs/subscriptionjob.cpp new file mode 100644 index 0000000..8998a80 --- /dev/null +++ b/src/core/jobs/subscriptionjob.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "subscriptionjob_p.h" + +#include "job_p.h" + +#include "collectionmodifyjob.h" + +using namespace Akonadi; + +class Akonadi::SubscriptionJobPrivate : public JobPrivate +{ +public: + explicit SubscriptionJobPrivate(SubscriptionJob *parent) + : JobPrivate(parent) + { + } + + Q_DECLARE_PUBLIC(SubscriptionJob) + + Collection::List mSub, mUnsub; +}; + +SubscriptionJob::SubscriptionJob(QObject *parent) + : Job(new SubscriptionJobPrivate(this), parent) +{ +} + +SubscriptionJob::~SubscriptionJob() +{ +} + +void SubscriptionJob::subscribe(const Collection::List &list) +{ + Q_D(SubscriptionJob); + + d->mSub = list; +} + +void SubscriptionJob::unsubscribe(const Collection::List &list) +{ + Q_D(SubscriptionJob); + + d->mUnsub = list; +} + +void SubscriptionJob::doStart() +{ + Q_D(SubscriptionJob); + + if (d->mSub.isEmpty() && d->mUnsub.isEmpty()) { + emitResult(); + return; + } + + for (Collection col : std::as_const(d->mSub)) { + col.setEnabled(true); + new CollectionModifyJob(col, this); + } + for (Collection col : std::as_const(d->mUnsub)) { + col.setEnabled(false); + new CollectionModifyJob(col, this); + } +} + +void SubscriptionJob::slotResult(KJob *job) +{ + if (job->error()) { + setError(job->error()); + setErrorText(job->errorText()); + Q_FOREACH (KJob *subjob, subjobs()) { + removeSubjob(subjob); + } + emitResult(); + } else { + Job::slotResult(job); + + if (!hasSubjobs()) { + emitResult(); + } + } +} + +#include "moc_subscriptionjob_p.cpp" diff --git a/src/core/jobs/subscriptionjob_p.h b/src/core/jobs/subscriptionjob_p.h new file mode 100644 index 0000000..30abe4a --- /dev/null +++ b/src/core/jobs/subscriptionjob_p.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "job.h" + +namespace Akonadi +{ +class SubscriptionJobPrivate; + +/** + * @internal + * + * @short Job to manipulate the local subscription state of a set of collections. + */ +class AKONADICORE_EXPORT SubscriptionJob : public Job +{ + Q_OBJECT +public: + /** + * Creates a new subscription job. + * + * @param parent The parent object. + */ + explicit SubscriptionJob(QObject *parent = nullptr); + + /** + * Destroys the subscription job. + */ + ~SubscriptionJob() override; + + /** + * Subscribes to the given list of collections. + * + * @param collections List of collections to subscribe to. + */ + void subscribe(const Collection::List &collections); + + /** + * Unsubscribes from the given list of collections. + * + * @param collections List of collections to unsubscribe from. + */ + void unsubscribe(const Collection::List &collections); + +protected: + void doStart() override; + void slotResult(KJob *job) override; + +private: + Q_DECLARE_PRIVATE(SubscriptionJob) +}; + +} + diff --git a/src/core/jobs/tagcreatejob.cpp b/src/core/jobs/tagcreatejob.cpp new file mode 100644 index 0000000..f05f22d --- /dev/null +++ b/src/core/jobs/tagcreatejob.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagcreatejob.h" +#include "akonadicore_debug.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "tag.h" +#include + +using namespace Akonadi; + +class Akonadi::TagCreateJobPrivate : public JobPrivate +{ +public: + explicit TagCreateJobPrivate(TagCreateJob *parent) + : JobPrivate(parent) + { + } + + Tag mTag; + Tag mResultTag; + bool mMerge = false; +}; + +TagCreateJob::TagCreateJob(const Akonadi::Tag &tag, QObject *parent) + : Job(new TagCreateJobPrivate(this), parent) +{ + Q_D(TagCreateJob); + d->mTag = tag; +} + +void TagCreateJob::setMergeIfExisting(bool merge) +{ + Q_D(TagCreateJob); + d->mMerge = merge; +} + +void TagCreateJob::doStart() +{ + Q_D(TagCreateJob); + + if (d->mTag.gid().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "The gid of a new tag must not be empty"; + setError(Job::Unknown); + setErrorText(i18n("Failed to create tag.")); + emitResult(); + return; + } + + auto cmd = Protocol::CreateTagCommandPtr::create(); + cmd->setGid(d->mTag.gid()); + cmd->setMerge(d->mMerge); + cmd->setType(d->mTag.type()); + cmd->setRemoteId(d->mTag.remoteId()); + cmd->setParentId(d->mTag.parent().id()); + cmd->setAttributes(ProtocolHelper::attributesToProtocol(d->mTag)); + d->sendCommand(cmd); +} + +bool TagCreateJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(TagCreateJob); + + if (response->isResponse() && response->type() == Protocol::Command::FetchTags) { + d->mResultTag = ProtocolHelper::parseTagFetchResult(Protocol::cmdCast(response)); + return false; + } + + if (response->isResponse() && response->type() == Protocol::Command::CreateTag) { + return true; + } + + return Job::doHandleResponse(tag, response); +} + +Tag TagCreateJob::tag() const +{ + Q_D(const TagCreateJob); + return d->mResultTag; +} diff --git a/src/core/jobs/tagcreatejob.h b/src/core/jobs/tagcreatejob.h new file mode 100644 index 0000000..728d858 --- /dev/null +++ b/src/core/jobs/tagcreatejob.h @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Tag; +class TagCreateJobPrivate; + +/** + * @short Job that creates a new tag in the Akonadi storage. + * @since 4.13 + */ +class AKONADICORE_EXPORT TagCreateJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new tag create job. + * + * @param tag The tag to create. + * @param parent The parent object. + */ + explicit TagCreateJob(const Tag &tag, QObject *parent = nullptr); + + /** + * Returns the created tag with the new unique id, or an invalid tag if the job failed. + */ + Q_REQUIRED_RESULT Tag tag() const; + + /** + * Merges the tag by GID if it is already existing, and returns the merged version. + * This is false by default. + * + * Note that the returned tag does not contain attributes. + */ + void setMergeIfExisting(bool merge); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(TagCreateJob) +}; + +} + diff --git a/src/core/jobs/tagdeletejob.cpp b/src/core/jobs/tagdeletejob.cpp new file mode 100644 index 0000000..998a2e1 --- /dev/null +++ b/src/core/jobs/tagdeletejob.cpp @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagdeletejob.h" +#include "job_p.h" +#include "protocolhelper_p.h" + +using namespace Akonadi; + +class Akonadi::TagDeleteJobPrivate : public JobPrivate +{ +public: + explicit TagDeleteJobPrivate(TagDeleteJob *parent) + : JobPrivate(parent) + { + } + + Tag::List mTagsToRemove; +}; + +TagDeleteJob::TagDeleteJob(const Akonadi::Tag &tag, QObject *parent) + : Job(new TagDeleteJobPrivate(this), parent) +{ + Q_D(TagDeleteJob); + d->mTagsToRemove << tag; +} + +TagDeleteJob::TagDeleteJob(const Tag::List &tags, QObject *parent) + : Job(new TagDeleteJobPrivate(this), parent) +{ + Q_D(TagDeleteJob); + d->mTagsToRemove = tags; +} + +void TagDeleteJob::doStart() +{ + Q_D(TagDeleteJob); + + d->sendCommand(Protocol::DeleteTagCommandPtr::create(ProtocolHelper::entitySetToScope(d->mTagsToRemove))); +} + +bool TagDeleteJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::DeleteTag) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +Tag::List TagDeleteJob::tags() const +{ + Q_D(const TagDeleteJob); + return d->mTagsToRemove; +} diff --git a/src/core/jobs/tagdeletejob.h b/src/core/jobs/tagdeletejob.h new file mode 100644 index 0000000..0df27f7 --- /dev/null +++ b/src/core/jobs/tagdeletejob.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" +#include "tag.h" + +namespace Akonadi +{ +class Tag; +class TagDeleteJobPrivate; + +/** + * @short Job that deletes tags. + * @since 4.13 + */ +class AKONADICORE_EXPORT TagDeleteJob : public Job +{ + Q_OBJECT + +public: + explicit TagDeleteJob(const Tag &tag, QObject *parent = nullptr); + explicit TagDeleteJob(const Tag::List &tag, QObject *parent = nullptr); + + /** + * Returns the tags passed to the constructor. + */ + Q_REQUIRED_RESULT Tag::List tags() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(TagDeleteJob) +}; + +} + diff --git a/src/core/jobs/tagfetchjob.cpp b/src/core/jobs/tagfetchjob.cpp new file mode 100644 index 0000000..c75471b --- /dev/null +++ b/src/core/jobs/tagfetchjob.cpp @@ -0,0 +1,158 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagfetchjob.h" +#include "attributefactory.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "tagfetchscope.h" +#include + +using namespace Akonadi; + +class Akonadi::TagFetchJobPrivate : public JobPrivate +{ +public: + explicit TagFetchJobPrivate(TagFetchJob *parent) + : JobPrivate(parent) + { + } + + void init() + { + Q_Q(TagFetchJob); + mEmitTimer = new QTimer(q); + mEmitTimer->setSingleShot(true); + mEmitTimer->setInterval(100); + q->connect(mEmitTimer, &QTimer::timeout, q, [this]() { + timeout(); + }); + } + + void aboutToFinish() override + { + timeout(); + } + + void timeout() + { + Q_Q(TagFetchJob); + mEmitTimer->stop(); // in case we are called by result() + if (!mPendingTags.isEmpty()) { + if (!q->error()) { + Q_EMIT q->tagsReceived(mPendingTags); + } + mPendingTags.clear(); + } + } + + Q_DECLARE_PUBLIC(TagFetchJob) + + Tag::List mRequestedTags; + Tag::List mResultTags; + Tag::List mPendingTags; // items pending for emitting itemsReceived() + QTimer *mEmitTimer = nullptr; + TagFetchScope mFetchScope; +}; + +TagFetchJob::TagFetchJob(QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); +} + +TagFetchJob::TagFetchJob(const Tag &tag, QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); + d->mRequestedTags << tag; +} + +TagFetchJob::TagFetchJob(const Tag::List &tags, QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); + d->mRequestedTags = tags; +} + +TagFetchJob::TagFetchJob(const QList &ids, QObject *parent) + : Job(new TagFetchJobPrivate(this), parent) +{ + Q_D(TagFetchJob); + d->init(); + for (Tag::Id id : ids) { + d->mRequestedTags << Tag(id); + } +} + +void TagFetchJob::setFetchScope(const TagFetchScope &fetchScope) +{ + Q_D(TagFetchJob); + d->mFetchScope = fetchScope; +} + +TagFetchScope &TagFetchJob::fetchScope() +{ + Q_D(TagFetchJob); + return d->mFetchScope; +} + +void TagFetchJob::doStart() +{ + Q_D(TagFetchJob); + + Protocol::FetchTagsCommandPtr cmd; + if (d->mRequestedTags.isEmpty()) { + cmd = Protocol::FetchTagsCommandPtr::create(Scope(ImapInterval(1, 0))); + } else { + try { + cmd = Protocol::FetchTagsCommandPtr::create(ProtocolHelper::entitySetToScope(d->mRequestedTags)); + } catch (const Exception &e) { + setError(Job::Unknown); + setErrorText(QString::fromUtf8(e.what())); + emitResult(); + return; + } + } + cmd->setFetchScope(ProtocolHelper::tagFetchScopeToProtocol(d->mFetchScope)); + + d->sendCommand(cmd); +} + +bool TagFetchJob::doHandleResponse(qint64 _tag, const Protocol::CommandPtr &response) +{ + Q_D(TagFetchJob); + + if (!response->isResponse() || response->type() != Protocol::Command::FetchTags) { + return Job::doHandleResponse(_tag, response); + } + + const auto &resp = Protocol::cmdCast(response); + // Invalid tag in response marks the last response + if (resp.id() < 0) { + return true; + } + + const Tag tag = ProtocolHelper::parseTagFetchResult(resp); + d->mResultTags.append(tag); + d->mPendingTags.append(tag); + if (!d->mEmitTimer->isActive()) { + d->mEmitTimer->start(); + } + + return false; +} + +Tag::List TagFetchJob::tags() const +{ + Q_D(const TagFetchJob); + return d->mResultTags; +} + +#include "moc_tagfetchjob.cpp" diff --git a/src/core/jobs/tagfetchjob.h b/src/core/jobs/tagfetchjob.h new file mode 100644 index 0000000..e4dc6df --- /dev/null +++ b/src/core/jobs/tagfetchjob.h @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" +#include "tag.h" + +namespace Akonadi +{ +class TagFetchScope; +class TagFetchJobPrivate; + +/** + * @short Job that fetches tags from the Akonadi storage. + * + * This class is used to fetch tags from the Akonadi storage. + * + * If you want to fetch all items with given tag, use ItemFetchJob and the + * ItemFetchJob(const Tag &tag, QObject *parent = nullptr) constructor (since 4.14) + * + * @since 4.13 + */ +class AKONADICORE_EXPORT TagFetchJob : public Job +{ + Q_OBJECT + +public: + /** + * Constructs a new tag fetch job that retrieves all tags stored in Akonadi. + * + * @param parent The parent object. + */ + explicit TagFetchJob(QObject *parent = nullptr); + + /** + * Constructs a new tag fetch job that retrieves the specified tag. + * If the tag has a uid set, this is used to identify the tag on the Akonadi + * server. If only a remote identifier is available, that is used. However + * as remote identifiers are internal to resources, it's necessary to set + * the resource context (see ResourceSelectJob). + * + * @param tag The tag to fetch. + * @param parent The parent object. + */ + explicit TagFetchJob(const Tag &tag, QObject *parent = nullptr); + + /** + * Constructs a new tag fetch job that retrieves specified tags. + * If the tags have a uid set, this is used to identify the tags on the Akonadi + * server. If only a remote identifier is available, that is used. However + * as remote identifiers are internal to resources, it's necessary to set + * the resource context (see ResourceSelectJob). + * + * @param tags Tags to fetch. + * @param parent The parent object. + */ + explicit TagFetchJob(const Tag::List &tags, QObject *parent = nullptr); + + /** + * Convenience ctor equivalent to ItemFetchJob(const Item::List &items, QObject *parent = nullptr) + * + * @param ids UIDs of tags to fetch. + * @param parent The parent object. + */ + explicit TagFetchJob(const QList &ids, QObject *parent = nullptr); + + /** + * Sets the tag fetch scope. + * + * The TagFetchScope controls how much of an tags's data is fetched + * from the server. + * + * @param fetchScope The new fetch scope for tag fetch operations. + * @see fetchScope() + */ + void setFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the TagFetchScope documentation + * for an example. + * + * @return a reference to the current tag fetch scope + * + * @see setFetchScope() for replacing the current tag fetch scope + */ + TagFetchScope &fetchScope(); + + /** + * Returns the fetched tags after the job has been completed. + */ + Q_REQUIRED_RESULT Tag::List tags() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever new tags have been fetched completely. + * + * @param tags The fetched tags + */ + void tagsReceived(const Akonadi::Tag::List &tags); + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(TagFetchJob) + + /// @cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void timeout()) + /// @endcond +}; + +} + diff --git a/src/core/jobs/tagmodifyjob.cpp b/src/core/jobs/tagmodifyjob.cpp new file mode 100644 index 0000000..acd3c73 --- /dev/null +++ b/src/core/jobs/tagmodifyjob.cpp @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagmodifyjob.h" +#include "changemediator_p.h" +#include "job_p.h" +#include "protocolhelper_p.h" +#include "tag.h" +#include "tag_p.h" + +using namespace Akonadi; + +class Akonadi::TagModifyJobPrivate : public JobPrivate +{ +public: + explicit TagModifyJobPrivate(TagModifyJob *parent) + : JobPrivate(parent) + { + } + + QString jobDebuggingString() const override; + Tag mTag; +}; + +QString Akonadi::TagModifyJobPrivate::jobDebuggingString() const +{ + return QStringLiteral("Modify Tag: %1").arg(mTag.name()); +} + +TagModifyJob::TagModifyJob(const Akonadi::Tag &tag, QObject *parent) + : Job(new TagModifyJobPrivate(this), parent) +{ + Q_D(TagModifyJob); + d->mTag = tag; +} + +void TagModifyJob::doStart() +{ + Q_D(TagModifyJob); + + auto cmd = Protocol::ModifyTagCommandPtr::create(d->mTag.id()); + if (!d->mTag.remoteId().isNull()) { + cmd->setRemoteId(d->mTag.remoteId()); + } + if (!d->mTag.type().isEmpty()) { + cmd->setType(d->mTag.type()); + } + if (d->mTag.parent().isValid() && !d->mTag.isImmutable()) { + cmd->setParentId(d->mTag.parent().id()); + } + if (!d->mTag.d_ptr->mAttributeStorage.deletedAttributes().isEmpty()) { + cmd->setRemovedAttributes(d->mTag.d_ptr->mAttributeStorage.deletedAttributes()); + } + if (d->mTag.d_ptr->mAttributeStorage.hasModifiedAttributes()) { + cmd->setAttributes(ProtocolHelper::attributesToProtocol(d->mTag.d_ptr->mAttributeStorage.modifiedAttributes())); + } + + d->sendCommand(cmd); +} + +bool TagModifyJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(TagModifyJob); + + if (response->isResponse()) { + if (response->type() == Protocol::Command::FetchTags || response->type() == Protocol::Command::DeleteTag) { + // Tag was modified, deleted or merged, we ignore the response for now + return false; + } else if (response->type() == Protocol::Command::ModifyTag) { + // Done. + d->mTag.d_ptr->resetChangeLog(); + return true; + } + } + + return Job::doHandleResponse(tag, response); +} diff --git a/src/core/jobs/tagmodifyjob.h b/src/core/jobs/tagmodifyjob.h new file mode 100644 index 0000000..6347d18 --- /dev/null +++ b/src/core/jobs/tagmodifyjob.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class Tag; +class TagModifyJobPrivate; + +/** + * @short Job that modifies a tag in the Akonadi storage. + * @since 4.13 + */ +class AKONADICORE_EXPORT TagModifyJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new tag modify job. + * + * @param tag The tag to modify. + * @param parent The parent object. + */ + explicit TagModifyJob(const Tag &tag, QObject *parent = nullptr); + + /** + * Returns the modified tag. + */ + Q_REQUIRED_RESULT Tag tag() const; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(TagModifyJob) +}; + +} + diff --git a/src/core/jobs/transactionjobs.cpp b/src/core/jobs/transactionjobs.cpp new file mode 100644 index 0000000..9fe9439 --- /dev/null +++ b/src/core/jobs/transactionjobs.cpp @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "transactionjobs.h" + +#include "job_p.h" +#include "private/protocol_p.h" + +using namespace Akonadi; + +class Akonadi::TransactionJobPrivate : public JobPrivate +{ +public: + explicit TransactionJobPrivate(Job *parent) + : JobPrivate(parent) + { + } +}; + +TransactionJob::TransactionJob(QObject *parent) + : Job(new TransactionJobPrivate(this), parent) +{ + Q_ASSERT(parent); +} + +TransactionJob::~TransactionJob() +{ +} + +void TransactionJob::doStart() +{ + Q_D(TransactionJob); + + Protocol::TransactionCommand::Mode mode; + if (qobject_cast(this)) { + mode = Protocol::TransactionCommand::Begin; + } else if (qobject_cast(this)) { + mode = Protocol::TransactionCommand::Rollback; + } else if (qobject_cast(this)) { + mode = Protocol::TransactionCommand::Commit; + } else { + Q_ASSERT(false); + mode = Protocol::TransactionCommand::Invalid; + } + + d->sendCommand(Protocol::TransactionCommandPtr::create(mode)); +} + +bool TransactionJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (!response->isResponse() || response->type() != Protocol::Command::Transaction) { + return Job::doHandleResponse(tag, response); + } + + return true; +} + +TransactionBeginJob::TransactionBeginJob(QObject *parent) + : TransactionJob(parent) +{ +} + +TransactionBeginJob::~TransactionBeginJob() +{ +} + +TransactionRollbackJob::TransactionRollbackJob(QObject *parent) + : TransactionJob(parent) +{ +} + +TransactionRollbackJob::~TransactionRollbackJob() +{ +} + +TransactionCommitJob::TransactionCommitJob(QObject *parent) + : TransactionJob(parent) +{ +} + +TransactionCommitJob::~TransactionCommitJob() +{ +} diff --git a/src/core/jobs/transactionjobs.h b/src/core/jobs/transactionjobs.h new file mode 100644 index 0000000..c9781dd --- /dev/null +++ b/src/core/jobs/transactionjobs.h @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class TransactionJobPrivate; +class AKONADICORE_EXPORT TransactionJob : public Job +{ + Q_OBJECT + +public: + ~TransactionJob() override; + +protected: + explicit TransactionJob(QObject *parent); + + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(TransactionJob) +}; + +class TransactionJobPrivate; + +/** + * @short Job that begins a session-global transaction. + * + * Sometimes you want to execute a sequence of commands in + * an atomic way, so that either all commands or none shall + * be executed. The TransactionBeginJob, TransactionCommitJob and + * TransactionRollbackJob provide these functionality for the + * Akonadi Job classes. + * + * @note This will only have an effect when used as a subjob or with a Session. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionBeginJob : public TransactionJob +{ + Q_OBJECT + +public: + /** + * Creates a new transaction begin job. + * + * @param parent The parent job or Session, must not be 0. + */ + explicit TransactionBeginJob(QObject *parent); + + /** + * Destroys the transaction begin job. + */ + ~TransactionBeginJob(); +}; + +/** + * @short Job that aborts a session-global transaction. + * + * If a job inside a TransactionBeginJob has been failed, + * the TransactionRollbackJob can be used to rollback all changes done by these + * jobs. + * + * @note This will only have an effect when used as a subjob or with a Session. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionRollbackJob : public TransactionJob +{ + Q_OBJECT + +public: + /** + * Creates a new transaction rollback job. + * The parent must be the same parent as for the TransactionBeginJob. + * + * @param parent The parent job or Session, must not be 0. + */ + explicit TransactionRollbackJob(QObject *parent); + + /** + * Destroys the transaction rollback job. + */ + ~TransactionRollbackJob(); +}; + +/** + * @short Job that commits a session-global transaction. + * + * This job commits all changes of this transaction. + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionCommitJob : public TransactionJob +{ + Q_OBJECT + +public: + /** + * Creates a new transaction commit job. + * The parent must be the same parent as for the TransactionBeginJob. + * + * @param parent The parent job or Session, must not be 0. + */ + explicit TransactionCommitJob(QObject *parent); + + /** + * Destroys the transaction commit job. + */ + ~TransactionCommitJob(); +}; + +} + diff --git a/src/core/jobs/transactionsequence.cpp b/src/core/jobs/transactionsequence.cpp new file mode 100644 index 0000000..d15136e --- /dev/null +++ b/src/core/jobs/transactionsequence.cpp @@ -0,0 +1,251 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "transactionsequence.h" +#include "transactionjobs.h" + +#include "job_p.h" + +#include + +using namespace Akonadi; + +class Akonadi::TransactionSequencePrivate : public JobPrivate +{ +public: + explicit TransactionSequencePrivate(TransactionSequence *parent) + : JobPrivate(parent) + , mState(Idle) + { + } + + enum TransactionState { + Idle, + Running, + WaitingForSubjobs, + RollingBack, + Committing, + }; + + Q_DECLARE_PUBLIC(TransactionSequence) + + TransactionState mState; + QSet mIgnoredErrorJobs; + bool mAutoCommit = true; + + void commitResult(KJob *job) + { + Q_Q(TransactionSequence); + + if (job->error()) { + q->setError(job->error()); + q->setErrorText(job->errorText()); + } + q->emitResult(); + } + + void rollbackResult(KJob *job) + { + Q_Q(TransactionSequence); + + Q_UNUSED(job) + q->emitResult(); + } + + QString jobDebuggingString() const override; +}; + +QString Akonadi::TransactionSequencePrivate::jobDebuggingString() const +{ + // TODO add state + return QStringLiteral("autocommit %1").arg(mAutoCommit); +} + +TransactionSequence::TransactionSequence(QObject *parent) + : Job(new TransactionSequencePrivate(this), parent) +{ +} + +TransactionSequence::~TransactionSequence() +{ +} + +bool TransactionSequence::addSubjob(KJob *job) +{ + Q_D(TransactionSequence); + + // Don't abort the rollback job, while keeping the state set. + if (d->mState == TransactionSequencePrivate::RollingBack) { + return Job::addSubjob(job); + } + + if (error()) { + // This can happen if a rollback is in progress, so make sure we don't set the state back to running. + job->kill(EmitResult); + return false; + } + // TODO KDE5: remove property hack once SpecialCollectionsRequestJob has been fixed + if (d->mState == TransactionSequencePrivate::Idle && !property("transactionsDisabled").toBool()) { + d->mState = TransactionSequencePrivate::Running; // needs to be set before creating the transaction job to avoid infinite recursion + new TransactionBeginJob(this); + } else { + d->mState = TransactionSequencePrivate::Running; + } + return Job::addSubjob(job); +} + +void TransactionSequence::slotResult(KJob *job) +{ + Q_D(TransactionSequence); + + if (!job->error() || d->mIgnoredErrorJobs.contains(job)) { + // If we have an error but want to ignore it, we can't call Job::slotResult + // because it would confuse the subjob queue processing logic. Just removing + // the subjob instead is fine. + if (!job->error()) { + Job::slotResult(job); + } else { + Job::removeSubjob(job); + } + + if (!hasSubjobs()) { + if (d->mState == TransactionSequencePrivate::WaitingForSubjobs) { + if (property("transactionsDisabled").toBool()) { + emitResult(); + return; + } + d->mState = TransactionSequencePrivate::Committing; + auto job = new TransactionCommitJob(this); + connect(job, &TransactionCommitJob::result, [d](KJob *job) { + d->commitResult(job); + }); + } + } + } else if (job->error() == KJob::KilledJobError) { + Job::slotResult(job); + } else { + setError(job->error()); + setErrorText(job->errorText()); + removeSubjob(job); + + // cancel all subjobs in case someone else is listening (such as ItemSync) + const auto subjobs = this->subjobs(); + for (KJob *job : subjobs) { + job->kill(KJob::EmitResult); + } + clearSubjobs(); + + if (d->mState == TransactionSequencePrivate::Running || d->mState == TransactionSequencePrivate::WaitingForSubjobs) { + if (property("transactionsDisabled").toBool()) { + emitResult(); + return; + } + d->mState = TransactionSequencePrivate::RollingBack; + auto job = new TransactionRollbackJob(this); + connect(job, &TransactionRollbackJob::result, this, [d](KJob *job) { + d->rollbackResult(job); + }); + } + } +} + +void TransactionSequence::commit() +{ + Q_D(TransactionSequence); + + if (d->mState == TransactionSequencePrivate::Running) { + d->mState = TransactionSequencePrivate::WaitingForSubjobs; + } else if (d->mState == TransactionSequencePrivate::RollingBack) { + return; + } else { + // we never got any subjobs, that means we never started a transaction + // so we can just quit here + if (d->mState == TransactionSequencePrivate::Idle) { + emitResult(); + } + return; + } + + if (subjobs().isEmpty()) { + if (property("transactionsDisabled").toBool()) { + emitResult(); + return; + } + if (!error()) { + d->mState = TransactionSequencePrivate::Committing; + auto job = new TransactionCommitJob(this); + connect(job, &TransactionCommitJob::result, this, [d](KJob *job) { + d->commitResult(job); + }); + } else { + d->mState = TransactionSequencePrivate::RollingBack; + auto job = new TransactionRollbackJob(this); + connect(job, &TransactionRollbackJob::result, this, [d](KJob *job) { + d->rollbackResult(job); + }); + } + } +} + +void TransactionSequence::setIgnoreJobFailure(KJob *job) +{ + Q_D(TransactionSequence); + + // make sure this is one of our sub jobs + Q_ASSERT(subjobs().contains(job)); + + d->mIgnoredErrorJobs.insert(job); +} + +void TransactionSequence::doStart() +{ + Q_D(TransactionSequence); + + if (d->mAutoCommit) { + if (d->mState == TransactionSequencePrivate::Idle) { + emitResult(); + } else { + commit(); + } + } +} + +void TransactionSequence::setAutomaticCommittingEnabled(bool enable) +{ + Q_D(TransactionSequence); + d->mAutoCommit = enable; +} + +void TransactionSequence::rollback() +{ + Q_D(TransactionSequence); + + setError(UserCanceled); + // we never really started + if (d->mState == TransactionSequencePrivate::Idle) { + emitResult(); + return; + } + + const auto jobList = subjobs(); + for (KJob *job : jobList) { + // Killing the current subjob means forcibly closing the akonadiserver socket + // (with a bit of delay since it happens in a secondary thread) + // which means the next job gets disconnected + // and the itemsync finishes with error "Cannot connect to the Akonadi service.", not ideal + if (job != d->mCurrentSubJob) { + job->kill(KJob::EmitResult); + } + } + + d->mState = TransactionSequencePrivate::RollingBack; + auto job = new TransactionRollbackJob(this); + connect(job, &TransactionRollbackJob::result, this, [d](KJob *job) { + d->rollbackResult(job); + }); +} + +#include "moc_transactionsequence.cpp" diff --git a/src/core/jobs/transactionsequence.h b/src/core/jobs/transactionsequence.h new file mode 100644 index 0000000..be16677 --- /dev/null +++ b/src/core/jobs/transactionsequence.h @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "job.h" + +namespace Akonadi +{ +class TransactionSequencePrivate; + +/** + * @short Base class for jobs that need to run a sequence of sub-jobs in a transaction. + * + * As soon as the first subjob is added, the transaction is started. + * As soon as the last subjob has successfully finished, the transaction is committed. + * If any subjob fails, the transaction is rolled back. + * + * Alternatively, a TransactionSequence object can be used as a parent object + * for a set of jobs to achieve the same behaviour without subclassing. + * + * Example: + * + * @code + * + * // Delete a couple of items inside a transaction + * Akonadi::TransactionSequence *transaction = new Akonadi::TransactionSequence; + * connect( transaction, SIGNAL(result(KJob*)), SLOT(transactionFinished(KJob*)) ); + * + * const Akonadi::Item::List items = ... + * + * for( const Akonadi::Item &item : items ) { + * new Akonadi::ItemDeleteJob( item, transaction ); + * } + * + * ... + * + * MyClass::transactionFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Items deleted successfully"; + * } + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT TransactionSequence : public Job +{ + Q_OBJECT +public: + /** + * Creates a new transaction sequence. + * + * @param parent The parent object. + */ + explicit TransactionSequence(QObject *parent = nullptr); + + /** + * Destroys the transaction sequence. + */ + ~TransactionSequence() override; + + /** + * Commits the transaction as soon as all pending sub-jobs finished successfully. + */ + void commit(); + + /** + * Rolls back the current transaction as soon as possible. + * You only need to call this method when you want to roll back due to external + * reasons (e.g. user cancellation), the transaction is automatically rolled back + * if one of its subjobs fails. + * @since 4.5 + */ + void rollback(); + + /** + * Sets which job of the sequence might fail without rolling back the + * complete transaction. + * @param job a job to ignore errors from + * @since 4.5 + */ + void setIgnoreJobFailure(KJob *job); + + /** + * Disable automatic committing. + * Use this when you want to add jobs to this sequence after execution + * has been started, usually that is outside of the constructor or the + * method that creates this transaction sequence. + * @note Calling this method after execution of this job has been started + * has no effect. + * @param enable @c true to enable autocommitting (default), @c false to disable it + * @since 4.5 + */ + void setAutomaticCommittingEnabled(bool enable); + +protected: + bool addSubjob(KJob *job) override; + void doStart() override; + +protected Q_SLOTS: + void slotResult(KJob *job) override; + +private: + Q_DECLARE_PRIVATE(TransactionSequence) + + /// @cond PRIVATE + Q_PRIVATE_SLOT(d_func(), void commitResult(KJob *)) + Q_PRIVATE_SLOT(d_func(), void rollbackResult(KJob *)) + /// @endcond +}; + +} + diff --git a/src/core/jobs/trashjob.cpp b/src/core/jobs/trashjob.cpp new file mode 100644 index 0000000..2503410 --- /dev/null +++ b/src/core/jobs/trashjob.cpp @@ -0,0 +1,381 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "trashjob.h" + +#include "entitydeletedattribute.h" +#include "job_p.h" +#include "trashsettings.h" + +#include + +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "collectionmovejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +class TrashJob::TrashJobPrivate : public JobPrivate +{ +public: + explicit TrashJobPrivate(TrashJob *parent) + : JobPrivate(parent) + { + } + // 4. + void selectResult(KJob *job); + // 3. + // Helper functions to recursively set the attribute on deleted collections + void setAttribute(const Akonadi::Collection::List & /*list*/); + void setAttribute(const Akonadi::Item::List & /*list*/); + // Set attributes after ensuring that move job was successful + void setAttribute(KJob *job); + + // 2. + // called after parent of the trashed item was fetched (needed to see in which resource the item is in) + void parentCollectionReceived(const Akonadi::Collection::List & /*collections*/); + + // 1. + // called after initial fetch of trashed items + void itemsReceived(const Akonadi::Item::List & /*items*/); + // called after initial fetch of trashed collection + void collectionsReceived(const Akonadi::Collection::List & /*collections*/); + + Q_DECLARE_PUBLIC(TrashJob) + + Item::List mItems; + Collection mCollection; + Collection mRestoreCollection; + Collection mTrashCollection; + bool mKeepTrashInCollection = false; + bool mSetRestoreCollection = false; // only set restore collection when moved to trash collection (not in place) + bool mDeleteIfInTrash = false; + QHash mCollectionItems; // list of trashed items sorted according to parent collection + QHash mParentCollections; // fetched parent collection of items (containing the resource name) +}; + +void TrashJob::TrashJobPrivate::selectResult(KJob *job) +{ + Q_Q(TrashJob); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->objectName(); + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; // KCompositeJob takes care of errors + } + + if (!q->hasSubjobs() || (q->subjobs().contains(static_cast(q->sender())) && q->subjobs().size() == 1)) { + q->emitResult(); + } +} + +void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list) +{ + Q_Q(TrashJob); + QVectorIterator i(list); + while (i.hasNext()) { + const Collection &col = i.next(); + auto eda = new EntityDeletedAttribute(); + if (mSetRestoreCollection) { + Q_ASSERT(mRestoreCollection.isValid()); + eda->setRestoreCollection(mRestoreCollection); + } + + Collection modCol(col.id()); // really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move + modCol.addAttribute(eda); + + auto job = new CollectionModifyJob(modCol, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + + auto itemFetchJob = new ItemFetchJob(col, q); + // TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set) + q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) { + setAttribute(items); + }); + q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } +} + +void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Item::List &list) +{ + Q_Q(TrashJob); + Item::List items = list; + QMutableVectorIterator i(items); + while (i.hasNext()) { + const Item &item = i.next(); + auto eda = new EntityDeletedAttribute(); + if (mSetRestoreCollection) { + // When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent + if (mRestoreCollection.isValid()) { + eda->setRestoreCollection(mRestoreCollection); + } else { + Q_ASSERT(mParentCollections.contains(item.parentCollection().id())); + eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id())); + } + } + + Item modItem(item.id()); // really only modify attribute (forget old remote ids, etc.) + modItem.addAttribute(eda); + auto job = new ItemModifyJob(modItem, q); + job->setIgnorePayload(true); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } + + // For some reason it is not possible to apply this change to multiple items at once + /*ItemModifyJob *job = new ItemModifyJob(items, q); + q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/ +} + +void TrashJob::TrashJobPrivate::setAttribute(KJob *job) +{ + Q_Q(TrashJob); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->objectName(); + qCWarning(AKONADICORE_LOG) << job->errorString(); + q->setError(Job::Unknown); + q->setErrorText(i18n("Move to trash collection failed, aborting trash operation")); + return; + } + + // For Items + const QVariant var = job->property("MovedItems"); + if (var.isValid()) { + int id = var.toInt(); + Q_ASSERT(id >= 0); + setAttribute(mCollectionItems.value(Collection(id))); + return; + } + + // For a collection + Q_ASSERT(mCollection.isValid()); + setAttribute(Collection::List() << mCollection); + // Set the attribute on all subcollections and items + auto colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); + q->connect(colFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { + setAttribute(cols); + }); + q->connect(colFetchJob, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); +} + +void TrashJob::TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections) +{ + Q_Q(TrashJob); + Q_ASSERT(collections.size() == 1); + const Collection &parentCollection = collections.first(); + + // store attribute + Q_ASSERT(!parentCollection.resource().isEmpty()); + Collection trashCollection = mTrashCollection; + if (!mTrashCollection.isValid()) { + trashCollection = TrashSettings::getTrashCollection(parentCollection.resource()); + } + if (!mKeepTrashInCollection && trashCollection.isValid()) { // Only set the restore collection if the item is moved to trash + mSetRestoreCollection = true; + } + + mParentCollections.insert(parentCollection.id(), parentCollection); + + if (trashCollection.isValid()) { // Move the items to the correct collection if available + auto job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q); + job->setProperty("MovedItems", parentCollection.id()); + q->connect(job, &KJob::result, q, [this](KJob *job) { + setAttribute(job); + }); // Wait until the move finished to set the attribute + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } else { + setAttribute(mCollectionItems.value(parentCollection)); + } +} + +void TrashJob::TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items) +{ + Q_Q(TrashJob); + if (items.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid items passed")); + q->emitResult(); + return; + } + + Item::List toDelete; + + QVectorIterator i(items); + while (i.hasNext()) { + const Item &item = i.next(); + if (item.hasAttribute()) { + toDelete.append(item); + continue; + } + Q_ASSERT(item.parentCollection().isValid()); + mCollectionItems[item.parentCollection()].append(item); // Sort by parent col ( = restore collection) + } + + for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) { + auto job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q); + q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { + parentCollectionReceived(cols); + }); + } + + if (mDeleteIfInTrash && !toDelete.isEmpty()) { + auto job = new ItemDeleteJob(toDelete, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } else if (mCollectionItems.isEmpty()) { // No job started, so we abort the job + qCWarning(AKONADICORE_LOG) << "Nothing to do"; + q->emitResult(); + } +} + +void TrashJob::TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) +{ + Q_Q(TrashJob); + if (collections.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid collection passed")); + q->emitResult(); + return; + } + Q_ASSERT(collections.size() == 1); + mCollection = collections.first(); + + if (mCollection.hasAttribute()) { // marked as deleted + if (mDeleteIfInTrash) { + auto job = new CollectionDeleteJob(mCollection, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } else { + qCWarning(AKONADICORE_LOG) << "Nothing to do"; + q->emitResult(); + } + return; + } + + Collection trashCollection = mTrashCollection; + if (!mTrashCollection.isValid()) { + trashCollection = TrashSettings::getTrashCollection(mCollection.resource()); + } + if (!mKeepTrashInCollection && trashCollection.isValid()) { // only set the restore collection if the item is moved to trash + mSetRestoreCollection = true; + Q_ASSERT(mCollection.parentCollection().isValid()); + mRestoreCollection = mCollection.parentCollection(); + mRestoreCollection.setResource(mCollection.resource()); // The parent collection doesn't contain the resource, so we have to set it manually + } + + if (trashCollection.isValid()) { + auto job = new CollectionMoveJob(mCollection, trashCollection, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + setAttribute(job); + }); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } else { + setAttribute(Collection::List() << mCollection); + } +} + +TrashJob::TrashJob(const Item &item, QObject *parent) + : Job(new TrashJobPrivate(this), parent) +{ + Q_D(TrashJob); + d->mItems << item; +} + +TrashJob::TrashJob(const Item::List &items, QObject *parent) + : Job(new TrashJobPrivate(this), parent) +{ + Q_D(TrashJob); + d->mItems = items; +} + +TrashJob::TrashJob(const Collection &collection, QObject *parent) + : Job(new TrashJobPrivate(this), parent) +{ + Q_D(TrashJob); + d->mCollection = collection; +} + +TrashJob::~TrashJob() +{ +} + +Item::List TrashJob::items() const +{ + Q_D(const TrashJob); + return d->mItems; +} + +void TrashJob::setTrashCollection(const Akonadi::Collection &collection) +{ + Q_D(TrashJob); + d->mTrashCollection = collection; +} + +void TrashJob::keepTrashInCollection(bool enable) +{ + Q_D(TrashJob); + d->mKeepTrashInCollection = enable; +} + +void TrashJob::deleteIfInTrash(bool enable) +{ + Q_D(TrashJob); + d->mDeleteIfInTrash = enable; +} + +void TrashJob::doStart() +{ + Q_D(TrashJob); + + // Fetch items first to ensure that the EntityDeletedAttribute is available + if (!d->mItems.isEmpty()) { + auto job = new ItemFetchJob(d->mItems, this); + job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); // so we have access to the resource + // job->fetchScope().setCacheOnly(true); + job->fetchScope().fetchAttribute(true); + connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) { + d->itemsReceived(items); + }); + + } else if (d->mCollection.isValid()) { + auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); + job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent); + connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { + d->collectionsReceived(cols); + }); + + } else { + qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; + setError(Job::Unknown); + setErrorText(i18n("No valid collection or empty itemlist")); + emitResult(); + } +} + +#include "moc_trashjob.cpp" diff --git a/src/core/jobs/trashjob.h b/src/core/jobs/trashjob.h new file mode 100644 index 0000000..2793246 --- /dev/null +++ b/src/core/jobs/trashjob.h @@ -0,0 +1,118 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +/** + * @short Job that moves items/collection to trash. + * + * This job marks the given entities as trash and moves them to a given trash collection, if available. + * + * Priorities of trash collections are the following: + * 1. keepTrashInCollection() + * 2. setTrashCollection() + * 3. configured collection in TrashSettings + * 4. keep in collection if nothing is configured + * + * If the item is already marked as trash, it will be deleted instead + * only if deleteIfInTrash() is set. + * The entity is marked as trash with the EntityDeletedAttribute. + * + * The restore collection in the EntityDeletedAttribute is set the following way: + * -If entities are not moved to trash -> no restore collection + * -If collection is deleted -> also subentities get collection.parentCollection as restore collection + * -If multiple items are deleted -> all items get their parentCollection as restore collection + * + * Example: + * + * @code + * + * const Akonadi::Item::List items = ... + * + * TrashJob *job = new TrashJob( items ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(deletionResult(KJob*)) ); + * + * @endcode + * + * @author Christian Mollekopf + * @since 4.8 + */ +class AKONADICORE_EXPORT TrashJob : public Job +{ + Q_OBJECT + +public: + /** + * Creates a new trash job that marks @p item as trash, and moves it to the configured trash collection. + * + * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. + * + * @param item The item to mark as trash. + * @param parent The parent object. + */ + explicit TrashJob(const Item &item, QObject *parent = nullptr); + + /** + * Creates a new trash job that marks all items in the list + * @p items as trash, and moves it to the configured trash collection. + * The items can be in different collections/resources and will still be moved to the correct trash collection. + * + * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. + * + * @param items The items to mark as trash. + * @param parent The parent object. + */ + explicit TrashJob(const Item::List &items, QObject *parent = nullptr); + + /** + * Creates a new trash job that marks @p collection as trash, and moves it to the configured trash collection. + * The subentities of the collection are also marked as trash. + * + * If @p keepTrashInCollection is set, the item will not be moved to the configured trash collection. + * + * @param collection The collection to mark as trash. + * @param parent The parent object. + */ + explicit TrashJob(const Collection &collection, QObject *parent = nullptr); + + ~TrashJob() override; + + /** + * Ignore configured Trash collections and keep all items local + */ + void keepTrashInCollection(bool enable); + + /** + * Moves all entities to the give collection + */ + void setTrashCollection(const Collection &trashcollection); + + /** + * Delete Items which are already in trash, instead of ignoring them + */ + void deleteIfInTrash(bool enable); + + Q_REQUIRED_RESULT Item::List items() const; + +protected: + void doStart() override; + +private: + /// @cond PRIVATE + class TrashJobPrivate; + Q_DECLARE_PRIVATE(TrashJob) + /// @endcond +}; + +} + diff --git a/src/core/jobs/trashrestorejob.cpp b/src/core/jobs/trashrestorejob.cpp new file mode 100644 index 0000000..6158d41 --- /dev/null +++ b/src/core/jobs/trashrestorejob.cpp @@ -0,0 +1,352 @@ +/* + * SPDX-FileCopyrightText: 2011 Christian Mollekopf + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "trashrestorejob.h" + +#include "entitydeletedattribute.h" +#include "job_p.h" + +#include "trashsettings.h" + +#include + +#include "collectiondeletejob.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "collectionmovejob.h" +#include "itemdeletejob.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +class TrashRestoreJob::TrashRestoreJobPrivate : public JobPrivate +{ +public: + explicit TrashRestoreJobPrivate(TrashRestoreJob *parent) + : JobPrivate(parent) + { + } + + void selectResult(KJob *job); + + // Called when the target collection was fetched, + // will issue the move and the removal of the attributes if collection is valid + void targetCollectionFetched(KJob *job); + + void removeAttribute(const Akonadi::Item::List &list); + void removeAttribute(const Akonadi::Collection::List &list); + + // Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore + void itemsReceived(const Akonadi::Item::List &items); + void collectionsReceived(const Akonadi::Collection::List &collections); + + Q_DECLARE_PUBLIC(TrashRestoreJob) + + Item::List mItems; + Collection mCollection; + Collection mTargetCollection; + QHash restoreCollections; // groups items to target restore collections +}; + +void TrashRestoreJob::TrashRestoreJobPrivate::selectResult(KJob *job) +{ + Q_Q(TrashRestoreJob); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; // KCompositeJob takes care of errors + } + + if (!q->hasSubjobs() || (q->subjobs().contains(static_cast(q->sender())) && q->subjobs().size() == 1)) { + // qCWarning(AKONADICORE_LOG) << "trash restore finished"; + q->emitResult(); + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::targetCollectionFetched(KJob *job) +{ + Q_Q(TrashRestoreJob); + + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + const Collection::List &list = fetchJob->collections(); + + if (list.isEmpty() || !list.first().isValid() || list.first().hasAttribute()) { // target collection is invalid/not + // existing + + const QString res = fetchJob->property("Resource").toString(); + if (res.isEmpty()) { // There is no fallback + q->setError(Job::Unknown); + q->setErrorText(i18n("Could not find restore collection and restore resource is not available")); + q->emitResult(); + // FAIL + qCWarning(AKONADICORE_LOG) << "restore collection not available"; + return; + } + + // Try again with the root collection of the resource as fallback + auto resRootFetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, q); + resRootFetch->fetchScope().setResource(res); + const QVariant &var = fetchJob->property("Items"); + if (var.isValid()) { + resRootFetch->setProperty("Items", var.toInt()); + } + q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) { + targetCollectionFetched(job); + }); + q->connect(resRootFetch, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + return; + } + Q_ASSERT(list.size() == 1); + // SUCCESS + // We know where to move the entity, so remove the attributes and move them to the right location + if (!mItems.isEmpty()) { + const QVariant &var = fetchJob->property("Items"); + Q_ASSERT(var.isValid()); + const Item::List &items = restoreCollections[Collection(var.toInt())]; + + // store removed attribute if destination collection is valid or the item doesn't have a restore collection + // TODO only remove the attribute if the move job was successful (although it is unlikely that it fails since we already fetched the collection) + removeAttribute(items); + if (items.first().parentCollection() != list.first()) { + auto job = new ItemMoveJob(items, list.first(), q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } + } else { + Q_ASSERT(mCollection.isValid()); + // TODO only remove the attribute if the move job was successful + removeAttribute(Collection::List() << mCollection); + auto collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); + q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { + removeAttribute(cols); + }); + + if (mCollection.parentCollection() != list.first()) { + auto job = new CollectionMoveJob(mCollection, list.first(), q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::itemsReceived(const Akonadi::Item::List &items) +{ + Q_Q(TrashRestoreJob); + if (items.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid items passed")); + q->emitResult(); + return; + } + mItems = items; + + // Sort by restore collection + for (const Item &item : std::as_const(mItems)) { + if (!item.hasAttribute()) { + continue; + } + // If the restore collection is invalid we restore the item in place, so we don't need to know its restore resource => we can put those cases in the + // same list + restoreCollections[item.attribute()->restoreCollection()].append(item); + } + + for (auto it = restoreCollections.cbegin(), e = restoreCollections.cend(); it != e; ++it) { + const Item &first = it.value().first(); + // Move the items to the correct collection if available + Collection targetCollection = it.key(); + const QString restoreResource = first.attribute()->restoreResource(); + + // Restore in place if no restore collection is set + if (!targetCollection.isValid()) { + removeAttribute(it.value()); + return; + } + + // Explicit target overrides the resource + if (mTargetCollection.isValid()) { + targetCollection = mTargetCollection; + } + + // Try to fetch the target resource to see if it is available + auto fetchJob = new CollectionFetchJob(targetCollection, Akonadi::CollectionFetchJob::Base, q); + if (!mTargetCollection.isValid()) { // explicit targets don't have a fallback + fetchJob->setProperty("Resource", restoreResource); + } + fetchJob->setProperty("Items", it.key().id()); // to find the items in restore collections again + q->connect(fetchJob, &KJob::result, q, [this](KJob *job) { + targetCollectionFetched(job); + }); + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections) +{ + Q_Q(TrashRestoreJob); + if (collections.isEmpty()) { + q->setError(Job::Unknown); + q->setErrorText(i18n("Invalid collection passed")); + q->emitResult(); + return; + } + Q_ASSERT(collections.size() == 1); + mCollection = collections.first(); + + if (!mCollection.hasAttribute()) { + return; + } + + const QString restoreResource = mCollection.attribute()->restoreResource(); + Collection targetCollection = mCollection.attribute()->restoreCollection(); + + // Restore in place if no restore collection/resource is set + if (!targetCollection.isValid()) { + removeAttribute(Collection::List() << mCollection); + auto collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q); + q->connect(collectionFetchJob, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + q->connect(collectionFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) { + removeAttribute(cols); + }); + return; + } + + // Explicit target overrides the resource/configured restore collection + if (mTargetCollection.isValid()) { + targetCollection = mTargetCollection; + } + + // Fetch the target collection to check if it's valid + auto fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base, q); + if (!mTargetCollection.isValid()) { // explicit targets don't have a fallback + fetchJob->setProperty("Resource", restoreResource); + } + q->connect(fetchJob, &KJob::result, q, [this](KJob *job) { + targetCollectionFetched(job); + }); +} + +void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Collection::List &list) +{ + Q_Q(TrashRestoreJob); + QVectorIterator i(list); + while (i.hasNext()) { + Collection col = i.next(); + col.removeAttribute(); + + auto job = new CollectionModifyJob(col, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + + auto itemFetchJob = new ItemFetchJob(col, q); + itemFetchJob->fetchScope().fetchAttribute(true); + q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) { + removeAttribute(items); + }); + } +} + +void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Item::List &list) +{ + Q_Q(TrashRestoreJob); + Item::List items = list; + QMutableVectorIterator i(items); + while (i.hasNext()) { + Item &item = i.next(); + item.removeAttribute(); + auto job = new ItemModifyJob(item, q); + job->setIgnorePayload(true); + q->connect(job, &KJob::result, q, [this](KJob *job) { + selectResult(job); + }); + } + // For some reason it is not possible to apply this change to multiple items at once + // ItemModifyJob *job = new ItemModifyJob(items, q); + // q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) ); +} + +TrashRestoreJob::TrashRestoreJob(const Item &item, QObject *parent) + : Job(new TrashRestoreJobPrivate(this), parent) +{ + Q_D(TrashRestoreJob); + d->mItems << item; +} + +TrashRestoreJob::TrashRestoreJob(const Item::List &items, QObject *parent) + : Job(new TrashRestoreJobPrivate(this), parent) +{ + Q_D(TrashRestoreJob); + d->mItems = items; +} + +TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent) + : Job(new TrashRestoreJobPrivate(this), parent) +{ + Q_D(TrashRestoreJob); + d->mCollection = collection; +} + +TrashRestoreJob::~TrashRestoreJob() +{ +} + +void TrashRestoreJob::setTargetCollection(const Akonadi::Collection &collection) +{ + Q_D(TrashRestoreJob); + d->mTargetCollection = collection; +} + +Item::List TrashRestoreJob::items() const +{ + Q_D(const TrashRestoreJob); + return d->mItems; +} + +void TrashRestoreJob::doStart() +{ + Q_D(TrashRestoreJob); + + // We always have to fetch the entities to ensure that the EntityDeletedAttribute is available + if (!d->mItems.isEmpty()) { + auto job = new ItemFetchJob(d->mItems, this); + job->fetchScope().setCacheOnly(true); + job->fetchScope().fetchAttribute(true); + connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) { + d->itemsReceived(items); + }); + } else if (d->mCollection.isValid()) { + auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this); + connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) { + d->collectionsReceived(cols); + }); + } else { + qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist"; + setError(Job::Unknown); + setErrorText(i18n("No valid collection or empty itemlist")); + emitResult(); + } +} + +#include "moc_trashrestorejob.cpp" diff --git a/src/core/jobs/trashrestorejob.h b/src/core/jobs/trashrestorejob.h new file mode 100644 index 0000000..2e42454 --- /dev/null +++ b/src/core/jobs/trashrestorejob.h @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2011 Christian Mollekopf + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +/** + * @short Job that restores entities from trash + * + * This job restores the given entities from trash. + * The EntityDeletedAttribute is removed and the item is restored to the stored restore collection. + * + * If the stored restore collection is not available, the root collection of the original resource is used. + * If also this is not available, setTargetCollection has to be used to restore the item to a specific collection. + * + * Example: + * + * @code + * + * const Akonadi::Item::List items = ... + * + * TrashRestoreJob *job = new TrashRestoreJob( items ); + * connect( job, SIGNAL(result(KJob*)), this, SLOT(restoreResult(KJob*)) ); + * + * @endcode + * + * @author Christian Mollekopf + * @since 4.8 + */ +class AKONADICORE_EXPORT TrashRestoreJob : public Job +{ + Q_OBJECT +public: + /** + * All items need to be from the same resource + */ + explicit TrashRestoreJob(const Item &item, QObject *parent = nullptr); + + explicit TrashRestoreJob(const Item::List &items, QObject *parent = nullptr); + + explicit TrashRestoreJob(const Collection &collection, QObject *parent = nullptr); + + ~TrashRestoreJob() override; + + /** + * Sets the target collection, where the item is moved to. + * If not set the item will be restored in the collection saved in the EntityDeletedAttribute. + * @param collection the collection to set as target + */ + void setTargetCollection(const Collection &collection); + + Q_REQUIRED_RESULT Item::List items() const; + +protected: + void doStart() override; + +private: + /// @cond PRIVATE + class TrashRestoreJobPrivate; + Q_DECLARE_PRIVATE(TrashRestoreJob) + /// @endcond +}; + +} + diff --git a/src/core/jobs/unlinkjob.cpp b/src/core/jobs/unlinkjob.cpp new file mode 100644 index 0000000..17782e7 --- /dev/null +++ b/src/core/jobs/unlinkjob.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "unlinkjob.h" + +#include "collection.h" +#include "job_p.h" +#include "linkjobimpl_p.h" + +using namespace Akonadi; + +class Akonadi::UnlinkJobPrivate : public LinkJobImpl +{ +public: + explicit UnlinkJobPrivate(UnlinkJob *parent) + : LinkJobImpl(parent) + { + } +}; + +UnlinkJob::UnlinkJob(const Collection &collection, const Item::List &items, QObject *parent) + : Job(new UnlinkJobPrivate(this), parent) +{ + Q_D(UnlinkJob); + d->destination = collection; + d->objectsToLink = items; +} + +UnlinkJob::~UnlinkJob() +{ +} + +void UnlinkJob::doStart() +{ + Q_D(UnlinkJob); + d->sendCommand(Protocol::LinkItemsCommand::Unlink); +} + +bool UnlinkJob::doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + Q_D(UnlinkJob); + return d->handleResponse(tag, response); +} diff --git a/src/core/jobs/unlinkjob.h b/src/core/jobs/unlinkjob.h new file mode 100644 index 0000000..effc616 --- /dev/null +++ b/src/core/jobs/unlinkjob.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "item.h" +#include "job.h" + +namespace Akonadi +{ +class Collection; +class UnlinkJobPrivate; + +/** + * @short Job that unlinks items inside the Akonadi storage. + * + * This job allows you to remove references to a set of items in a virtual + * collection. + * + * Example: + * + * @code + * + * // Unlink the given items from the given collection + * const Akonadi::Collection virtualCollection = ... + * const Akonadi::Item::List items = ... + * + * Akonadi::UnlinkJob *job = new Akonadi::UnlinkJob( virtualCollection, items ); + * connect( job, SIGNAL(result(KJob*)), SLOT(jobFinished(KJob*)) ); + * + * ... + * + * MyClass::jobFinished( KJob *job ) + * { + * if ( job->error() ) + * qDebug() << "Error occurred"; + * else + * qDebug() << "Unlinked items successfully"; + * } + * + * @endcode + * + * @author Volker Krause + * @since 4.2 + * @see LinkJob + */ +class AKONADICORE_EXPORT UnlinkJob : public Job +{ + Q_OBJECT +public: + /** + * Creates a new unlink job. + * + * The job will remove references to the given items from the given collection. + * + * @param collection The collection from which the references should be removed. + * @param items The items of which the references should be removed. + * @param parent The parent object. + */ + UnlinkJob(const Collection &collection, const Item::List &items, QObject *parent = nullptr); + + /** + * Destroys the unlink job. + */ + ~UnlinkJob() override; + +protected: + void doStart() override; + bool doHandleResponse(qint64 tag, const Protocol::CommandPtr &response) override; + +private: + Q_DECLARE_PRIVATE(UnlinkJob) + template friend class LinkJobImpl; +}; + +} + diff --git a/src/core/kcfg2dbus.xsl b/src/core/kcfg2dbus.xsl new file mode 100644 index 0000000..8a8695b --- /dev/null +++ b/src/core/kcfg2dbus.xsl @@ -0,0 +1,91 @@ + + + + +interfaceName + + + + + + + save + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + s + as + ? + (iiii) + (ii) + ? + (ii) + i + u + b + d + ((iii)(iiii)i) + x + t + ai + i + s + as + s + s + as + v + + + + + + QRect + QSize + QPoint + QDateTime + QList<int> + + + + + diff --git a/src/core/mimetypechecker.cpp b/src/core/mimetypechecker.cpp new file mode 100644 index 0000000..605fcc5 --- /dev/null +++ b/src/core/mimetypechecker.cpp @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "mimetypechecker.h" + +#include "mimetypechecker_p.h" + +#include "collection.h" +#include "item.h" + +using namespace Akonadi; + +MimeTypeChecker::MimeTypeChecker() +{ + d = new MimeTypeCheckerPrivate(); +} + +MimeTypeChecker::MimeTypeChecker(const MimeTypeChecker &other) + : d(other.d) +{ +} + +MimeTypeChecker::~MimeTypeChecker() +{ +} + +MimeTypeChecker &MimeTypeChecker::operator=(const MimeTypeChecker &other) +{ + if (&other != this) { + d = other.d; + } + + return *this; +} + +QStringList MimeTypeChecker::wantedMimeTypes() const +{ + return d->mWantedMimeTypes.values(); +} + +bool MimeTypeChecker::hasWantedMimeTypes() const +{ + return !d->mWantedMimeTypes.isEmpty(); +} + +void MimeTypeChecker::setWantedMimeTypes(const QStringList &mimeTypes) +{ + d->mWantedMimeTypes = QSet(mimeTypes.begin(), mimeTypes.end()); +} + +void MimeTypeChecker::addWantedMimeType(const QString &mimeType) +{ + d->mWantedMimeTypes.insert(mimeType); +} + +void MimeTypeChecker::removeWantedMimeType(const QString &mimeType) +{ + d->mWantedMimeTypes.remove(mimeType); +} + +bool MimeTypeChecker::isWantedItem(const Item &item) const +{ + if (d->mWantedMimeTypes.isEmpty() || !item.isValid()) { + return false; + } + + const QString mimeType = item.mimeType(); + if (mimeType.isEmpty()) { + return false; + } + + return d->isWantedMimeType(mimeType); +} + +bool MimeTypeChecker::isWantedCollection(const Collection &collection) const +{ + if (d->mWantedMimeTypes.isEmpty() || !collection.isValid()) { + return false; + } + + const QStringList contentMimeTypes = collection.contentMimeTypes(); + if (contentMimeTypes.isEmpty()) { + return false; + } + + for (const QString &mimeType : contentMimeTypes) { + if (mimeType.isEmpty()) { + continue; + } + + if (d->isWantedMimeType(mimeType)) { + return true; + } + } + + return false; +} + +bool MimeTypeChecker::isWantedItem(const Item &item, const QString &wantedMimeType) +{ + if (wantedMimeType.isEmpty() || !item.isValid()) { + return false; + } + + const QString mimeType = item.mimeType(); + if (mimeType.isEmpty()) { + return false; + } + + if (mimeType == wantedMimeType) { + return true; + } + + QMimeDatabase db; + const QMimeType mt = db.mimeTypeForName(mimeType); + if (!mt.isValid()) { + return false; + } + + return mt.inherits(wantedMimeType); +} + +bool MimeTypeChecker::isWantedCollection(const Collection &collection, const QString &wantedMimeType) +{ + if (wantedMimeType.isEmpty() || !collection.isValid()) { + return false; + } + + const QStringList contentMimeTypes = collection.contentMimeTypes(); + if (contentMimeTypes.isEmpty()) { + return false; + } + + for (const QString &mimeType : contentMimeTypes) { + if (mimeType.isEmpty()) { + continue; + } + + if (mimeType == wantedMimeType) { + return true; + } + + QMimeDatabase db; + const QMimeType mt = db.mimeTypeForName(mimeType); + if (!mt.isValid()) { + continue; + } + + if (mt.inherits(wantedMimeType)) { + return true; + } + } + + return false; +} + +bool MimeTypeChecker::isWantedMimeType(const QString &mimeType) const +{ + return d->isWantedMimeType(mimeType); +} + +bool MimeTypeChecker::containsWantedMimeType(const QStringList &mimeTypes) const +{ + for (const QString &mt : mimeTypes) { + if (d->isWantedMimeType(mt)) { + return true; + } + } + return false; +} diff --git a/src/core/mimetypechecker.h b/src/core/mimetypechecker.h new file mode 100644 index 0000000..c444093 --- /dev/null +++ b/src/core/mimetypechecker.h @@ -0,0 +1,254 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +class QString; +#include + +namespace Akonadi +{ +class Collection; +class Item; +class MimeTypeCheckerPrivate; + +/** + * @short Helper for checking MIME types of Collections and Items. + * + * When it is necessary to decide whether an item has a certain MIME type + * or whether a collection can contain a certain MIME type, direct string + * comparison might not render the desired result because MIME types can + * have aliases and be a node in an "inheritance" hierarchy. + * + * For example a check like this + * @code + * if ( item.mimeType() == QLatin1String( "text/directory" ) ) + * @endcode + * would fail to detect @c "text/x-vcard" as being the same MIME type. + * + * @note KDE deals with this inside the KMimeType framework, this class is just + * a convenience helper for common Akonadi related checks. + * + * Example: Checking whether an Akonadi::Item is contact MIME type + * @code + * Akonadi::MimeTypeChecker checker; + * checker.addWantedMimeType( KContacts::Addressee::mimeType() ); + * + * if ( checker.isWantedItem( item ) ){ + * // item.mimeType() is equal KContacts::Addressee::mimeType(), an aliases + * // or a sub type. + * } + * @endcode + * + * Example: Checking whether an Akonadi::Collection could contain calendar + * items + * @code + * Akonadi::MimeTypeChecker checker; + * checker.addWantedMimeType( QLatin1String( "text/calendar" ) ); + * + * if ( checker.isWantedCollection( collection ) ) { + * // collection.contentMimeTypes() contains @c "text/calendar" + * // or a sub type. + * } + * @endcode + * + * Example: Checking whether an Akonadi::Collection could contain + * Calendar Event items (i.e. KCal::Event), making use of the respective + * MIME type "subclassing" provided by Akonadi's MIME type extensions. + * @code + * Akonadi::MimeTypeChecker checker; + * checker.addWantedMimeType( QLatin1String( "application/x-vnd.akonadi.calendar.event" ) ); + * + * if ( checker.isWantedCollection( collection ) ) { + * // collection.contentMimeTypes() contains @c "application/x-vnd.akonadi.calendar.event" + * // or a sub type, but just containing @c "text/calendar" would not + * // get here + * } + * @endcode + * + * Example: Checking for items of more than one MIME type and treat one + * of them specially. + * @code + * Akonadi::MimeTypeChecker mimeFilter; + * mimeFilter.setWantedMimeTypes( QStringList() << KContacts::Addressee::mimeType() + * << KContacts::ContactGroup::mimeType() ); + * + * if ( mimeFilter.isWantedItem( item ) ) { + * if ( Akonadi::MimeTypeChecker::isWantedItem( item, KContacts::ContactGroup::mimeType() ) { + * // treat contact group's differently + * } + * } + * @endcode + * + * This class is implicitly shared. + * + * @author Kevin Krammer + * + * @since 4.3 + */ +class AKONADICORE_EXPORT MimeTypeChecker +{ +public: + /** + * Creates an empty MIME type checker. + * + * An empty checker will not report any items or collections as wanted. + */ + MimeTypeChecker(); + + /** + * Creates a new MIME type checker from an @p other. + */ + MimeTypeChecker(const MimeTypeChecker &other); + + /** + * Destroys the MIME type checker. + */ + ~MimeTypeChecker(); + + /** + * Assigns the @p other to this checker and returns a reference to this checker. + */ + MimeTypeChecker &operator=(const MimeTypeChecker &other); + + /** + * Returns the list of wanted MIME types this instance checks against. + * + * @note Don't use this just to check whether there are any wanted mimetypes. + * It is much faster to call @c hasWantedMimeTypes() instead for that purpose. + * + * @see setWantedMimeTypes(), hasWantedMimeTypes() + */ + Q_REQUIRED_RESULT QStringList wantedMimeTypes() const; + + /** + * Checks whether any wanted MIME types are set. + * + * @return @c true if any wanted MIME types are set, false otherwise. + * + * @since 5.6.43 + */ + Q_REQUIRED_RESULT bool hasWantedMimeTypes() const; + + /** + * Sets the list of wanted MIME types this instance checks against. + * + * @param mimeTypes The list of MIME types to check against. + * + * @see wantedMimeTypes() + */ + void setWantedMimeTypes(const QStringList &mimeTypes); + + /** + * Adds another MIME type to the list of wanted MIME types this instance checks against. + * + * @param mimeType The MIME types to add to the checklist. + * + * @see setWantedMimeTypes() + */ + void addWantedMimeType(const QString &mimeType); + + /** + * Removes a MIME type from the list of wanted MIME types this instance checks against. + * + * @param mimeType The MIME type to remove from the checklist. + * + * @see addWantedMimeType() + */ + void removeWantedMimeType(const QString &mimeType); + + /** + * Checks whether a given @p item has one of the wanted MIME types + * + * @param item The item to check the MIME type of. + * + * @return @c true if the @p item MIME type is one of the wanted ones, + * @c false if it isn't, the item is invalid or has an empty MIME type. + * + * @see setWantedMimeTypes() + * @see Item::mimeType() + */ + Q_REQUIRED_RESULT bool isWantedItem(const Item &item) const; + + /** + * Checks whether a given @p collection has one of the wanted MIME types + * + * @param collection The collection to check the content MIME types of. + * + * @return @c true if one of the @p collection content MIME types is + * one of the wanted ones, @c false if non is, the collection + * is invalid or has an empty content MIME type list. + * + * @see setWantedMimeTypes() + * @see Collection::contentMimeTypes() + */ + Q_REQUIRED_RESULT bool isWantedCollection(const Collection &collection) const; + + /** + * Checks whether a given mime type is covered by one of the wanted MIME types. + * + * @param mimeType The mime type to check. + * + * @return @c true if the mime type @p mimeType is coverd by one of the + * wanted MIME types, @c false otherwise. + * + * @since 4.6 + */ + Q_REQUIRED_RESULT bool isWantedMimeType(const QString &mimeType) const; + + /** + * Checks whether any of the given MIME types is covered by one of the wanted MIME types. + * + * @param mimeTypes The MIME types to check. + * + * @return @c true if any of the MIME types in @p mimeTypes is coverd by one of the + * wanted MIME types, @c false otherwise. + * + * @since 4.6 + */ + Q_REQUIRED_RESULT bool containsWantedMimeType(const QStringList &mimeTypes) const; + + /** + * Checks whether a given @p item has the given wanted MIME type + * + * @param item The item to check the MIME type of. + * @param wantedMimeType The MIME type to check against. + * + * @return @c true if the @p item MIME type is the given one, + * @c false if it isn't, the item is invalid or has an empty MIME type. + * + * @see setWantedMimeTypes() + * @see Item::mimeType() + */ + Q_REQUIRED_RESULT static bool isWantedItem(const Item &item, const QString &wantedMimeType); + + /** + * Checks whether a given @p collection has the given MIME type + * + * @param collection The collection to check the content MIME types of. + * @param wantedMimeType The MIME type to check against. + * + * @return @c true if one of the @p collection content MIME types is + * the given wanted one, @c false if it isn't, the collection + * is invalid or has an empty content MIME type list. + * + * @see setWantedMimeTypes() + * @see Collection::contentMimeTypes() + */ + Q_REQUIRED_RESULT static bool isWantedCollection(const Collection &collection, const QString &wantedMimeType); + +private: + /// @cond PRIVATE + QSharedDataPointer d; + /// @endcond +}; + +} + diff --git a/src/core/mimetypechecker_p.h b/src/core/mimetypechecker_p.h new file mode 100644 index 0000000..8d7c12b --- /dev/null +++ b/src/core/mimetypechecker_p.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Krammer + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +namespace Akonadi +{ +/** + * @internal + */ +class MimeTypeCheckerPrivate : public QSharedData +{ +public: + MimeTypeCheckerPrivate() + { + } + + MimeTypeCheckerPrivate(const MimeTypeCheckerPrivate &other) + : QSharedData(other) + { + mWantedMimeTypes = other.mWantedMimeTypes; + } + + bool isWantedMimeType(const QString &mimeType) const + { + if (mWantedMimeTypes.contains(mimeType)) { + return true; + } + + QMimeDatabase db; + const QMimeType mt = db.mimeTypeForName(mimeType); + if (!mt.isValid()) { + return false; + } + + for (const QString &wantedMimeType : std::as_const(mWantedMimeTypes)) { + if (mt.inherits(wantedMimeType)) { + return true; + } + } + + return false; + } + +public: + QSet mWantedMimeTypes; +}; + +} + diff --git a/src/core/models/agentfilterproxymodel.cpp b/src/core/models/agentfilterproxymodel.cpp new file mode 100644 index 0000000..0848d05 --- /dev/null +++ b/src/core/models/agentfilterproxymodel.cpp @@ -0,0 +1,146 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentfilterproxymodel.h" + +#include "agentinstancemodel.h" +#include "agenttypemodel.h" + +#include +#include +#include + +using namespace Akonadi; + +// ensure the role numbers are equivalent for both source models +static_assert(static_cast(AgentTypeModel::CapabilitiesRole) == static_cast(AgentInstanceModel::CapabilitiesRole), + "AgentTypeModel::CapabilitiesRole does not match AgentInstanceModel::CapabilitiesRole"); +static_assert(static_cast(AgentTypeModel::MimeTypesRole) == static_cast(AgentInstanceModel::MimeTypesRole), + "AgentTypeModel::MimeTypesRole does not match AgentInstanceModel::MimeTypesRole"); + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentFilterProxyModel::Private +{ +public: + QStringList mimeTypes; + QStringList capabilities; + QStringList excludeCapabilities; + bool filterAcceptRegExp(const QModelIndex &index, const QRegularExpression &filterRegExpStr); +}; + +AgentFilterProxyModel::AgentFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private) +{ + setDynamicSortFilter(true); +} + +AgentFilterProxyModel::~AgentFilterProxyModel() +{ + delete d; +} + +void AgentFilterProxyModel::addMimeTypeFilter(const QString &mimeType) +{ + d->mimeTypes << mimeType; + invalidateFilter(); +} + +void AgentFilterProxyModel::addCapabilityFilter(const QString &capability) +{ + d->capabilities << capability; + invalidateFilter(); +} + +void AgentFilterProxyModel::excludeCapabilities(const QString &capability) +{ + d->excludeCapabilities << capability; + invalidateFilter(); +} + +void AgentFilterProxyModel::clearFilters() +{ + d->capabilities.clear(); + d->mimeTypes.clear(); + d->excludeCapabilities.clear(); + invalidateFilter(); +} + +bool AgentFilterProxyModel::Private::filterAcceptRegExp(const QModelIndex &index, const QRegularExpression &filterRegExpStr) +{ + // First see if the name matches a set regexp filter. + if (!filterRegExpStr.pattern().isEmpty()) { + return index.data(AgentTypeModel::IdentifierRole).toString().contains(filterRegExpStr) || index.data().toString().contains(filterRegExpStr); + } + return true; +} + +bool AgentFilterProxyModel::filterAcceptsRow(int row, const QModelIndex & /*source_parent*/) const +{ + const QModelIndex index = sourceModel()->index(row, 0); + + if (!d->mimeTypes.isEmpty()) { + QMimeDatabase mimeDb; + bool found = false; + const QStringList lst = index.data(AgentTypeModel::MimeTypesRole).toStringList(); + for (const QString &mimeType : lst) { + if (d->mimeTypes.contains(mimeType)) { + found = true; + } else { + const QMimeType mt = mimeDb.mimeTypeForName(mimeType); + if (mt.isValid()) { + for (const QString &type : std::as_const(d->mimeTypes)) { + if (mt.inherits(type)) { + found = true; + break; + } + } + } + } + + if (found) { + break; + } + } + + if (!found) { + return false; + } + } + + if (!d->capabilities.isEmpty()) { + bool found = false; + const QStringList lst = index.data(AgentTypeModel::CapabilitiesRole).toStringList(); + for (const QString &capability : lst) { + if (d->capabilities.contains(capability)) { + found = true; + break; + } + } + + if (!found) { + return false; + } + + if (found && !d->excludeCapabilities.isEmpty()) { + const QStringList lst = index.data(AgentTypeModel::CapabilitiesRole).toStringList(); + for (const QString &capability : lst) { + if (d->excludeCapabilities.contains(capability)) { + found = false; + break; + } + } + + if (!found) { + return false; + } + } + } + + return d->filterAcceptRegExp(index, filterRegularExpression()); +} diff --git a/src/core/models/agentfilterproxymodel.h b/src/core/models/agentfilterproxymodel.h new file mode 100644 index 0000000..ed5be04 --- /dev/null +++ b/src/core/models/agentfilterproxymodel.h @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ +/** + * @short A proxy model for filtering AgentType or AgentInstance + * + * This filter proxy model works on top of a AgentTypeModel or AgentInstanceModel + * and can be used to show only AgentType or AgentInstance objects + * which provide a given mime type or capability. + * + * @code + * + * // Show only running agent instances that provide contacts + * Akonadi::AgentInstanceModel *model = new Akonadi::AgentInstanceModel( this ); + * + * Akonadi::AgentFilterProxyModel *proxy = new Akonadi::AgentFilterProxyModel( this ); + * proxy->addMimeTypeFilter( "text/directory" ); + * + * proxy->setSourceModel( model ); + * + * QListView *view = new QListView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT AgentFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + /** + * Create a new agent filter proxy model. + * By default no filtering is done. + * @param parent parent object + */ + explicit AgentFilterProxyModel(QObject *parent = nullptr); + + /** + * Destroys the agent filter proxy model. + */ + ~AgentFilterProxyModel() override; + + /** + * Accept agents supporting @p mimeType. + */ + void addMimeTypeFilter(const QString &mimeType); + + /** + * Accept agents with the given @p capability. + */ + void addCapabilityFilter(const QString &capability); + + /** + * Clear the filters ( mimeTypes & capabilities ). + */ + void clearFilters(); + + /** + * Excludes agents with the given @p capability. + * @param capability undesired agent capability + * @since 4.6 + */ + void excludeCapabilities(const QString &capability); + +protected: + bool filterAcceptsRow(int row, const QModelIndex &parent) const override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/models/agentinstancemodel.cpp b/src/core/models/agentinstancemodel.cpp new file mode 100644 index 0000000..84fe7df --- /dev/null +++ b/src/core/models/agentinstancemodel.cpp @@ -0,0 +1,243 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstancemodel.h" + +#include "agentinstance.h" +#include "agentmanager.h" + +#include + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentInstanceModel::Private +{ +public: + explicit Private(AgentInstanceModel *parent) + : mParent(parent) + { + } + + AgentInstanceModel *const mParent; + AgentInstance::List mInstances; + + void instanceAdded(const AgentInstance & /*instance*/); + void instanceRemoved(const AgentInstance & /*instance*/); + void instanceChanged(const AgentInstance & /*instance*/); +}; + +void AgentInstanceModel::Private::instanceAdded(const AgentInstance &instance) +{ + mParent->beginInsertRows(QModelIndex(), mInstances.count(), mInstances.count()); + mInstances.append(instance); + mParent->endInsertRows(); +} + +void AgentInstanceModel::Private::instanceRemoved(const AgentInstance &instance) +{ + const int index = mInstances.indexOf(instance); + if (index == -1) { + return; + } + + mParent->beginRemoveRows(QModelIndex(), index, index); + mInstances.removeAll(instance); + mParent->endRemoveRows(); +} + +void AgentInstanceModel::Private::instanceChanged(const AgentInstance &instance) +{ + const int numberOfInstance(mInstances.count()); + for (int i = 0; i < numberOfInstance; ++i) { + if (mInstances[i] == instance) { + // TODO why reassign it if equals ? + mInstances[i] = instance; + + const QModelIndex idx = mParent->index(i, 0); + Q_EMIT mParent->dataChanged(idx, idx); + + return; + } + } +} + +AgentInstanceModel::AgentInstanceModel(QObject *parent) + : QAbstractItemModel(parent) + , d(new Private(this)) +{ + d->mInstances = AgentManager::self()->instances(); + + connect(AgentManager::self(), &AgentManager::instanceAdded, this, [this](const Akonadi::AgentInstance &inst) { + d->instanceAdded(inst); + }); + connect(AgentManager::self(), &AgentManager::instanceRemoved, this, [this](const Akonadi::AgentInstance &inst) { + d->instanceRemoved(inst); + }); + connect(AgentManager::self(), &AgentManager::instanceStatusChanged, this, [this](const Akonadi::AgentInstance &inst) { + d->instanceChanged(inst); + }); + connect(AgentManager::self(), &AgentManager::instanceProgressChanged, this, [this](const Akonadi::AgentInstance &inst) { + d->instanceChanged(inst); + }); + connect(AgentManager::self(), &AgentManager::instanceNameChanged, this, [this](const Akonadi::AgentInstance &inst) { + d->instanceChanged(inst); + }); + connect(AgentManager::self(), &AgentManager::instanceOnline, this, [this](const Akonadi::AgentInstance &inst) { + d->instanceChanged(inst); + }); +} + +AgentInstanceModel::~AgentInstanceModel() +{ + delete d; +} + +QHash AgentInstanceModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles.insert(StatusRole, "status"); + roles.insert(StatusMessageRole, "statusMessage"); + roles.insert(ProgressRole, "progress"); + roles.insert(OnlineRole, "online"); + return roles; +} + +int AgentInstanceModel::columnCount(const QModelIndex &index) const +{ + return index.isValid() ? 0 : 1; +} + +int AgentInstanceModel::rowCount(const QModelIndex &index) const +{ + return index.isValid() ? 0 : d->mInstances.count(); +} + +QVariant AgentInstanceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() < 0 || index.row() >= d->mInstances.count()) { + return QVariant(); + } + + const AgentInstance &instance = d->mInstances[index.row()]; + + switch (role) { + case Qt::DisplayRole: + return instance.name(); + case Qt::DecorationRole: + return instance.type().icon(); + case InstanceRole: { + QVariant var; + var.setValue(instance); + return var; + } + case InstanceIdentifierRole: + return instance.identifier(); + case Qt::ToolTipRole: + return QStringLiteral("

%1

%2
").arg(instance.name(), instance.type().description()); + case StatusRole: + return instance.status(); + case StatusMessageRole: + return instance.statusMessage(); + case ProgressRole: + return instance.progress(); + case OnlineRole: + return instance.isOnline(); + case TypeRole: { + QVariant var; + var.setValue(instance.type()); + return var; + } + case TypeIdentifierRole: + return instance.type().identifier(); + case DescriptionRole: + return instance.type().description(); + case CapabilitiesRole: + return instance.type().capabilities(); + case MimeTypesRole: + return instance.type().mimeTypes(); + } + return QVariant(); +} + +QVariant AgentInstanceModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (role != Qt::DisplayRole) { + return QVariant(); + } + + switch (section) { + case 0: + return i18nc("@title:column, name of a thing", "Name"); + default: + return QVariant(); + } +} + +QModelIndex AgentInstanceModel::index(int row, int column, const QModelIndex & /*parent*/) const +{ + if (row < 0 || row >= d->mInstances.count()) { + return QModelIndex(); + } + + if (column != 0) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex AgentInstanceModel::parent(const QModelIndex & /*child*/) const +{ + return QModelIndex(); +} + +Qt::ItemFlags AgentInstanceModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= d->mInstances.count()) { + return QAbstractItemModel::flags(index); + } + + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; +} + +bool AgentInstanceModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + if (index.row() < 0 || index.row() >= d->mInstances.count()) { + return false; + } + + AgentInstance &instance = d->mInstances[index.row()]; + + switch (role) { + case OnlineRole: + instance.setIsOnline(value.toBool()); + Q_EMIT dataChanged(index, index); + return true; + default: + return false; + } + + return false; +} + +#include "moc_agentinstancemodel.cpp" diff --git a/src/core/models/agentinstancemodel.h b/src/core/models/agentinstancemodel.h new file mode 100644 index 0000000..39313cd --- /dev/null +++ b/src/core/models/agentinstancemodel.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Provides a data model for agent instances. + * + * This class provides the interface of a QAbstractItemModel to + * access all available agent instances: their name, identifier, + * supported mimetypes and capabilities. + * + * @code + * + * Akonadi::AgentInstanceModel *model = new Akonadi::AgentInstanceModel( this ); + * + * QListView *view = new QListView( this ); + * view->setModel( model ); + * + * @endcode + * + * To show only agent instances that match a given mime type or special + * capabilities, use the AgentFilterProxyModel on top of this model. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentInstanceModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * Describes the roles of this model. + */ + enum Roles { + TypeRole = Qt::UserRole + 1, ///< The agent type itself + TypeIdentifierRole, ///< The identifier of the agent type + DescriptionRole, ///< A description of the agent type + MimeTypesRole, ///< A list of supported mimetypes + CapabilitiesRole, ///< A list of supported capabilities + InstanceRole, ///< The agent instance itself + InstanceIdentifierRole, ///< The identifier of the agent instance + StatusRole, ///< The current status (numerical) of the instance + StatusMessageRole, ///< A textual presentation of the current status + ProgressRole, ///< The current progress (numerical in percent) of an operation + OnlineRole, ///< The current online/offline status + UserRole = Qt::UserRole + 42 ///< Role for user extensions + }; + + /** + * Creates a new agent instance model. + * + * @param parent The parent object. + */ + explicit AgentInstanceModel(QObject *parent = nullptr); + + /** + * Destroys the agent instance model. + */ + ~AgentInstanceModel() override; + + Q_REQUIRED_RESULT QHash roleNames() const override; + Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &index) const override; + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + Q_REQUIRED_RESULT bool setData(const QModelIndex &index, const QVariant &value, int role) override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/models/agenttypemodel.cpp b/src/core/models/agenttypemodel.cpp new file mode 100644 index 0000000..36c7005 --- /dev/null +++ b/src/core/models/agenttypemodel.cpp @@ -0,0 +1,153 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agenttypemodel.h" +#include "agentmanager.h" +#include "agenttype.h" + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentTypeModel::Private +{ +public: + explicit Private(AgentTypeModel *parent) + : mParent(parent) + { + mTypes = AgentManager::self()->types(); + } + + AgentTypeModel *const mParent; + AgentType::List mTypes; + + void typeAdded(const AgentType &agentType); + void typeRemoved(const AgentType &agentType); +}; + +void AgentTypeModel::Private::typeAdded(const AgentType &agentType) +{ + mTypes.append(agentType); + + Q_EMIT mParent->layoutChanged(); +} + +void AgentTypeModel::Private::typeRemoved(const AgentType &agentType) +{ + mTypes.removeAll(agentType); + + Q_EMIT mParent->layoutChanged(); +} + +AgentTypeModel::AgentTypeModel(QObject *parent) + : QAbstractItemModel(parent) + , d(new Private(this)) +{ + connect(AgentManager::self(), &AgentManager::typeAdded, this, [this](const Akonadi::AgentType &type) { + d->typeAdded(type); + }); + connect(AgentManager::self(), &AgentManager::typeRemoved, this, [this](const Akonadi::AgentType &type) { + d->typeRemoved(type); + }); +} + +AgentTypeModel::~AgentTypeModel() +{ + delete d; +} + +int AgentTypeModel::columnCount(const QModelIndex & /*parent*/) const +{ + return 1; +} + +int AgentTypeModel::rowCount(const QModelIndex & /*parent*/) const +{ + return d->mTypes.count(); +} + +QHash AgentTypeModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles.insert(TypeRole, QByteArrayLiteral("type")); + roles.insert(IdentifierRole, QByteArrayLiteral("identifier")); + roles.insert(DescriptionRole, QByteArrayLiteral("description")); + roles.insert(MimeTypesRole, QByteArrayLiteral("mimeTypes")); + roles.insert(CapabilitiesRole, QByteArrayLiteral("capabilities")); + return roles; +} + +QVariant AgentTypeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() < 0 || index.row() >= d->mTypes.count()) { + return QVariant(); + } + + const AgentType &type = d->mTypes[index.row()]; + + switch (role) { + case Qt::DisplayRole: + return type.name(); + case Qt::DecorationRole: + return type.icon(); + case TypeRole: { + QVariant var; + var.setValue(type); + return var; + } + case IdentifierRole: + return type.identifier(); + case DescriptionRole: + return type.description(); + case MimeTypesRole: + return type.mimeTypes(); + case CapabilitiesRole: + return type.capabilities(); + default: + break; + } + return QVariant(); +} + +QModelIndex AgentTypeModel::index(int row, int column, const QModelIndex & /*parent*/) const +{ + if (row < 0 || row >= d->mTypes.count()) { + return QModelIndex(); + } + + if (column != 0) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex AgentTypeModel::parent(const QModelIndex & /*child*/) const +{ + return QModelIndex(); +} + +Qt::ItemFlags AgentTypeModel::flags(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= d->mTypes.count()) { + return QAbstractItemModel::flags(index); + } + + const AgentType &type = d->mTypes[index.row()]; + if (type.capabilities().contains(QLatin1String("Unique")) && AgentManager::self()->instance(type.identifier()).isValid()) { + return QAbstractItemModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + return QAbstractItemModel::flags(index); +} + +#include "moc_agenttypemodel.cpp" diff --git a/src/core/models/agenttypemodel.h b/src/core/models/agenttypemodel.h new file mode 100644 index 0000000..fc6533f --- /dev/null +++ b/src/core/models/agenttypemodel.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Provides a data model for agent types. + * + * This class provides the interface of a QAbstractItemModel to + * access all available agent types: their name, identifier, + * supported mimetypes and capabilities. + * + * @code + * + * Akonadi::AgentTypeModel *model = new Akonadi::AgentTypeModel( this ); + * + * QListView *view = new QListView( this ); + * view->setModel( model ); + * + * @endcode + * + * To show only agent types that match a given mime type or special + * capabilities, use the AgentFilterProxyModel on top of this model. + * + * @author Tobias Koenig + */ +class AKONADICORE_EXPORT AgentTypeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * Describes the roles of this model. + */ + enum Roles { + TypeRole = Qt::UserRole + 1, ///< The agent type itself + IdentifierRole, ///< The identifier of the agent type + DescriptionRole, ///< A description of the agent type + MimeTypesRole, ///< A list of supported mimetypes + CapabilitiesRole, ///< A list of supported capabilities + UserRole = Qt::UserRole + 42 ///< Role for user extensions + }; + + /** + * Creates a new agent type model. + */ + explicit AgentTypeModel(QObject *parent = nullptr); + + /** + * Destroys the agent type model. + */ + ~AgentTypeModel() override; + + Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &index) const override; + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + Q_REQUIRED_RESULT QHash roleNames() const override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/models/collectionfilterproxymodel.cpp b/src/core/models/collectionfilterproxymodel.cpp new file mode 100644 index 0000000..3858e56 --- /dev/null +++ b/src/core/models/collectionfilterproxymodel.cpp @@ -0,0 +1,165 @@ +/* + SPDX-FileCopyrightText: 2007 Bruno Virlet + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionfilterproxymodel.h" +#include "akonadicore_debug.h" +#include "entitytreemodel.h" +#include "mimetypechecker.h" + +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionFilterProxyModel::Private +{ +public: + explicit Private(CollectionFilterProxyModel *parent) + : mParent(parent) + { + mimeChecker.addWantedMimeType(QStringLiteral("text/uri-list")); + } + + bool collectionAccepted(const QModelIndex &index, bool checkResourceVisibility = true); + + QVector acceptedResources; + CollectionFilterProxyModel *const mParent; + MimeTypeChecker mimeChecker; + bool mExcludeVirtualCollections = false; +}; + +bool CollectionFilterProxyModel::Private::collectionAccepted(const QModelIndex &index, bool checkResourceVisibility) +{ + // Retrieve supported mimetypes + const auto collection = mParent->sourceModel()->data(index, EntityTreeModel::CollectionRole).value(); + + if (!collection.isValid()) { + return false; + } + + if (collection.isVirtual() && mExcludeVirtualCollections) { + return false; + } + + // If this collection directly contains one valid mimetype, it is accepted + if (mimeChecker.isWantedCollection(collection)) { + // The folder will be accepted, but we need to make sure the resource is visible too. + if (checkResourceVisibility) { + // find the resource + QModelIndex resource = index; + while (resource.parent().isValid()) { + resource = resource.parent(); + } + + // See if that resource is visible, if not, invalidate the filter. + if (resource != index && !acceptedResources.contains(resource)) { + qCDebug(AKONADICORE_LOG) << "We got a new collection:" << mParent->sourceModel()->data(index).toString() + << "but the resource is not visible:" << mParent->sourceModel()->data(resource).toString(); + acceptedResources.clear(); + // defer reset, the model might still be supplying new items at this point which crashs + mParent->invalidateFilter(); + return true; + } + } + + // Keep track of all the resources that are visible. + if (!index.parent().isValid()) { + acceptedResources.append(index); + } + + return true; + } + + // If this collection has a child which contains valid mimetypes, it is accepted + QModelIndex childIndex = mParent->sourceModel()->index(0, 0, index); + while (childIndex.isValid()) { + if (collectionAccepted(childIndex, false /* don't check visibility of the parent, as we are checking the child now */)) { + // Keep track of all the resources that are visible. + if (!index.parent().isValid()) { + acceptedResources.append(index); + } + + return true; + } + childIndex = childIndex.sibling(childIndex.row() + 1, 0); + } + + // Or else, no reason to keep this collection. + return false; +} + +CollectionFilterProxyModel::CollectionFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d(new Private(this)) +{ +} + +CollectionFilterProxyModel::~CollectionFilterProxyModel() +{ + delete d; +} + +void CollectionFilterProxyModel::addMimeTypeFilters(const QStringList &typeList) +{ + const QStringList mimeTypes = d->mimeChecker.wantedMimeTypes() + typeList; + d->mimeChecker.setWantedMimeTypes(mimeTypes); + invalidateFilter(); +} + +void CollectionFilterProxyModel::addMimeTypeFilter(const QString &type) +{ + d->mimeChecker.addWantedMimeType(type); + invalidateFilter(); +} + +bool CollectionFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + return d->collectionAccepted(sourceModel()->index(sourceRow, 0, sourceParent)); +} + +QStringList CollectionFilterProxyModel::mimeTypeFilters() const +{ + return d->mimeChecker.wantedMimeTypes(); +} + +void CollectionFilterProxyModel::clearFilters() +{ + d->mimeChecker = MimeTypeChecker(); + invalidateFilter(); +} + +void CollectionFilterProxyModel::setExcludeVirtualCollections(bool exclude) +{ + if (exclude != d->mExcludeVirtualCollections) { + d->mExcludeVirtualCollections = exclude; + invalidateFilter(); + } +} + +bool CollectionFilterProxyModel::excludeVirtualCollections() const +{ + return d->mExcludeVirtualCollections; +} + +Qt::ItemFlags CollectionFilterProxyModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + // Don't crash + return Qt::NoItemFlags; + } + + const auto collection = sourceModel()->data(mapToSource(index), EntityTreeModel::CollectionRole).value(); + + // If this collection directly contains one valid mimetype, it is accepted + if (d->mimeChecker.isWantedCollection(collection)) { + return QSortFilterProxyModel::flags(index); + } else { + return QSortFilterProxyModel::flags(index) & ~(Qt::ItemIsSelectable); + } +} diff --git a/src/core/models/collectionfilterproxymodel.h b/src/core/models/collectionfilterproxymodel.h new file mode 100644 index 0000000..14d151f --- /dev/null +++ b/src/core/models/collectionfilterproxymodel.h @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2007 Bruno Virlet + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include + +namespace Akonadi +{ +class CollectionModel; + +/** + * @short A proxy model that filters collections by mime type. + * + * This class can be used on top of a CollectionModel to filter out + * all collections that doesn't match a given mime type. + * + * For instance, a mail application will use addMimeType( "message/rfc822" ) to only show + * collections containing mail. + * + * @code + * + * Akonadi::CollectionModel *model = new Akonadi::CollectionModel( this ); + * + * Akonadi::CollectionFilterProxyModel *proxy = new Akonadi::CollectionFilterProxyModel(); + * proxy->addMimeTypeFilter( "message/rfc822" ); + * proxy->setSourceModel( model ); + * + * QTreeView *view = new QTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Bruno Virlet + */ +class AKONADICORE_EXPORT CollectionFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new collection proxy filter model. + * + * @param parent The parent object. + */ + explicit CollectionFilterProxyModel(QObject *parent = nullptr); + + /** + * Destroys the collection proxy filter model. + */ + ~CollectionFilterProxyModel() override; + + /** + * Adds a list of mime types to be shown by the filter. + * + * @param mimeTypes A list of mime types to be shown. + */ + void addMimeTypeFilters(const QStringList &mimeTypes); + + /** + * Adds a mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addMimeTypeFilter(const QString &mimeType); + + /** + * Returns the list of mime type filters. + */ + Q_REQUIRED_RESULT QStringList mimeTypeFilters() const; + + /** + * Sets whether we want virtual collections to be filtered or not. + * By default, virtual collections are accepted. + * + * @param exclude If true, virtual collections aren't accepted. + * + * @since 4.7 + */ + void setExcludeVirtualCollections(bool exclude); + /* + * @since 4.12 + */ + Q_REQUIRED_RESULT bool excludeVirtualCollections() const; + + /** + * Clears all mime type filters. + */ + void clearFilters(); + + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/models/entitymimetypefiltermodel.cpp b/src/core/models/entitymimetypefiltermodel.cpp new file mode 100644 index 0000000..e400b3b --- /dev/null +++ b/src/core/models/entitymimetypefiltermodel.cpp @@ -0,0 +1,235 @@ +/* + SPDX-FileCopyrightText: 2007 Bruno Virlet + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitymimetypefiltermodel.h" +#include "akonadicore_debug.h" +#include "mimetypechecker.h" + +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +/** + * @internal + */ +class EntityMimeTypeFilterModelPrivate +{ +public: + explicit EntityMimeTypeFilterModelPrivate(EntityMimeTypeFilterModel *parent) + : q_ptr(parent) + , m_headerGroup(EntityTreeModel::EntityTreeHeaders) + { + } + + Q_DECLARE_PUBLIC(EntityMimeTypeFilterModel) + EntityMimeTypeFilterModel *q_ptr; + + QStringList includedMimeTypes; + QStringList excludedMimeTypes; + + QPersistentModelIndex m_rootIndex; + + EntityTreeModel::HeaderGroup m_headerGroup; +}; + +} // namespace Akonadi + +EntityMimeTypeFilterModel::EntityMimeTypeFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new EntityMimeTypeFilterModelPrivate(this)) +{ +} + +EntityMimeTypeFilterModel::~EntityMimeTypeFilterModel() +{ + delete d_ptr; +} + +void EntityMimeTypeFilterModel::addMimeTypeInclusionFilters(const QStringList &typeList) +{ + Q_D(EntityMimeTypeFilterModel); + d->includedMimeTypes << typeList; + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::addMimeTypeExclusionFilters(const QStringList &typeList) +{ + Q_D(EntityMimeTypeFilterModel); + d->excludedMimeTypes << typeList; + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::addMimeTypeInclusionFilter(const QString &type) +{ + Q_D(EntityMimeTypeFilterModel); + d->includedMimeTypes << type; + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::addMimeTypeExclusionFilter(const QString &type) +{ + Q_D(EntityMimeTypeFilterModel); + d->excludedMimeTypes << type; + invalidateFilter(); +} + +bool EntityMimeTypeFilterModel::filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const +{ + if (sourceColumn >= columnCount(mapFromSource(sourceParent))) { + return false; + } + return QSortFilterProxyModel::filterAcceptsColumn(sourceColumn, sourceParent); +} + +bool EntityMimeTypeFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const EntityMimeTypeFilterModel); + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + const QString rowMimetype = idx.data(EntityTreeModel::MimeTypeRole).toString(); + + if (d->excludedMimeTypes.contains(rowMimetype)) { + return false; + } + + if (d->includedMimeTypes.isEmpty() || d->includedMimeTypes.contains(rowMimetype)) { + const auto item = idx.data(EntityTreeModel::ItemRole).value(); + + if (item.isValid() && !item.hasPayload()) { + qCDebug(AKONADICORE_LOG) << "Item " << item.id() << " doesn't have payload"; + return false; + } + + return true; + } + + return false; +} + +QStringList EntityMimeTypeFilterModel::mimeTypeInclusionFilters() const +{ + Q_D(const EntityMimeTypeFilterModel); + return d->includedMimeTypes; +} + +QStringList EntityMimeTypeFilterModel::mimeTypeExclusionFilters() const +{ + Q_D(const EntityMimeTypeFilterModel); + return d->excludedMimeTypes; +} + +void EntityMimeTypeFilterModel::clearFilters() +{ + Q_D(EntityMimeTypeFilterModel); + d->includedMimeTypes.clear(); + d->excludedMimeTypes.clear(); + invalidateFilter(); +} + +void EntityMimeTypeFilterModel::setHeaderGroup(EntityTreeModel::HeaderGroup headerGroup) +{ + Q_D(EntityMimeTypeFilterModel); + d->m_headerGroup = headerGroup; +} + +QVariant EntityMimeTypeFilterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (!sourceModel()) { + return QVariant(); + } + + Q_D(const EntityMimeTypeFilterModel); + role += (EntityTreeModel::TerminalUserRole * d->m_headerGroup); + return sourceModel()->headerData(section, orientation, role); +} + +QModelIndexList EntityMimeTypeFilterModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (!sourceModel()) { + return QModelIndexList(); + } + + if (role < Qt::UserRole) { + return QSortFilterProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + const auto matches = sourceModel()->match(mapToSource(start), role, value, hits, flags); + for (const auto &idx : matches) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list.push_back(proxyIndex); + } + } + + return list; +} + +int EntityMimeTypeFilterModel::columnCount(const QModelIndex &parent) const +{ + Q_D(const EntityMimeTypeFilterModel); + + if (!sourceModel()) { + return 0; + } + + const QVariant value = sourceModel()->data(mapToSource(parent), EntityTreeModel::ColumnCountRole + (EntityTreeModel::TerminalUserRole * d->m_headerGroup)); + if (!value.isValid()) { + return 0; + } + + return value.toInt(); +} + +bool EntityMimeTypeFilterModel::hasChildren(const QModelIndex &parent) const +{ + if (!sourceModel()) { + return false; + } + + // QSortFilterProxyModel implementation is buggy in that it emits rowsAboutToBeInserted etc + // only after the source model has emitted rowsInserted, instead of emitting it when the + // source model emits rowsAboutToBeInserted. That means that the source and the proxy are out + // of sync around the time of insertions, so we can't use the optimization below. + return rowCount(parent) > 0; +#if 0 + + if (!parent.isValid()) { + return sourceModel()->hasChildren(parent); + } + + Q_D(const EntityMimeTypeFilterModel); + if (EntityTreeModel::ItemListHeaders == d->m_headerGroup) { + return false; + } + + if (EntityTreeModel::CollectionTreeHeaders == d->m_headerGroup) { + QModelIndex childIndex = parent.child(0, 0); + while (childIndex.isValid()) { + Collection col = childIndex.data(EntityTreeModel::CollectionRole).value(); + if (col.isValid()) { + return true; + } + childIndex = childIndex.sibling(childIndex.row() + 1, childIndex.column()); + } + } + return false; +#endif +} + +bool EntityMimeTypeFilterModel::canFetchMore(const QModelIndex &parent) const +{ + Q_D(const EntityMimeTypeFilterModel); + if (EntityTreeModel::CollectionTreeHeaders == d->m_headerGroup) { + return false; + } + return QSortFilterProxyModel::canFetchMore(parent); +} diff --git a/src/core/models/entitymimetypefiltermodel.h b/src/core/models/entitymimetypefiltermodel.h new file mode 100644 index 0000000..509e9df --- /dev/null +++ b/src/core/models/entitymimetypefiltermodel.h @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2007 Bruno Virlet + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "entitytreemodel.h" + +#include + +namespace Akonadi +{ +class EntityMimeTypeFilterModelPrivate; + +/** + * @short A proxy model that filters entities by mime type. + * + * This class can be used on top of an EntityTreeModel to exclude entities by mimetype + * or to include only certain mimetypes. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( this ); + * + * Akonadi::EntityMimeTypeFilterModel *proxy = new Akonadi::EntityMimeTypeFilterModel(); + * proxy->addMimeTypeInclusionFilter( "message/rfc822" ); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @li If a mimetype is in both the exclusion list and the inclusion list, it is excluded. + * @li If the mimeTypeInclusionFilter is empty, all mimetypes are + * accepted (except if they are in the exclusion filter of course). + * + * + * @author Bruno Virlet + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_EXPORT EntityMimeTypeFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new entity mime type filter model. + * + * @param parent The parent object. + */ + explicit EntityMimeTypeFilterModel(QObject *parent = nullptr); + + /** + * Destroys the entity mime type filter model. + */ + ~EntityMimeTypeFilterModel() override; + + /** + * Add mime types to be shown by the filter. + * + * @param mimeTypes A list of mime types to be included. + */ + void addMimeTypeInclusionFilters(const QStringList &mimeTypes); + + /** + * Add mimetypes to filter out + * + * @param mimeTypes A list to exclude from the model. + */ + void addMimeTypeExclusionFilters(const QStringList &mimeTypes); + + /** + * Add mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addMimeTypeInclusionFilter(const QString &mimeType); + + /** + * Add mime type to be excluded by the filter. + * + * @param mimeType A mime type to be excluded. + */ + void addMimeTypeExclusionFilter(const QString &mimeType); + + /** + * Returns the list of mime type inclusion filters. + */ + Q_REQUIRED_RESULT QStringList mimeTypeInclusionFilters() const; + + /** + * Returns the list of mime type exclusion filters. + */ + Q_REQUIRED_RESULT QStringList mimeTypeExclusionFilters() const; + + /** + * Clear all mime type filters. + */ + void clearFilters(); + + /** + * Sets the header @p set of the filter model. + * @param headerGroup the header to set. + * \sa EntityTreeModel::HeaderGroup + */ + void setHeaderGroup(EntityTreeModel::HeaderGroup headerGroup); + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + Q_REQUIRED_RESULT bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + + Q_REQUIRED_RESULT bool canFetchMore(const QModelIndex &parent) const override; + + Q_REQUIRED_RESULT QModelIndexList match(const QModelIndex &start, + int role, + const QVariant &value, + int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; + + Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool filterAcceptsColumn(int sourceColumn, const QModelIndex &sourceParent) const override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(EntityMimeTypeFilterModel) + EntityMimeTypeFilterModelPrivate *const d_ptr; + /// @endcond +}; + +} + diff --git a/src/core/models/entityorderproxymodel.cpp b/src/core/models/entityorderproxymodel.cpp new file mode 100644 index 0000000..4d767cc --- /dev/null +++ b/src/core/models/entityorderproxymodel.cpp @@ -0,0 +1,320 @@ +/* + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net + SPDX-FileContributor: Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entityorderproxymodel.h" + +#include + +#include +#include + +#include "entitytreemodel.h" +#include "item.h" + +namespace Akonadi +{ +class EntityOrderProxyModelPrivate +{ +public: + explicit EntityOrderProxyModelPrivate(EntityOrderProxyModel *qq) + : q_ptr(qq) + { + } + + void saveOrder(const QModelIndex &index); + + KConfigGroup m_orderConfig; + + Q_DECLARE_PUBLIC(EntityOrderProxyModel) + EntityOrderProxyModel *const q_ptr; +}; + +} // namespace Akonadi + +using namespace Akonadi; + +EntityOrderProxyModel::EntityOrderProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new EntityOrderProxyModelPrivate(this)) +{ + setRecursiveFilteringEnabled(true); + setDynamicSortFilter(true); + // setSortCaseSensitivity( Qt::CaseInsensitive ); +} + +EntityOrderProxyModel::~EntityOrderProxyModel() +{ + delete d_ptr; +} + +void EntityOrderProxyModel::setOrderConfig(const KConfigGroup &configGroup) +{ + Q_D(EntityOrderProxyModel); + Q_EMIT layoutAboutToBeChanged(); + d->m_orderConfig = configGroup; + Q_EMIT layoutChanged(); +} + +// reimplemented in FavoriteCollectionOrderProxyModel +Collection EntityOrderProxyModel::parentCollection(const QModelIndex &index) const +{ + return index.data(EntityTreeModel::ParentCollectionRole).value(); +} + +static QString configKey(const Collection &col) +{ + return !col.isValid() ? QStringLiteral("0") : QString::number(col.id()); +} + +bool EntityOrderProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + Q_D(const EntityOrderProxyModel); + + if (!d->m_orderConfig.isValid()) { + return QSortFilterProxyModel::lessThan(left, right); + } + const Collection col = parentCollection(left); + + const QStringList list = d->m_orderConfig.readEntry(configKey(col), QStringList()); + + if (list.isEmpty()) { + return QSortFilterProxyModel::lessThan(left, right); + } + + const QString leftValue = configString(left); + const QString rightValue = configString(right); + + const int leftPosition = list.indexOf(leftValue); + const int rightPosition = list.indexOf(rightValue); + + if (leftPosition < 0 || rightPosition < 0) { + return QSortFilterProxyModel::lessThan(left, right); + } + + return leftPosition < rightPosition; +} + +QStringList EntityOrderProxyModel::configStringsForDroppedUrls(const QList &urls, const Akonadi::Collection &parentCol, bool *containsMove) const +{ + QStringList droppedList; + droppedList.reserve(urls.count()); + for (const QUrl &url : urls) { + Collection col = Collection::fromUrl(url); + + if (!col.isValid()) { + Item item = Item::fromUrl(url); + if (!item.isValid()) { + continue; + } + + const QModelIndexList list = EntityTreeModel::modelIndexesForItem(this, item); + if (list.isEmpty()) { + continue; + } + + if (!*containsMove && parentCollection(list.first()).id() != parentCol.id()) { + *containsMove = true; + } + + droppedList << configString(list.first()); + } else { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(this, col); + if (!idx.isValid()) { + continue; + } + + if (!*containsMove && parentCollection(idx).id() != parentCol.id()) { + *containsMove = true; + } + + droppedList << configString(idx); + } + } + return droppedList; +} + +bool EntityOrderProxyModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_D(EntityOrderProxyModel); + + if (!d->m_orderConfig.isValid()) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + if (!data->hasFormat(QStringLiteral("text/uri-list"))) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + if (row == -1) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + const QList urls = data->urls(); + if (urls.isEmpty()) { + return false; + } + + Collection parentCol; + + if (parent.isValid()) { + parentCol = parent.data(EntityTreeModel::CollectionRole).value(); + } else { + if (!hasChildren(parent)) { + return QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + } + + const QModelIndex targetIndex = index(0, column, parent); + parentCol = parentCollection(targetIndex); + } + + bool containsMove = false; + QStringList droppedList = configStringsForDroppedUrls(urls, parentCol, &containsMove); + + // Dropping new favorite folders + if (droppedList.isEmpty()) { + const bool ok = QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + if (ok) { + droppedList = configStringsForDroppedUrls(urls, parentCol, &containsMove); + } + } + + QStringList existingList; + if (d->m_orderConfig.hasKey(QString::number(parentCol.id()))) { + existingList = d->m_orderConfig.readEntry(configKey(parentCol), QStringList()); + } else { + const int rowCount = this->rowCount(parent); + existingList.reserve(rowCount); + for (int row = 0; row < rowCount; ++row) { + static const int column = 0; + const QModelIndex idx = this->index(row, column, parent); + existingList.append(configString(idx)); + } + } + const int numberOfDroppedElement(droppedList.size()); + for (int i = 0; i < numberOfDroppedElement; ++i) { + const QString &droppedItem = droppedList.at(i); + const int existingIndex = existingList.indexOf(droppedItem); + existingList.removeAt(existingIndex); + existingList.insert(row + i - (existingIndex > row ? 0 : 1), droppedList.at(i)); + } + + d->m_orderConfig.writeEntry(configKey(parentCol), existingList); + + if (containsMove) { + bool result = QSortFilterProxyModel::dropMimeData(data, action, row, column, parent); + invalidate(); + return result; + } + invalidate(); + return true; +} + +QModelIndexList EntityOrderProxyModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (role < Qt::UserRole) { + return QSortFilterProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + const auto matches = sourceModel()->match(mapToSource(start), role, value, hits, flags); + for (const auto &idx : matches) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list.push_back(proxyIndex); + } + } + + return list; +} + +void EntityOrderProxyModelPrivate::saveOrder(const QModelIndex &parent) +{ + Q_Q(const EntityOrderProxyModel); + int rowCount = q->rowCount(parent); + + if (rowCount == 0) { + return; + } + + static const int column = 0; + QModelIndex childIndex = q->index(0, column, parent); + + const QString parentKey = q->parentConfigString(childIndex); + + if (parentKey.isEmpty()) { + return; + } + + QStringList list; + + list << q->configString(childIndex); + saveOrder(childIndex); + list.reserve(list.count() + rowCount); + for (int row = 1; row < rowCount; ++row) { + childIndex = q->index(row, column, parent); + list << q->configString(childIndex); + saveOrder(childIndex); + } + + m_orderConfig.writeEntry(parentKey, list); +} + +QString EntityOrderProxyModel::parentConfigString(const QModelIndex &index) const +{ + const Collection col = parentCollection(index); + + Q_ASSERT(col.isValid()); + if (!col.isValid()) { + return QString(); + } + + return QString::number(col.id()); +} + +QString EntityOrderProxyModel::configString(const QModelIndex &index) const +{ + Item::Id iId = index.data(EntityTreeModel::ItemIdRole).toLongLong(); + if (iId != -1) { + return QLatin1Char('i') + QString::number(iId); + } + Collection::Id cId = index.data(EntityTreeModel::CollectionIdRole).toLongLong(); + if (cId != -1) { + return QLatin1Char('c') + QString::number(cId); + } + Q_ASSERT(!"Invalid entity"); + return QString(); +} + +void EntityOrderProxyModel::saveOrder() +{ + Q_D(EntityOrderProxyModel); + d->saveOrder(QModelIndex()); + d->m_orderConfig.sync(); +} + +void EntityOrderProxyModel::clearOrder(const QModelIndex &parent) +{ + Q_D(EntityOrderProxyModel); + + const QString parentKey = parentConfigString(index(0, 0, parent)); + + if (parentKey.isEmpty()) { + return; + } + + d->m_orderConfig.deleteEntry(parentKey); + invalidate(); +} + +void EntityOrderProxyModel::clearTreeOrder() +{ + Q_D(EntityOrderProxyModel); + d->m_orderConfig.deleteGroup(); + invalidate(); +} diff --git a/src/core/models/entityorderproxymodel.h b/src/core/models/entityorderproxymodel.h new file mode 100644 index 0000000..219c081 --- /dev/null +++ b/src/core/models/entityorderproxymodel.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net + SPDX-FileContributor: Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" + +#include + +class KConfigGroup; + +namespace Akonadi +{ +class EntityOrderProxyModelPrivate; + +/** + * @short A model that keeps the order of entities persistent. + * + * This proxy maintains the order of entities in a tree. The user can re-order + * items and the new order will be persisted restored on reset or restart. + * + * @author Stephen Kelly + * @since 4.6 + */ +class AKONADICORE_EXPORT EntityOrderProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new entity order proxy model. + * + * @param parent The parent object. + */ + explicit EntityOrderProxyModel(QObject *parent = nullptr); + + /** + * Destroys the entity order proxy model. + */ + ~EntityOrderProxyModel() override; + + /** + * Sets the config @p group that will be used for storing the order. + */ + void setOrderConfig(const KConfigGroup &group); + + /** + * Saves the order. + */ + void saveOrder(); + + void clearOrder(const QModelIndex &index); + void clearTreeOrder(); + + /** + * @reimp + */ + Q_REQUIRED_RESULT bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + + /** + * @reimp + */ + Q_REQUIRED_RESULT bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + + /** + * @reimp + */ + Q_REQUIRED_RESULT QModelIndexList match(const QModelIndex &start, + int role, + const QVariant &value, + int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; + +protected: + EntityOrderProxyModelPrivate *const d_ptr; + + virtual QString parentConfigString(const QModelIndex &index) const; + virtual QString configString(const QModelIndex &index) const; + virtual Akonadi::Collection parentCollection(const QModelIndex &index) const; + +private: + QStringList configStringsForDroppedUrls(const QList &urls, const Akonadi::Collection &parentCol, bool *containsMove) const; + + /// @cond PRIVATE + Q_DECLARE_PRIVATE(EntityOrderProxyModel) + /// @endcond +}; + +} + diff --git a/src/core/models/entityrightsfiltermodel.cpp b/src/core/models/entityrightsfiltermodel.cpp new file mode 100644 index 0000000..8eb19dc --- /dev/null +++ b/src/core/models/entityrightsfiltermodel.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2007 Bruno Virlet + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entityrightsfiltermodel.h" + +using namespace Akonadi; + +namespace Akonadi +{ +/** + * @internal + */ +class EntityRightsFilterModelPrivate +{ +public: + explicit EntityRightsFilterModelPrivate(EntityRightsFilterModel *parent) + : q_ptr(parent) + , mAccessRights(Collection::AllRights) + { + } + + bool rightsMatches(const QModelIndex &index) const + { + if (mAccessRights == Collection::AllRights || mAccessRights == Collection::ReadOnly) { + return true; + } + + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + return (mAccessRights & collection.rights()); + } else { + const Item item = index.data(EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + const auto collection = index.data(EntityTreeModel::ParentCollectionRole).value(); + return (mAccessRights & collection.rights()); + } else { + return false; + } + } + } + + Q_DECLARE_PUBLIC(EntityRightsFilterModel) + EntityRightsFilterModel *q_ptr; + + Collection::Rights mAccessRights; +}; + +} // namespace Akonadi + +EntityRightsFilterModel::EntityRightsFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new EntityRightsFilterModelPrivate(this)) +{ + setRecursiveFilteringEnabled(true); +} + +EntityRightsFilterModel::~EntityRightsFilterModel() +{ + delete d_ptr; +} + +bool EntityRightsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const EntityRightsFilterModel); + + const QModelIndex modelIndex = sourceModel()->index(sourceRow, 0, sourceParent); + + return d->rightsMatches(modelIndex); +} + +void EntityRightsFilterModel::setAccessRights(Collection::Rights rights) +{ + Q_D(EntityRightsFilterModel); + d->mAccessRights = rights; + invalidateFilter(); +} + +Collection::Rights EntityRightsFilterModel::accessRights() const +{ + Q_D(const EntityRightsFilterModel); + return d->mAccessRights; +} + +Qt::ItemFlags EntityRightsFilterModel::flags(const QModelIndex &index) const +{ + Q_D(const EntityRightsFilterModel); + + if (d->rightsMatches(index)) { + return QSortFilterProxyModel::flags(index); + } else { + return QSortFilterProxyModel::flags(index) & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } +} + +QModelIndexList EntityRightsFilterModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (role < Qt::UserRole) { + return QSortFilterProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + const auto matches = sourceModel()->match(mapToSource(start), role, value, hits, flags); + for (const auto &idx : matches) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list.push_back(proxyIndex); + } + } + + return list; +} diff --git a/src/core/models/entityrightsfiltermodel.h b/src/core/models/entityrightsfiltermodel.h new file mode 100644 index 0000000..bab57d2 --- /dev/null +++ b/src/core/models/entityrightsfiltermodel.h @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "entitytreemodel.h" + +#include + +namespace Akonadi +{ +class EntityRightsFilterModelPrivate; + +/** + * @short A proxy model that filters entities by access rights. + * + * This class can be used on top of an EntityTreeModel to exclude entities by access type + * or to include only certain entities with special access rights. + * + * @code + * + * using namespace Akonadi; + * + * EntityTreeModel *model = new EntityTreeModel( this ); + * + * EntityRightsFilterModel *filter = new EntityRightsFilterModel(); + * filter->setAccessRights( Collection::CanCreateItem | Collection::CanCreateCollection ); + * filter->setSourceModel( model ); + * + * EntityTreeView *view = new EntityTreeView( this ); + * view->setModel( filter ); + * + * @endcode + * + * @li For collections the access rights are checked against the collections own rights. + * @li For items the access rights are checked against the item's parent collection rights. + * + * @author Tobias Koenig + * @since 4.6 + */ +class AKONADICORE_EXPORT EntityRightsFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new entity rights filter model. + * + * @param parent The parent object. + */ + explicit EntityRightsFilterModel(QObject *parent = nullptr); + + /** + * Destroys the entity rights filter model. + */ + ~EntityRightsFilterModel() override; + + /** + * Sets the access @p rights the entities shall be filtered + * against. If no rights are set explicitly, Collection::AllRights + * is assumed. + * @param rights the access rights filter values + */ + void setAccessRights(Collection::Rights rights); + + /** + * Returns the access rights that are used for filtering. + */ + Q_REQUIRED_RESULT Collection::Rights accessRights() const; + + /** + * @reimp + */ + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + + /** + * @reimp + */ + Q_REQUIRED_RESULT QModelIndexList match(const QModelIndex &start, + int role, + const QVariant &value, + int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(EntityRightsFilterModel) + EntityRightsFilterModelPrivate *const d_ptr; + /// @endcond +}; + +} + diff --git a/src/core/models/entitytreemodel.cpp b/src/core/models/entitytreemodel.cpp new file mode 100644 index 0000000..52f814c --- /dev/null +++ b/src/core/models/entitytreemodel.cpp @@ -0,0 +1,1113 @@ +/* + SPDX-FileCopyrightText: 2008 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitytreemodel.h" +#include "akonadicore_debug.h" +#include "entitytreemodel_p.h" +#include "monitor_p.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "attributefactory.h" +#include "collectionmodifyjob.h" +#include "entitydisplayattribute.h" +#include "itemmodifyjob.h" +#include "monitor.h" +#include "session.h" +#include "transactionsequence.h" + +#include "collectionutils.h" + +#include "pastehelper_p.h" + +// clazy:excludeall=old-style-connect + +Q_DECLARE_METATYPE(QSet) + +using namespace Akonadi; + +EntityTreeModel::EntityTreeModel(Monitor *monitor, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(new EntityTreeModelPrivate(this)) +{ + Q_D(EntityTreeModel); + d->init(monitor); +} + +EntityTreeModel::EntityTreeModel(Monitor *monitor, EntityTreeModelPrivate *d, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(d) +{ + d->init(monitor); +} + +EntityTreeModel::~EntityTreeModel() +{ + Q_D(EntityTreeModel); + + for (const QList &list : std::as_const(d->m_childEntities)) { + qDeleteAll(list); + } + + delete d_ptr; +} + +CollectionFetchScope::ListFilter EntityTreeModel::listFilter() const +{ + Q_D(const EntityTreeModel); + return d->m_listFilter; +} + +void EntityTreeModel::setListFilter(CollectionFetchScope::ListFilter filter) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_listFilter = filter; + d->m_monitor->setAllMonitored(filter == CollectionFetchScope::NoFilter); + d->endResetModel(); +} + +void EntityTreeModel::setCollectionsMonitored(const Collection::List &collections) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + const Akonadi::Collection::List lstCols = d->m_monitor->collectionsMonitored(); + for (const Akonadi::Collection &col : lstCols) { + d->m_monitor->setCollectionMonitored(col, false); + } + for (const Akonadi::Collection &col : collections) { + d->m_monitor->setCollectionMonitored(col, true); + } + d->endResetModel(); +} + +void EntityTreeModel::setCollectionMonitored(const Collection &col, bool monitored) +{ + Q_D(EntityTreeModel); + d->m_monitor->setCollectionMonitored(col, monitored); +} + +bool EntityTreeModel::systemEntitiesShown() const +{ + Q_D(const EntityTreeModel); + return d->m_showSystemEntities; +} + +void EntityTreeModel::setShowSystemEntities(bool show) +{ + Q_D(EntityTreeModel); + d->m_showSystemEntities = show; +} + +void EntityTreeModel::clearAndReset() +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->endResetModel(); +} + +QHash EntityTreeModel::roleNames() const +{ + return {{Qt::DecorationRole, "decoration"}, + {Qt::DisplayRole, "display"}, + + {EntityTreeModel::ItemIdRole, "itemId"}, + {EntityTreeModel::CollectionIdRole, "collectionId"}, + + {EntityTreeModel::UnreadCountRole, "unreadCount"}, + // TODO: expose when states for reporting of fetching payload parts of items is changed + // { EntityTreeModel::FetchStateRole, "fetchState" }, + {EntityTreeModel::EntityUrlRole, "url"}, + {EntityTreeModel::RemoteIdRole, "remoteId"}, + {EntityTreeModel::IsPopulatedRole, "isPopulated"}, + {EntityTreeModel::CollectionRole, "collection"}}; +} + +int EntityTreeModel::columnCount(const QModelIndex &parent) const +{ + // TODO: Statistics? + if (parent.isValid() && parent.column() != 0) { + return 0; + } + + return qMax(entityColumnCount(CollectionTreeHeaders), entityColumnCount(ItemListHeaders)); +} + +QVariant EntityTreeModel::entityData(const Item &item, int column, int role) const +{ + Q_D(const EntityTreeModel); + + if (column == 0) { + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + if (const auto *attr = item.attribute(); attr && !attr->displayName().isEmpty()) { + return attr->displayName(); + } else if (!item.remoteId().isEmpty()) { + return item.remoteId(); + } + return QString(QLatin1Char('<') + QString::number(item.id()) + QLatin1Char('>')); + case Qt::DecorationRole: + if (const auto *attr = item.attribute(); attr && !attr->iconName().isEmpty()) { + return d->iconForName(attr->iconName()); + } + break; + default: + break; + } + } + + return QVariant(); +} + +QVariant EntityTreeModel::entityData(const Collection &collection, int column, int role) const +{ + Q_D(const EntityTreeModel); + + if (column > 0) { + return QString(); + } + + if (collection == Collection::root()) { + // Only display the root collection. It may not be edited. + if (role == Qt::DisplayRole) { + return d->m_rootCollectionDisplayName; + } else if (role == Qt::EditRole) { + return QVariant(); + } + } + + switch (role) { + case Qt::DisplayRole: + case Qt::EditRole: + if (column == 0) { + if (const QString displayName = collection.displayName(); !displayName.isEmpty()) { + return displayName; + } else { + return i18nc("@info:status", "Loading..."); + } + } + break; + case Qt::DecorationRole: + if (const auto *const attr = collection.attribute(); attr && !attr->iconName().isEmpty()) { + return d->iconForName(attr->iconName()); + } + return d->iconForName(CollectionUtils::defaultIconName(collection)); + default: + break; + } + + return QVariant(); +} + +QVariant EntityTreeModel::data(const QModelIndex &index, int role) const +{ + Q_D(const EntityTreeModel); + if (role == SessionRole) { + return QVariant::fromValue(qobject_cast(d->m_session)); + } + + // Ugly, but at least the API is clean. + const auto headerGroup = static_cast((role / static_cast(TerminalUserRole))); + + role %= TerminalUserRole; + if (!index.isValid()) { + if (ColumnCountRole != role) { + return QVariant(); + } + + return entityColumnCount(headerGroup); + } + + if (ColumnCountRole == role) { + return entityColumnCount(headerGroup); + } + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (ParentCollectionRole == role && d->m_collectionFetchStrategy != FetchNoCollections) { + const Collection parentCollection = d->m_collections.value(node->parent); + Q_ASSERT(parentCollection.isValid()); + + return QVariant::fromValue(parentCollection); + } + + if (Node::Collection == node->type) { + const Collection collection = d->m_collections.value(node->id); + if (!collection.isValid()) { + return QVariant(); + } + + switch (role) { + case MimeTypeRole: + return collection.mimeType(); + case RemoteIdRole: + return collection.remoteId(); + case CollectionIdRole: + return collection.id(); + case ItemIdRole: + // QVariant().toInt() is 0, not -1, so we have to handle the ItemIdRole + // and CollectionIdRole (below) specially + return -1; + case CollectionRole: + return QVariant::fromValue(collection); + case EntityUrlRole: + return collection.url().url(); + case UnreadCountRole: + return collection.statistics().unreadCount(); + case FetchStateRole: + return d->m_pendingCollectionRetrieveJobs.contains(collection.id()) ? FetchingState : IdleState; + case IsPopulatedRole: + return d->m_populatedCols.contains(collection.id()); + case OriginalCollectionNameRole: + return entityData(collection, index.column(), Qt::DisplayRole); + case PendingCutRole: + return d->m_pendingCutCollections.contains(node->id); + case Qt::BackgroundRole: + if (const auto *const attr = collection.attribute(); attr && attr->backgroundColor().isValid()) { + return attr->backgroundColor(); + } + Q_FALLTHROUGH(); + default: + return entityData(collection, index.column(), role); + } + + } else if (Node::Item == node->type) { + const Item item = d->m_items.value(node->id); + if (!item.isValid()) { + return QVariant(); + } + + switch (role) { + case ParentCollectionRole: + return QVariant::fromValue(item.parentCollection()); + case MimeTypeRole: + return item.mimeType(); + case RemoteIdRole: + return item.remoteId(); + case ItemRole: + return QVariant::fromValue(item); + case ItemIdRole: + return item.id(); + case CollectionIdRole: + return -1; + case LoadedPartsRole: + return QVariant::fromValue(item.loadedPayloadParts()); + case AvailablePartsRole: + return QVariant::fromValue(item.availablePayloadParts()); + case EntityUrlRole: + return item.url(Akonadi::Item::UrlWithMimeType).url(); + case PendingCutRole: + return d->m_pendingCutItems.contains(node->id); + case Qt::BackgroundRole: + if (const auto *const attr = item.attribute(); attr && attr->backgroundColor().isValid()) { + return attr->backgroundColor(); + } + Q_FALLTHROUGH(); + default: + return entityData(item, index.column(), role); + } + } + + return QVariant(); +} + +Qt::ItemFlags EntityTreeModel::flags(const QModelIndex &index) const +{ + Q_D(const EntityTreeModel); + // Pass modeltest. + if (!index.isValid()) { + return {}; + } + + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (Node::Collection == node->type) { + const Collection collection = d->m_collections.value(node->id); + if (collection.isValid()) { + if (collection == Collection::root()) { + // Selectable and displayable only. + return flags; + } + + const int rights = collection.rights(); + + if (rights & Collection::CanChangeCollection) { + if (index.column() == 0) { + flags |= Qt::ItemIsEditable; + } + // Changing the collection includes changing the metadata (child entityordering). + // Need to allow this by drag and drop. + flags |= Qt::ItemIsDropEnabled; + } + if (rights & (Collection::CanCreateCollection | Collection::CanCreateItem | Collection::CanLinkItem)) { + // Can we drop new collections and items into this collection? + flags |= Qt::ItemIsDropEnabled; + } + + // dragging is always possible, even for read-only objects, but they can only be copied, not moved. + flags |= Qt::ItemIsDragEnabled; + } + } else if (Node::Item == node->type) { + // cut out entities are shown as disabled + // TODO: Not sure this is wanted, it prevents any interaction with them, better + // solution would be to move this to the delegate, as was done for collections. + if (d->m_pendingCutItems.contains(node->id)) { + return Qt::ItemIsSelectable; + } + + // Rights come from the parent collection. + + Collection parentCollection; + if (!index.parent().isValid()) { + parentCollection = d->m_rootCollection; + } else { + const Node *parentNode = reinterpret_cast(index.parent().internalPointer()); + parentCollection = d->m_collections.value(parentNode->id); + } + if (parentCollection.isValid()) { + const int rights = parentCollection.rights(); + + // Can't drop onto items. + if (rights & Collection::CanChangeItem && index.column() == 0) { + flags |= Qt::ItemIsEditable; + } + // dragging is always possible, even for read-only objects, but they can only be copied, not moved. + flags |= Qt::ItemIsDragEnabled; + } + } + + return flags; +} + +Qt::DropActions EntityTreeModel::supportedDropActions() const +{ + return (Qt::CopyAction | Qt::MoveAction | Qt::LinkAction); +} + +QStringList EntityTreeModel::mimeTypes() const +{ + // TODO: Should this return the mimetypes that the items provide? Allow dragging a contact from here for example. + return {QStringLiteral("text/uri-list")}; +} + +bool EntityTreeModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(row) + Q_UNUSED(column) + Q_D(EntityTreeModel); + + // Can't drop onto Collection::root. + if (!parent.isValid()) { + return false; + } + + // TODO Use action and collection rights and return false if necessary + + // if row and column are -1, then the drop was on parent directly. + // data should then be appended on the end of the items of the collections as appropriate. + // That will mean begin insert rows etc. + // Otherwise it was a sibling of the row^th item of parent. + // Needs to be handled when ordering is accounted for. + + // Handle dropping between items as well as on items. + // if ( row != -1 && column != -1 ) + // { + // } + + if (action == Qt::IgnoreAction) { + return true; + } + + // Shouldn't do this. Need to be able to drop vcards for example. + // if ( !data->hasFormat( "text/uri-list" ) ) + // return false; + + Node *node = reinterpret_cast(parent.internalId()); + + Q_ASSERT(node); + + if (Node::Item == node->type) { + if (!parent.parent().isValid()) { + // The drop is somehow on an item with no parent (shouldn't happen) + // The drop should be considered handled anyway. + qCWarning(AKONADICORE_LOG) << "Dropped onto item with no parent collection"; + return true; + } + + // A drop onto an item should be considered as a drop onto its parent collection + node = reinterpret_cast(parent.parent().internalId()); + } + + if (Node::Collection == node->type) { + const Collection destCollection = d->m_collections.value(node->id); + + // Applications can't create new collections in root. Only resources can. + if (destCollection == Collection::root()) { + // Accept the event so that it doesn't propagate. + return true; + } + + if (data->hasFormat(QStringLiteral("text/uri-list"))) { + MimeTypeChecker mimeChecker; + mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes()); + + const QList urls = data->urls(); + for (const QUrl &url : urls) { + const Collection collection = d->m_collections.value(Collection::fromUrl(url).id()); + if (collection.isValid()) { + if (collection.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { + qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same."; + return false; + } + + if (!mimeChecker.isWantedCollection(collection)) { + qCDebug(AKONADICORE_LOG) << "unwanted collection" << mimeChecker.wantedMimeTypes() << collection.contentMimeTypes(); + return false; + } + + QUrlQuery query(url); + if (query.hasQueryItem(QStringLiteral("name"))) { + const QString collectionName = query.queryItemValue(QStringLiteral("name")); + const QStringList collectionNames = d->childCollectionNames(destCollection); + + if (collectionNames.contains(collectionName)) { + QMessageBox::critical( + nullptr, + i18nc("@window:title", "Error"), + i18n("The target collection '%1' contains already\na collection with name '%2'.", destCollection.name(), collection.name())); + return false; + } + } + } else { + const Item item = d->m_items.value(Item::fromUrl(url).id()); + if (item.isValid()) { + if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { + qCWarning(AKONADICORE_LOG) << "Error: source and destination of move are the same."; + return false; + } + + if (!mimeChecker.isWantedItem(item)) { + qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType(); + return false; + } + } + } + } + + KJob *job = PasteHelper::pasteUriList(data, destCollection, action, d->m_session); + if (!job) { + return false; + } + + connect(job, SIGNAL(result(KJob *)), SLOT(pasteJobDone(KJob *))); + + // Accept the event so that it doesn't propagate. + return true; + } else { + // not a set of uris. Maybe vcards etc. Check if the parent supports them, and maybe do + // fromMimeData for them. Hmm, put it in the same transaction with the above? + // TODO: This should be handled first, not last. + } + } + + return false; +} + +QModelIndex EntityTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_D(const EntityTreeModel); + + if (parent.column() > 0) { + return QModelIndex(); + } + + // TODO: don't use column count here? Use some d-> func. + if (column >= columnCount() || column < 0) { + return QModelIndex(); + } + + QList childEntities; + + const Node *parentNode = reinterpret_cast(parent.internalPointer()); + if (!parentNode || !parent.isValid()) { + if (d->m_showRootCollection) { + childEntities << d->m_childEntities.value(-1); + } else { + childEntities = d->m_childEntities.value(d->m_rootCollection.id()); + } + } else if (parentNode->id >= 0) { + childEntities = d->m_childEntities.value(parentNode->id); + } + + const int size = childEntities.size(); + if (row < 0 || row >= size) { + return QModelIndex(); + } + + Node *node = childEntities.at(row); + return createIndex(row, column, reinterpret_cast(node)); +} + +QModelIndex EntityTreeModel::parent(const QModelIndex &index) const +{ + Q_D(const EntityTreeModel); + + if (!index.isValid()) { + return QModelIndex(); + } + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) { + return QModelIndex(); + } + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (!node) { + return QModelIndex(); + } + + const Collection collection = d->m_collections.value(node->parent); + + if (!collection.isValid()) { + return QModelIndex(); + } + + if (collection.id() == d->m_rootCollection.id()) { + if (!d->m_showRootCollection) { + return QModelIndex(); + } else { + return createIndex(0, 0, reinterpret_cast(d->m_rootNode)); + } + } + + Q_ASSERT(collection.parentCollection().isValid()); + const int row = d->indexOf(d->m_childEntities.value(collection.parentCollection().id()), collection.id()); + + Q_ASSERT(row >= 0); + Node *parentNode = d->m_childEntities.value(collection.parentCollection().id()).at(row); + + return createIndex(row, 0, reinterpret_cast(parentNode)); +} + +int EntityTreeModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const EntityTreeModel); + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) { + if (parent.isValid()) { + return 0; + } else { + return d->m_items.size(); + } + } + + if (!parent.isValid()) { + // If we're showing the root collection then it will be the only child of the root. + if (d->m_showRootCollection) { + return d->m_childEntities.value(-1).size(); + } + return d->m_childEntities.value(d->m_rootCollection.id()).size(); + } + + if (parent.column() != 0) { + return 0; + } + + const Node *node = reinterpret_cast(parent.internalPointer()); + + if (!node) { + return 0; + } + + if (Node::Item == node->type) { + return 0; + } + + Q_ASSERT(parent.isValid()); + return d->m_childEntities.value(node->id).size(); +} + +int EntityTreeModel::entityColumnCount(HeaderGroup headerGroup) const +{ + // Not needed in this model. + Q_UNUSED(headerGroup) + + return 1; +} + +QVariant EntityTreeModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const +{ + Q_D(const EntityTreeModel); + // Not needed in this model. + Q_UNUSED(headerGroup) + + if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) { + if (d->m_rootCollection == Collection::root()) { + return i18nc("@title:column Name of a thing", "Name"); + } + return d->m_rootCollection.name(); + } + + return QAbstractItemModel::headerData(section, orientation, role); +} + +QVariant EntityTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + const auto headerGroup = static_cast((role / static_cast(TerminalUserRole))); + + role %= TerminalUserRole; + return entityHeaderData(section, orientation, role, headerGroup); +} + +QMimeData *EntityTreeModel::mimeData(const QModelIndexList &indexes) const +{ + Q_D(const EntityTreeModel); + + auto *data = new QMimeData(); + QList urls; + for (const QModelIndex &index : indexes) { + if (index.column() != 0) { + continue; + } + + if (!index.isValid()) { + continue; + } + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (Node::Collection == node->type) { + urls << d->m_collections.value(node->id).url(Collection::UrlWithName); + } else if (Node::Item == node->type) { + QUrl url = d->m_items.value(node->id).url(Item::Item::UrlWithMimeType); + QUrlQuery query(url); + query.addQueryItem(QStringLiteral("parent"), QString::number(node->parent)); + url.setQuery(query); + urls << url; + } else { // if that happens something went horrible wrong + Q_ASSERT(false); + } + } + + data->setUrls(urls); + + return data; +} + +// Always return false for actions which take place asynchronously, eg via a Job. +bool EntityTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_D(EntityTreeModel); + + const Node *node = reinterpret_cast(index.internalPointer()); + + if (role == PendingCutRole) { + if (index.isValid() && value.toBool()) { + if (Node::Collection == node->type) { + d->m_pendingCutCollections.append(node->id); + } else if (Node::Item == node->type) { + d->m_pendingCutItems.append(node->id); + } + } else { + d->m_pendingCutCollections.clear(); + d->m_pendingCutItems.clear(); + } + return true; + } + + if (index.isValid() && node->type == Node::Collection && (role == CollectionRefRole || role == CollectionDerefRole)) { + const Collection collection = index.data(CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + if (role == CollectionDerefRole) { + d->deref(collection.id()); + } else if (role == CollectionRefRole) { + d->ref(collection.id()); + } + return true; + } + + if (index.column() == 0 && (role & (Qt::EditRole | ItemRole | CollectionRole))) { + if (Node::Collection == node->type) { + Collection collection = d->m_collections.value(node->id); + if (!collection.isValid() || !value.isValid()) { + return false; + } + + if (Qt::EditRole == role) { + collection.setName(value.toString()); + if (collection.hasAttribute()) { + auto *displayAttribute = collection.attribute(); + displayAttribute->setDisplayName(value.toString()); + } + } else if (Qt::BackgroundRole == role) { + auto color = value.value(); + if (!color.isValid()) { + return false; + } + + auto *eda = collection.attribute(Collection::AddIfMissing); + eda->setBackgroundColor(color); + } else if (CollectionRole == role) { + collection = value.value(); + } + + auto *job = new CollectionModifyJob(collection, d->m_session); + connect(job, SIGNAL(result(KJob *)), SLOT(updateJobDone(KJob *))); + + return false; + } else if (Node::Item == node->type) { + Item item = d->m_items.value(node->id); + if (!item.isValid() || !value.isValid()) { + return false; + } + + if (Qt::EditRole == role) { + if (item.hasAttribute()) { + auto *displayAttribute = item.attribute(Item::AddIfMissing); + displayAttribute->setDisplayName(value.toString()); + } + } else if (Qt::BackgroundRole == role) { + auto color = value.value(); + if (!color.isValid()) { + return false; + } + + auto *eda = item.attribute(Item::AddIfMissing); + eda->setBackgroundColor(color); + } else if (ItemRole == role) { + item = value.value(); + Q_ASSERT(item.id() == node->id); + } + + auto *itemModifyJob = new ItemModifyJob(item, d->m_session); + connect(itemModifyJob, SIGNAL(result(KJob *)), SLOT(updateJobDone(KJob *))); + + return false; + } + } + + return QAbstractItemModel::setData(index, value, role); +} + +bool EntityTreeModel::canFetchMore(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return false; +} + +void EntityTreeModel::fetchMore(const QModelIndex &parent) +{ + Q_D(EntityTreeModel); + + if (!d->canFetchMore(parent)) { + return; + } + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch) { + return; + } + + if (d->m_itemPopulation == ImmediatePopulation) { + // Nothing to do. The items are already in the model. + return; + } else if (d->m_itemPopulation == LazyPopulation) { + const Collection collection = parent.data(CollectionRole).value(); + + if (!collection.isValid()) { + return; + } + + d->fetchItems(collection); + } +} + +bool EntityTreeModel::hasChildren(const QModelIndex &parent) const +{ + Q_D(const EntityTreeModel); + + if (d->m_collectionFetchStrategy == InvisibleCollectionFetch || d->m_collectionFetchStrategy == FetchNoCollections) { + return parent.isValid() ? false : !d->m_items.isEmpty(); + } + + // TODO: Empty collections right now will return true and get a little + to expand. + // There is probably no way to tell if a collection + // has child items in akonadi without first attempting an itemFetchJob... + // Figure out a way to fix this. (Statistics) + return ((rowCount(parent) > 0) || (canFetchMore(parent) && d->m_itemPopulation == LazyPopulation)); +} + +bool EntityTreeModel::isCollectionTreeFetched() const +{ + Q_D(const EntityTreeModel); + return d->m_collectionTreeFetched; +} + +bool EntityTreeModel::isCollectionPopulated(Collection::Id id) const +{ + Q_D(const EntityTreeModel); + return d->m_populatedCols.contains(id); +} + +bool EntityTreeModel::isFullyPopulated() const +{ + Q_D(const EntityTreeModel); + return d->m_collectionTreeFetched && d->m_pendingCollectionRetrieveJobs.isEmpty(); +} + +QModelIndexList EntityTreeModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + Q_D(const EntityTreeModel); + + if (role == CollectionIdRole || role == CollectionRole) { + Collection::Id id; + if (role == CollectionRole) { + const Collection collection = value.value(); + id = collection.id(); + } else { + id = value.toLongLong(); + } + + const Collection collection = d->m_collections.value(id); + if (!collection.isValid()) { + return {}; + } + + const QModelIndex collectionIndex = d->indexForCollection(collection); + Q_ASSERT(collectionIndex.isValid()); + return {collectionIndex}; + } else if (role == ItemIdRole || role == ItemRole) { + Item::Id id; + if (role == ItemRole) { + id = value.value().id(); + } else { + id = value.toLongLong(); + } + + const Item item = d->m_items.value(id); + if (!item.isValid()) { + return {}; + } + return d->indexesForItem(item); + } else if (role == EntityUrlRole) { + const QUrl url(value.toString()); + const Item item = Item::fromUrl(url); + + if (item.isValid()) { + return d->indexesForItem(d->m_items.value(item.id())); + } + + const Collection collection = Collection::fromUrl(url); + if (!collection.isValid()) { + return {}; + } + return {d->indexForCollection(collection)}; + } + + return QAbstractItemModel::match(start, role, value, hits, flags); +} + +bool EntityTreeModel::insertRows(int /*row*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +bool EntityTreeModel::insertColumns(int /*column*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +bool EntityTreeModel::removeRows(int /*row*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +bool EntityTreeModel::removeColumns(int /*column*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +void EntityTreeModel::setItemPopulationStrategy(ItemPopulationStrategy strategy) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_itemPopulation = strategy; + + if (strategy == NoItemPopulation) { + disconnect(d->m_monitor, SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection)), this, SLOT(monitoredItemAdded(Akonadi::Item, Akonadi::Collection))); + disconnect(d->m_monitor, SIGNAL(itemChanged(Akonadi::Item, QSet)), this, SLOT(monitoredItemChanged(Akonadi::Item, QSet))); + disconnect(d->m_monitor, SIGNAL(itemRemoved(Akonadi::Item)), this, SLOT(monitoredItemRemoved(Akonadi::Item))); + disconnect(d->m_monitor, + SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)), + this, + SLOT(monitoredItemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + + disconnect(d->m_monitor, SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection)), this, SLOT(monitoredItemLinked(Akonadi::Item, Akonadi::Collection))); + disconnect(d->m_monitor, + SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection)), + this, + SLOT(monitoredItemUnlinked(Akonadi::Item, Akonadi::Collection))); + } + + d->m_monitor->d_ptr->useRefCounting = (strategy == LazyPopulation); + + d->endResetModel(); +} + +EntityTreeModel::ItemPopulationStrategy EntityTreeModel::itemPopulationStrategy() const +{ + Q_D(const EntityTreeModel); + return d->m_itemPopulation; +} + +void EntityTreeModel::setIncludeRootCollection(bool include) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_showRootCollection = include; + d->endResetModel(); +} + +bool EntityTreeModel::includeRootCollection() const +{ + Q_D(const EntityTreeModel); + return d->m_showRootCollection; +} + +void EntityTreeModel::setRootCollectionDisplayName(const QString &displayName) +{ + Q_D(EntityTreeModel); + d->m_rootCollectionDisplayName = displayName; + + // TODO: Emit datachanged if it is being shown. +} + +QString EntityTreeModel::rootCollectionDisplayName() const +{ + Q_D(const EntityTreeModel); + return d->m_rootCollectionDisplayName; +} + +void EntityTreeModel::setCollectionFetchStrategy(CollectionFetchStrategy strategy) +{ + Q_D(EntityTreeModel); + d->beginResetModel(); + d->m_collectionFetchStrategy = strategy; + + if (strategy == FetchNoCollections || strategy == InvisibleCollectionFetch) { + disconnect(d->m_monitor, SIGNAL(collectionChanged(Akonadi::Collection)), this, SLOT(monitoredCollectionChanged(Akonadi::Collection))); + disconnect(d->m_monitor, + SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection)), + this, + SLOT(monitoredCollectionAdded(Akonadi::Collection, Akonadi::Collection))); + disconnect(d->m_monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), this, SLOT(monitoredCollectionRemoved(Akonadi::Collection))); + disconnect(d->m_monitor, + SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)), + this, + SLOT(monitoredCollectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection))); + d->m_monitor->fetchCollection(false); + } else { + d->m_monitor->fetchCollection(true); + } + + d->endResetModel(); +} + +EntityTreeModel::CollectionFetchStrategy EntityTreeModel::collectionFetchStrategy() const +{ + Q_D(const EntityTreeModel); + return d->m_collectionFetchStrategy; +} + +static QPair, const EntityTreeModel *> proxiesAndModel(const QAbstractItemModel *model) +{ + QList proxyChain; + const auto *proxy = qobject_cast(model); + const QAbstractItemModel *_model = model; + while (proxy) { + proxyChain.prepend(proxy); + _model = proxy->sourceModel(); + proxy = qobject_cast(_model); + } + + const auto *etm = qobject_cast(_model); + return qMakePair(proxyChain, etm); +} + +static QModelIndex proxiedIndex(const QModelIndex &idx, const QList &proxyChain) +{ + QModelIndex _idx = idx; + for (const auto *proxy : proxyChain) { + _idx = proxy->mapFromSource(_idx); + } + return _idx; +} + +QModelIndex EntityTreeModel::modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection) +{ + const auto &[proxy, etm] = proxiesAndModel(model); + if (!etm) { + qCWarning(AKONADICORE_LOG) << "Model" << model << "is not derived from ETM or a proxy model on top of ETM."; + return {}; + } + + QModelIndex idx = etm->d_ptr->indexForCollection(collection); + return proxiedIndex(idx, proxy); +} + +QModelIndexList EntityTreeModel::modelIndexesForItem(const QAbstractItemModel *model, const Item &item) +{ + const auto &[proxy, etm] = proxiesAndModel(model); + + if (!etm) { + qCWarning(AKONADICORE_LOG) << "Model" << model << "is not derived from ETM or a proxy model on top of ETM."; + return QModelIndexList(); + } + + const QModelIndexList list = etm->d_ptr->indexesForItem(item); + QModelIndexList proxyList; + for (const QModelIndex &idx : list) { + const QModelIndex pIdx = proxiedIndex(idx, proxy); + if (pIdx.isValid()) { + proxyList.push_back(pIdx); + } + } + return proxyList; +} + +Collection EntityTreeModel::updatedCollection(const QAbstractItemModel *model, qint64 collectionId) +{ + const auto *proxy = qobject_cast(model); + const QAbstractItemModel *_model = model; + while (proxy) { + _model = proxy->sourceModel(); + proxy = qobject_cast(_model); + } + + const auto *etm = qobject_cast(_model); + if (etm) { + return etm->d_ptr->m_collections.value(collectionId); + } else { + return Collection{collectionId}; + } +} + +Collection EntityTreeModel::updatedCollection(const QAbstractItemModel *model, const Collection &collection) +{ + return updatedCollection(model, collection.id()); +} + +#include "moc_entitytreemodel.cpp" diff --git a/src/core/models/entitytreemodel.h b/src/core/models/entitytreemodel.h new file mode 100644 index 0000000..4201290 --- /dev/null +++ b/src/core/models/entitytreemodel.h @@ -0,0 +1,717 @@ +/* + SPDX-FileCopyrightText: 2008 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "collectionfetchscope.h" +#include "item.h" + +#include +#include + +namespace Akonadi +{ +class CollectionStatistics; +class Item; +class ItemFetchScope; +class Monitor; +class Session; + +class EntityTreeModelPrivate; + +/** + * @short A model for collections and items together. + * + * Akonadi models and views provide a high level way to interact with the akonadi server. + * Most applications will use these classes. + * + * Models provide an interface for viewing, updating, deleting and moving Items and Collections. + * Additionally, the models are updated automatically if another application changes the + * data or inserts of deletes items etc. + * + * @note The EntityTreeModel should be used with the EntityTreeView or the EntityListView class + * either directly or indirectly via proxy models. + * + *

Retrieving Collections and Items from the model

+ * + * If you want to retrieve and Item or Collection from the model, and already have a valid + * QModelIndex for the correct row, the Collection can be retrieved like this: + * + * @code + * Collection col = index.data( EntityTreeModel::CollectionRole ).value(); + * @endcode + * + * And similarly for Items. This works even if there is a proxy model between the calling code + * and the EntityTreeModel. + * + * If you want to retrieve a Collection for a particular Collection::Id and you do not yet + * have a valid QModelIndex, use modelIndexForCollection. + * + *

Using EntityTreeModel in your application

+ * + * The responsibilities which fall to the application developer are + * - Configuring the Monitor and EntityTreeModel + * - Making use of this class via proxy models + * - Subclassing for type specific display information + * + *

Creating and configuring the EntityTreeModel

+ * + * This class is a wrapper around a Akonadi::Monitor object. The model represents a + * part of the collection and item tree configured in the Monitor. The structure of the + * model mirrors the structure of Collections and Items on the %Akonadi server. + * + * The following code creates a model which fetches items and collections relevant to + * addressees (contacts), and automatically manages keeping the items up to date. + * + * @code + * + * Monitor *monitor = new Monitor( this ); + * monitor->setCollectionMonitored( Collection::root() ); + * monitor->setMimeTypeMonitored( KContacts::addresseeMimeType() ); + * monitor->setSession( session ); + * + * EntityTreeModel *model = new EntityTreeModel( monitor, this ); + * + * EntityTreeView *view = new EntityTreeView( this ); + * view->setModel( model ); + * + * @endcode + * + * The EntityTreeModel will show items of a different type by changing the line + * + * @code + * monitor->setMimeTypeMonitored( KContacts::addresseeMimeType() ); + * @endcode + * + * to a different mimetype. KContacts::addresseeMimeType() is an alias for "text/directory". If changed to KMime::Message::mimeType() + * (an alias for "message/rfc822") the model would instead contain emails. The model can be configured to contain items of any mimetype + * known to %Akonadi. + * + * @note The EntityTreeModel does some extra configuration on the Monitor, such as setting itemFetchScope() and collectionFetchScope() + * to retrieve all ancestors. This is necessary for proper function of the model. + * + * @see Akonadi::ItemFetchScope::AncestorRetrieval. + * + * @see akonadi-mimetypes. + * + * The EntityTreeModel can be further configured for certain behaviours such as fetching of collections and items. + * + * The model can be configured to not fetch items into the model (ie, fetch collections only) by setting + * + * @code + * entityTreeModel->setItemPopulationStrategy( EntityTreeModel::NoItemPopulation ); + * @endcode + * + * The items may be fetched lazily, i.e. not inserted into the model until request by the user for performance reasons. + * + * The Collection tree is always built immediately if Collections are to be fetched. + * + * @code + * entityTreeModel->setItemPopulationStrategy( EntityTreeModel::LazyPopulation ); + * @endcode + * + * This will typically be used with a EntityMimeTypeFilterModel in a configuration such as KMail4.5 or AkonadiConsole. + * + * The CollectionFetchStrategy determines how the model will be populated with Collections. That is, if FetchNoCollections is set, + * no collections beyond the root of the model will be fetched. This can be used in combination with setting a particular Collection to monitor. + * + * @code + * // Get an collection id from a config file. + * Collection::Id id; + * monitor->setCollectionMonitored( Collection( id ) ); + * // ... Other initialization code. + * entityTree->setCollectionFetchStrategy( FetchNoCollections ); + * @endcode + * + * This has the effect of creating a model of only a list of Items, and not collections. This is similar in behaviour and aims to the ItemModel. + * By using FetchFirstLevelCollections instead, a mixed list of entities can be created. + * + * @note It is important that you set only one Collection to be monitored in the monitor object. This one collection will be the root of the tree. + * If you need a model with a more complex structure, consider monitoring a common ancestor and using a SelectionProxyModel. + * + * @see lazy-model-population + * + * It is also possible to show the root Collection as part of the selectable model: + * + * @code + * entityTree->setIncludeRootCollection( true ); + * @endcode + * + * + * By default the displayed name of the root collection is '[*]', because it doesn't require i18n, and is generic. It can be changed too. + * + * @code + * entityTree->setIncludeRootCollection( true ); + * entityTree->setRootCollectionDisplayName( i18nc( "Name of top level for all addressbooks in the application", "[All AddressBooks]" ) ) + * @endcode + * + * This feature is used in KAddressBook. + * + * If items are to be fetched by the model, it is necessary to specify which parts of the items + * are to be fetched, using the ItemFetchScope class. By default, only the basic metadata is + * fetched. To fetch all item data, including all attributes: + * + * @code + * monitor->itemFetchScope().fetchFullPayload(); + * monitor->itemFetchScope().fetchAllAttributes(); + * @endcode + * + *

Using EntityTreeModel with Proxy models

+ * + * An Akonadi::SelectionProxyModel can be used to simplify managing selection in one view through multiple proxy models to a representation in another view. + * The selectionModel of the initial view is used to create a proxied model which filters out anything not related to the current selection. + * + * @code + * // ... create an EntityTreeModel + * + * collectionTree = new EntityMimeTypeFilterModel( this ); + * collectionTree->setSourceModel( entityTreeModel ); + * + * // Include only collections in this proxy model. + * collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + * collectionTree->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * + * treeview->setModel(collectionTree); + * + * // SelectionProxyModel can handle complex selections: + * treeview->setSelectionMode( QAbstractItemView::ExtendedSelection ); + * + * SelectionProxyModel *selProxy = new SelectionProxyModel( treeview->selectionModel(), this ); + * selProxy->setSourceModel( entityTreeModel ); + * + * itemList = new EntityMimeTypeFilterModel( this ); + * itemList->setSourceModel( selProxy ); + * + * // Filter out collections. Show only items. + * itemList->addMimeTypeExclusionFilter( Collection::mimeType() ); + * itemList->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * + * EntityTreeView *itemView = new EntityTreeView( splitter ); + * itemView->setModel( itemList ); + * @endcode + * + * The SelectionProxyModel can handle complex selections. + * + * See the KSelectionProxyModel documentation for the valid configurations of a Akonadi::SelectionProxyModel. + * + * Obviously, the SelectionProxyModel may be used in a view, or further processed with other proxy models. Typically, the result + * from this model will be further filtered to remove collections from the item list as in the above example. + * + * There are several advantages of using EntityTreeModel with the SelectionProxyModel, namely the items can be fetched and cached + * instead of being fetched many times, and the chain of proxies from the core model to the view is automatically handled. There is + * no need to manage all the mapToSource and mapFromSource calls manually. + * + * A KDescendantsProxyModel can be used to represent all descendants of a model as a flat list. + * For example, to show all descendant items in a selected Collection in a list: + * @code + * collectionTree = new EntityMimeTypeFilterModel( this ); + * collectionTree->setSourceModel( entityTreeModel ); + * + * // Include only collections in this proxy model. + * collectionTree->addMimeTypeInclusionFilter( Collection::mimeType() ); + * collectionTree->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * + * treeview->setModel( collectionTree ); + * + * SelectionProxyModel *selProxy = new SelectionProxyModel( treeview->selectionModel(), this ); + * selProxy->setSourceModel( entityTreeModel ); + * + * descendedList = new KDescendantsProxyModel( this ); + * descendedList->setSourceModel( selProxy ); + * + * itemList = new EntityMimeTypeFilterModel( this ); + * itemList->setSourceModel( descendedList ); + * + * // Exclude collections from the list view. + * itemList->addMimeTypeExclusionFilter( Collection::mimeType() ); + * itemList->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * + * listView = new EntityTreeView( this ); + * listView->setModel( itemList ); + * @endcode + * + * + * Note that it is important in this case to use the KDescendantsProxyModel before the EntityMimeTypeFilterModel. + * Otherwise, by filtering out the collections first, you would also be filtering out their child items. + * + * This pattern is used in KAddressBook. + * + * It would not make sense to use a KDescendantsProxyModel with LazyPopulation. + * + *

Subclassing EntityTreeModel

+ * + * Usually an application will create a subclass of an EntityTreeModel and use that in several views via proxy models. + * + * The subclassing is necessary in order for the data in the model to have type-specific representation in applications + * + * For example, the headerData for an EntityTreeModel will be different depending on whether it is in a view showing only Collections + * in which case the header data should be "AddressBooks" for example, or only Items, in which case the headerData would be + * for example "Family Name", "Given Name" and "Email address" for contacts or "Subject", "Sender", "Date" in the case of emails. + * + * Additionally, the actual data shown in the rows of the model should be type specific. + * + * In summary, it must be possible to have different numbers of columns, different data in hte rows of those columns, and different + * titles for each column depending on the contents of the view. + * + * The way this is accomplished is by using the EntityMimeTypeFilterModel for splitting the model into a "CollectionTree" and an "Item List" + * as in the above example, and using a type-specific EntityTreeModel subclass to return the type-specific data, typically for only one type (for example, + * contacts or emails). + * + * The following protected virtual methods should be implemented in the subclass: + * - int entityColumnCount( HeaderGroup headerGroup ) const; + * -- Implement to return the number of columns for a HeaderGroup. If the HeaderGroup is CollectionTreeHeaders, return the number of columns to display for the + * Collection tree, and if it is ItemListHeaders, return the number of columns to display for the item. In the case of addressee, this could be for example, + * two (for given name and family name) or for emails it could be three (for subject, sender, date). This is a decision of the subclass implementor. + * - QVariant entityHeaderData( int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup ) const; + * -- Implement to return the data for each section for a HeaderGroup. For example, if the header group is CollectionTreeHeaders in a contacts model, + * the string "Address books" might be returned for column 0, whereas if the headerGroup is ItemListHeaders, the strings "Given Name", "Family Name", + * "Email Address" might be returned for the columns 0, 1, and 2. + * - QVariant entityData( const Collection &collection, int column, int role = Qt::DisplayRole ) const; + * -- Implement to return data for a particular Collection. Typically this will be the name of the collection or the EntityDisplayAttribute. + * - QVariant entityData( const Item &item, int column, int role = Qt::DisplayRole ) const; + * -- Implement to return the data for a particular item and column. In the case of email for example, this would be the actual subject, sender and date of the + * email. + * + * @note The entityData methods are just for convenience. the QAbstractItemModel::data method can be overridden if required. + * + * The application writer must then properly configure proxy models for the views, so that the correct data is shown in the correct view. + * That is the purpose of these lines in the above example + * + * @code + * collectionTree->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * itemList->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * @endcode + * + *

Progress reporting

+ * + * The EntityTreeModel uses asynchronous Akonadi::Job instances to fill and update itself. + * For example, a job is run to fetch the contents of collections (that is, list the items in it). + * Additionally, individual Akonadi::Items can be fetched in different parts at different times. + * + * To indicate that such a job is underway, the EntityTreeModel makes the FetchState available. The + * FetchState returned from a QModelIndex representing a Akonadi::Collection will be FetchingState if a + * listing of the items in that collection is underway, otherwise the state is IdleState. + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_EXPORT EntityTreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * Describes the roles for items. Roles for collections are defined by the superclass. + */ + enum Roles { + // sebsauer, 2009-05-07; to be able here to keep the akonadi_next EntityTreeModel compatible with + // the akonadi_old ItemModel and CollectionModel, we need to use the same int-values for + // ItemRole, ItemIdRole and MimeTypeRole like the Akonadi::ItemModel is using and the same + // CollectionIdRole and CollectionRole like the Akonadi::CollectionModel is using. + ItemIdRole = Qt::UserRole + 1, ///< The item id + ItemRole = Qt::UserRole + 2, ///< The Item + MimeTypeRole = Qt::UserRole + 3, ///< The mimetype of the entity + + CollectionIdRole = Qt::UserRole + 10, ///< The collection id. + CollectionRole = Qt::UserRole + 11, ///< The collection. + + RemoteIdRole, ///< The remoteId of the entity + CollectionChildOrderRole, ///< Ordered list of child items if available + ParentCollectionRole, ///< The parent collection of the entity + ColumnCountRole, ///< @internal Used by proxies to determine the number of columns for a header group. + LoadedPartsRole, ///< Parts available in the model for the item + AvailablePartsRole, ///< Parts available in the Akonadi server for the item + SessionRole, ///< @internal The Session used by this model + CollectionRefRole, ///< @internal Used to increase the reference count on a Collection + CollectionDerefRole, ///< @internal Used to decrease the reference count on a Collection + PendingCutRole, ///< Used to indicate items which are to be cut + EntityUrlRole, ///< The akonadi:/ Url of the entity as a string. Item urls will contain the mimetype. + UnreadCountRole, ///< Returns the number of unread items in a collection. @since 4.5 + FetchStateRole, ///< Returns the FetchState of a particular item. @since 4.5 + IsPopulatedRole, ///< Returns whether a Collection has been populated, i.e. whether its items have been fetched. @since 4.10 + OriginalCollectionNameRole, ///< Returns original name for collection @since 4.14 + UserRole = Qt::UserRole + 500, ///< First role for user extensions. + TerminalUserRole = 2000, ///< Last role for user extensions. Don't use a role beyond this or headerData will break. + EndRole = 65535 + }; + + /** + * Describes the state of fetch jobs related to particular collections. + * + * @code + * QModelIndex collectionIndex = getIndex(); + * if (collectionIndex.data(EntityTreeModel::FetchStateRole).toLongLong() == FetchingState) { + * // There is a fetch underway + * } else { + * // There is no fetch underway. + * } + * @endcode + * + * @since 4.5 + */ + enum FetchState { + IdleState, ///< There is no fetch of items in this collection in progress. + FetchingState ///< There is a fetch of items in this collection in progress. + // TODO: Change states for reporting of fetching payload parts of items. + }; + + /** + * Describes what header information the model shall return. + */ + enum HeaderGroup { + EntityTreeHeaders, ///< Header information for a tree with collections and items + CollectionTreeHeaders, ///< Header information for a collection-only tree + ItemListHeaders, ///< Header information for a list of items + UserHeaders = 10, ///< Last header information for submodel extensions + EndHeaderGroup = 32 ///< Last headergroup role. Don't use a role beyond this or headerData will break. + // Note that we're splitting up available roles for the header data hack and int(EndRole / TerminalUserRole) == 32 + }; + + /** + * Creates a new entity tree model. + * + * @param monitor The Monitor whose entities should be represented in the model. + * @param parent The parent object. + */ + explicit EntityTreeModel(Monitor *monitor, QObject *parent = nullptr); + + /** + * Destroys the entity tree model. + */ + ~EntityTreeModel() override; + + /** + * Describes how the model should populated its items. + */ + enum ItemPopulationStrategy { + NoItemPopulation, ///< Do not include items in the model. + ImmediatePopulation, ///< Retrieve items immediately when their parent is in the model. This is the default. + LazyPopulation ///< Fetch items only when requested (using canFetchMore/fetchMore) + }; + + /** + * Some Entities are hidden in the model, but exist for internal purposes, for example, custom object + * directories in groupware resources. + * They are hidden by default, but can be shown by setting @p show to true. + * @param show enabled displaying of hidden entities if set as @c true + * Most applications will not need to use this feature. + */ + void setShowSystemEntities(bool show); + + /** + * Returns @c true if internal system entities are shown, and @c false otherwise. + */ + Q_REQUIRED_RESULT bool systemEntitiesShown() const; + + /** + * Returns the currently used listfilter. + * + * @since 4.14 + */ + Q_REQUIRED_RESULT Akonadi::CollectionFetchScope::ListFilter listFilter() const; + + /** + * Sets the currently used listfilter. + * + * @since 4.14 + */ + void setListFilter(Akonadi::CollectionFetchScope::ListFilter filter); + + /** + * Monitors the specified collections and resets the model. + * + * @since 4.14 + */ + void setCollectionsMonitored(const Akonadi::Collection::List &collections); + + /** + * Adds or removes a specific collection from the monitored set without resetting the model. + * Only call this if you're monitoring specific collections (not mimetype/resources/items). + * + * @since 4.14 + * @see setCollectionsMonitored() + */ + void setCollectionMonitored(const Akonadi::Collection &col, bool monitored = true); + + /** + * Sets the item population @p strategy of the model. + */ + void setItemPopulationStrategy(ItemPopulationStrategy strategy); + + /** + * Returns the item population strategy of the model. + */ + Q_REQUIRED_RESULT ItemPopulationStrategy itemPopulationStrategy() const; + + /** + * Sets whether the root collection shall be provided by the model. + * @param include enables root collection if set as @c true + * @see setRootCollectionDisplayName() + */ + void setIncludeRootCollection(bool include); + + /** + * Returns whether the root collection is provided by the model. + */ + Q_REQUIRED_RESULT bool includeRootCollection() const; + + /** + * Sets the display @p name of the root collection of the model. + * The default display name is "[*]". + * @param name the name to display for the root collection + * @note The display name for the root collection is only used if + * the root collection has been included with setIncludeRootCollection(). + */ + void setRootCollectionDisplayName(const QString &name); + + /** + * Returns the display name of the root collection. + */ + Q_REQUIRED_RESULT QString rootCollectionDisplayName() const; + + /** + * Describes what collections shall be fetched by and represent in the model. + */ + enum CollectionFetchStrategy { + FetchNoCollections, ///< Fetches nothing. This creates an empty model. + FetchFirstLevelChildCollections, ///< Fetches first level collections in the root collection. + FetchCollectionsRecursive, ///< Fetches collections in the root collection recursively. This is the default. + InvisibleCollectionFetch ///< Fetches collections, but does not put them in the model. This can be used to create a list of items in all collections. + ///< The ParentCollectionRole can still be used to retrieve the parent collection of an Item. @since 4.5 + }; + + /** + * Sets the collection fetch @p strategy of the model. + */ + void setCollectionFetchStrategy(CollectionFetchStrategy strategy); + + /** + * Returns the collection fetch strategy of the model. + */ + Q_REQUIRED_RESULT CollectionFetchStrategy collectionFetchStrategy() const; + + Q_REQUIRED_RESULT QHash roleNames() const override; + + Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + Q_REQUIRED_RESULT QStringList mimeTypes() const override; + + Q_REQUIRED_RESULT Qt::DropActions supportedDropActions() const override; + Q_REQUIRED_RESULT QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &index) const override; + + // TODO: Review the implementations of these. I think they could be better. + Q_REQUIRED_RESULT bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + Q_REQUIRED_RESULT bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + + /** + * Returns whether the collection tree has been fetched at initialisation. + * + * @see collectionTreeFetched + * @since 4.10 + */ + Q_REQUIRED_RESULT bool isCollectionTreeFetched() const; + + /** + * Returns whether the collection has been populated. + * + * @see collectionPopulated + * @since 4.12 + */ + Q_REQUIRED_RESULT bool isCollectionPopulated(Akonadi::Collection::Id) const; + + /** + * Returns whether the model is fully populated. + * + * Returns true once the collection tree has been fetched and all collections have been populated. + * + * @see isCollectionPopulated + * @see isCollectionTreeFetched + * @since 4.14 + */ + Q_REQUIRED_RESULT bool isFullyPopulated() const; + + /** + * Reimplemented to handle the AmazingCompletionRole. + */ + Q_REQUIRED_RESULT QModelIndexList match(const QModelIndex &start, + int role, + const QVariant &value, + int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; + + /** + * Returns a QModelIndex in @p model which points to @p collection. + * This method can be used through proxy models if @p model is a proxy model. + * @code + * EntityTreeModel *model = getEntityTreeModel(); + * QSortFilterProxyModel *proxy1 = new QSortFilterProxyModel; + * proxy1->setSourceModel(model); + * QSortFilterProxyModel *proxy2 = new QSortFilterProxyModel; + * proxy2->setSourceModel(proxy1); + * + * ... + * + * QModelIndex idx = EntityTreeModel::modelIndexForCollection(proxy2, Collection(colId)); + * if (!idx.isValid()) + * // Collection with id colId is not in the proxy2. + * // Maybe it is filtered out if proxy 2 is only showing items? Make sure you use the correct proxy. + * return; + * + * Collection collection = idx.data( EntityTreeModel::CollectionRole ).value(); + * // collection has the id colId, and all other attributes already fetched by the model such as name, remoteId, Akonadi::Attributes etc. + * + * @endcode + * + * This can be useful for example if an id is stored in a config file and needs to be used in the application. + * + * Note however, that to restore view state such as scrolling, selection and expansion of items in trees, the ETMViewStateSaver can be used for convenience. + * + * @see modelIndexesForItem + * @since 4.5 + */ + static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection); + + /** + * Returns a QModelIndex in @p model which points to @p item. + * This method can be used through proxy models if @p model is a proxy model. + * @param model the model to query for the item + * @param item the item to look for + * @see modelIndexForCollection + * @since 4.5 + */ + static QModelIndexList modelIndexesForItem(const QAbstractItemModel *model, const Item &item); + + /** + * Returns an Akonadi::Collection from the @p model based on given @p collectionId. + * + * This is faster and simpler than retrieving a full Collection from the ETM + * by using modelIndexForCollection() and then querying for the index data. + */ + static Collection updatedCollection(const QAbstractItemModel *model, qint64 collectionId); + static Collection updatedCollection(const QAbstractItemModel *model, const Collection &col); + +Q_SIGNALS: + /** + * Signal emitted when the collection tree has been fetched for the first time. + * @param collections list of collections which have been fetched + * + * @see isCollectionTreeFetched, collectionPopulated + * @since 4.10 + */ + void collectionTreeFetched(const Akonadi::Collection::List &collections); + + /** + * Signal emitted when a collection has been populated, i.e. its items have been fetched. + * @param collectionId id of the collection which has been populated + * + * @see collectionTreeFetched + * @since 4.10 + */ + void collectionPopulated(Akonadi::Collection::Id collectionId); + /** + * Emitted once a collection has been fetched for the very first time. + * This is like a dataChanged(), but specific to the initial loading, in order to update + * the GUI (window caption, state of actions). + * Usually, the GUI uses Akonadi::Monitor to be notified of further changes to the collections. + * @param collectionId the identifier of the fetched collection + * @since 4.9.3 + */ + void collectionFetched(int collectionId); + +protected: + /** + * Clears and resets the model. Always call this instead of the reset method in the superclass. + * Using the reset method will not reliably clear or refill the model. + */ + void clearAndReset(); + + /** + * Provided for convenience of subclasses. + */ + virtual QVariant entityData(const Item &item, int column, int role = Qt::DisplayRole) const; + + /** + * Provided for convenience of subclasses. + */ + virtual QVariant entityData(const Collection &collection, int column, int role = Qt::DisplayRole) const; + + /** + * Reimplement this to provide different header data. This is needed when using one model + * with multiple proxies and views, and each should show different header data. + */ + virtual QVariant entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const; + + virtual int entityColumnCount(HeaderGroup headerGroup) const; + +protected: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(EntityTreeModel) + EntityTreeModelPrivate *d_ptr; + EntityTreeModel(Monitor *monitor, EntityTreeModelPrivate *d, QObject *parent = nullptr); + /// @endcond + +private: + /// @cond PRIVATE + // Make these private, they shouldn't be called by applications + bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) override; + bool insertColumns(int column, int count, const QModelIndex &index = QModelIndex()) override; + bool removeColumns(int column, int count, const QModelIndex &index = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex &index = QModelIndex()) override; + + Q_PRIVATE_SLOT(d_func(), void monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, const Akonadi::CollectionStatistics &)) + + Q_PRIVATE_SLOT(d_func(), void startFirstListJob()) + Q_PRIVATE_SLOT(d_func(), void serverStarted()) + + Q_PRIVATE_SLOT(d_func(), void collectionFetchJobDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void rootFetchJobDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void pasteJobDone(KJob *job)) + Q_PRIVATE_SLOT(d_func(), void updateJobDone(KJob *job)) + + Q_PRIVATE_SLOT(d_func(), void itemsFetched(const Akonadi::Item::List &)) + Q_PRIVATE_SLOT(d_func(), void collectionsFetched(Akonadi::Collection::List)) + Q_PRIVATE_SLOT(d_func(), void topLevelCollectionsFetched(Akonadi::Collection::List)) + Q_PRIVATE_SLOT(d_func(), void ancestorsFetched(Akonadi::Collection::List)) + + Q_PRIVATE_SLOT(d_func(), void monitoredMimeTypeChanged(const QString &, bool)) + Q_PRIVATE_SLOT(d_func(), void monitoredCollectionsChanged(const Akonadi::Collection &, bool)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemsChanged(const Akonadi::Item &, bool)) + Q_PRIVATE_SLOT(d_func(), void monitoredResourcesChanged(const QByteArray &, bool)) + + Q_PRIVATE_SLOT(d_func(), void monitoredCollectionAdded(const Akonadi::Collection &, const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void monitoredCollectionRemoved(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void monitoredCollectionChanged(const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void monitoredCollectionMoved(const Akonadi::Collection &, const Akonadi::Collection &, const Akonadi::Collection &)) + + Q_PRIVATE_SLOT(d_func(), void monitoredItemAdded(const Akonadi::Item &, const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemRemoved(const Akonadi::Item &)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemChanged(const Akonadi::Item &, const QSet &)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemMoved(const Akonadi::Item &, const Akonadi::Collection &, const Akonadi::Collection &)) + + Q_PRIVATE_SLOT(d_func(), void monitoredItemLinked(const Akonadi::Item &, const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemUnlinked(const Akonadi::Item &, const Akonadi::Collection &)) + Q_PRIVATE_SLOT(d_func(), void changeFetchState(const Akonadi::Collection &)) + + Q_PRIVATE_SLOT(d_func(), void agentInstanceRemoved(Akonadi::AgentInstance)) + Q_PRIVATE_SLOT(d_func(), void monitoredItemsRetrieved(KJob *job)) + /// @endcond +}; + +} // namespace + diff --git a/src/core/models/entitytreemodel_p.cpp b/src/core/models/entitytreemodel_p.cpp new file mode 100644 index 0000000..ae7b005 --- /dev/null +++ b/src/core/models/entitytreemodel_p.cpp @@ -0,0 +1,1859 @@ +/* + SPDX-FileCopyrightText: 2008 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitytreemodel_p.h" + +#include "agentmanagerinterface.h" +#include "akranges.h" +#include "monitor_p.h" // For friend ref/deref +#include "servermanager.h" + +#include + +#include "agentmanager.h" +#include "agenttype.h" +#include "changerecorder.h" +#include "collectioncopyjob.h" +#include "collectionfetchscope.h" +#include "collectionmovejob.h" +#include "collectionstatistics.h" +#include "collectionstatisticsjob.h" +#include "entityhiddenattribute.h" +#include "itemcopyjob.h" +#include "itemfetchjob.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" +#include "linkjob.h" +#include "monitor.h" +#include "private/protocol_p.h" +#include "session.h" + +#include "akonadicore_debug.h" + +#include +#include +#include +#include + +// clazy:excludeall=old-style-connect + +QHash jobTimeTracker; + +Q_LOGGING_CATEGORY(DebugETM, "org.kde.pim.akonadi.ETM", QtInfoMsg) + +using namespace Akonadi; +using namespace AkRanges; + +static CollectionFetchJob::Type getFetchType(EntityTreeModel::CollectionFetchStrategy strategy) +{ + switch (strategy) { + case EntityTreeModel::FetchFirstLevelChildCollections: + return CollectionFetchJob::FirstLevel; + case EntityTreeModel::InvisibleCollectionFetch: + case EntityTreeModel::FetchCollectionsRecursive: + default: + break; + } + return CollectionFetchJob::Recursive; +} + +EntityTreeModelPrivate::EntityTreeModelPrivate(EntityTreeModel *parent) + : q_ptr(parent) +{ + // using collection as a parameter of a queued call in runItemFetchJob() + qRegisterMetaType(); + + Akonadi::AgentManager *agentManager = Akonadi::AgentManager::self(); + QObject::connect(agentManager, SIGNAL(instanceRemoved(Akonadi::AgentInstance)), q_ptr, SLOT(agentInstanceRemoved(Akonadi::AgentInstance))); +} + +EntityTreeModelPrivate::~EntityTreeModelPrivate() +{ + if (m_needDeleteRootNode) { + delete m_rootNode; + } + m_rootNode = nullptr; +} + +void EntityTreeModelPrivate::init(Monitor *monitor) +{ + Q_Q(EntityTreeModel); + Q_ASSERT(!m_monitor); + m_monitor = monitor; + // The default is to FetchCollectionsRecursive, so we tell the monitor to fetch collections + // That way update signals from the monitor will contain the full collection. + // This may be updated if the CollectionFetchStrategy is changed. + m_monitor->fetchCollection(true); + m_session = m_monitor->session(); + + m_rootCollectionDisplayName = QStringLiteral("[*]"); + + if (auto cr = qobject_cast(m_monitor)) { + cr->setChangeRecordingEnabled(false); + } + + m_includeStatistics = true; + m_monitor->fetchCollectionStatistics(true); + m_monitor->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + + q->connect(monitor, SIGNAL(mimeTypeMonitored(QString, bool)), SLOT(monitoredMimeTypeChanged(QString, bool))); + q->connect(monitor, SIGNAL(collectionMonitored(Akonadi::Collection, bool)), SLOT(monitoredCollectionsChanged(Akonadi::Collection, bool))); + q->connect(monitor, SIGNAL(itemMonitored(Akonadi::Item, bool)), SLOT(monitoredItemsChanged(Akonadi::Item, bool))); + q->connect(monitor, SIGNAL(resourceMonitored(QByteArray, bool)), SLOT(monitoredResourcesChanged(QByteArray, bool))); + + // monitor collection changes + q->connect(monitor, SIGNAL(collectionChanged(Akonadi::Collection)), SLOT(monitoredCollectionChanged(Akonadi::Collection))); + q->connect(monitor, + SIGNAL(collectionAdded(Akonadi::Collection, Akonadi::Collection)), + SLOT(monitoredCollectionAdded(Akonadi::Collection, Akonadi::Collection))); + q->connect(monitor, SIGNAL(collectionRemoved(Akonadi::Collection)), SLOT(monitoredCollectionRemoved(Akonadi::Collection))); + q->connect(monitor, + SIGNAL(collectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection)), + SLOT(monitoredCollectionMoved(Akonadi::Collection, Akonadi::Collection, Akonadi::Collection))); + + // Monitor item changes. + q->connect(monitor, SIGNAL(itemAdded(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemAdded(Akonadi::Item, Akonadi::Collection))); + q->connect(monitor, SIGNAL(itemChanged(Akonadi::Item, QSet)), SLOT(monitoredItemChanged(Akonadi::Item, QSet))); + q->connect(monitor, SIGNAL(itemRemoved(Akonadi::Item)), SLOT(monitoredItemRemoved(Akonadi::Item))); + q->connect(monitor, + SIGNAL(itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection)), + SLOT(monitoredItemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))); + + q->connect(monitor, SIGNAL(itemLinked(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemLinked(Akonadi::Item, Akonadi::Collection))); + q->connect(monitor, SIGNAL(itemUnlinked(Akonadi::Item, Akonadi::Collection)), SLOT(monitoredItemUnlinked(Akonadi::Item, Akonadi::Collection))); + + q->connect(monitor, + SIGNAL(collectionStatisticsChanged(Akonadi::Collection::Id, Akonadi::CollectionStatistics)), + SLOT(monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, Akonadi::CollectionStatistics))); + + Akonadi::ServerManager *serverManager = Akonadi::ServerManager::self(); + q->connect(serverManager, SIGNAL(started()), SLOT(serverStarted())); + + fillModel(); +} + +void EntityTreeModelPrivate::prependNode(Node *node) +{ + m_childEntities[node->parent].prepend(node); +} + +void EntityTreeModelPrivate::appendNode(Node *node) +{ + m_childEntities[node->parent].append(node); +} + +void EntityTreeModelPrivate::serverStarted() +{ + // Don't emit about to be reset. Too late for that + endResetModel(); +} + +void EntityTreeModelPrivate::changeFetchState(const Collection &parent) +{ + Q_Q(EntityTreeModel); + const QModelIndex collectionIndex = indexForCollection(parent); + if (!collectionIndex.isValid()) { + // Because we are called delayed, it is possible that @p parent has been deleted. + return; + } + Q_EMIT q->dataChanged(collectionIndex, collectionIndex); +} + +void EntityTreeModelPrivate::agentInstanceRemoved(const Akonadi::AgentInstance &instance) +{ + Q_Q(EntityTreeModel); + if (!instance.type().capabilities().contains(QLatin1String("Resource"))) { + return; + } + + if (m_rootCollection.isValid()) { + if (m_rootCollection != Collection::root()) { + if (m_rootCollection.resource() == instance.identifier()) { + q->clearAndReset(); + } + return; + } + const auto &children = m_childEntities[Collection::root().id()]; + for (const Node *node : children) { + Q_ASSERT(node->type == Node::Collection); + + const Collection collection = m_collections[node->id]; + if (collection.resource() == instance.identifier()) { + monitoredCollectionRemoved(collection); + } + } + } +} + +static const char s_fetchCollectionId[] = "FetchCollectionId"; + +void EntityTreeModelPrivate::fetchItems(const Collection &parent) +{ + Q_Q(const EntityTreeModel); + Q_ASSERT(parent.isValid()); + Q_ASSERT(m_collections.contains(parent.id())); + // TODO: Use a more specific fetch scope to get only the envelope for mails etc. + auto itemFetchJob = new Akonadi::ItemFetchJob(parent, m_session); + itemFetchJob->setFetchScope(m_monitor->itemFetchScope()); + itemFetchJob->fetchScope().setAncestorRetrieval(ItemFetchScope::All); + itemFetchJob->fetchScope().setIgnoreRetrievalErrors(true); + itemFetchJob->setDeliveryOption(ItemFetchJob::EmitItemsInBatches); + + itemFetchJob->setProperty(s_fetchCollectionId, QVariant(parent.id())); + + if (m_showRootCollection || parent != m_rootCollection) { + m_pendingCollectionRetrieveJobs.insert(parent.id()); + + // If collections are not in the model, there will be no valid index for them. + if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections)) { + // We need to invoke this delayed because we would otherwise be emitting a sequence like + // - beginInsertRows + // - dataChanged + // - endInsertRows + // which would confuse proxies. + QMetaObject::invokeMethod(const_cast(q), "changeFetchState", Qt::QueuedConnection, Q_ARG(Akonadi::Collection, parent)); + } + } + + q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this, parentId = parent.id()](const Item::List &items) { + itemsFetched(parentId, items); + }); + q->connect(itemFetchJob, &ItemFetchJob::result, q, [this, parentId = parent.id()](KJob *job) { + itemFetchJobDone(parentId, job); + }); + qCDebug(DebugETM) << "collection:" << parent.name(); + jobTimeTracker[itemFetchJob].start(); +} + +void EntityTreeModelPrivate::fetchCollections(Akonadi::CollectionFetchJob *job) +{ + Q_Q(EntityTreeModel); + + job->fetchScope().setListFilter(m_listFilter); + job->fetchScope().setContentMimeTypes(m_monitor->mimeTypesMonitored()); + m_pendingCollectionFetchJobs.insert(static_cast(job)); + + if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { + // This is invisible fetch, so no model signals are emitted + q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const Collection::List &collections) { + for (const auto &collection : collections) { + if (isHidden(collection)) { + continue; + } + m_collections.insert(collection.id(), collection); + prependNode(new Node{Node::Collection, collection.id(), -1}); + fetchItems(collection); + } + }); + } else { + job->fetchScope().setIncludeStatistics(m_includeStatistics); + job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(collectionsFetched(Akonadi::Collection::List))); + } + q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *))); + + jobTimeTracker[job].start(); +} + +void EntityTreeModelPrivate::fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type) +{ + fetchCollections(new CollectionFetchJob(collections, type, m_session)); +} + +void EntityTreeModelPrivate::fetchCollections(const Collection &collection, CollectionFetchJob::Type type) +{ + Q_ASSERT(collection.isValid()); + auto job = new CollectionFetchJob(collection, type, m_session); + fetchCollections(job); +} + +namespace Akonadi +{ +template inline bool EntityTreeModelPrivate::isHiddenImpl(const T &entity, Node::Type type) const +{ + if (m_showSystemEntities) { + return false; + } + + if (type == Node::Collection && entity.id() == m_rootCollection.id()) { + return false; + } + + // entity.hasAttribute() does not compile w/ GCC for + // some reason + if (entity.hasAttribute(EntityHiddenAttribute().type())) { + return true; + } + + const Collection parent = entity.parentCollection(); + if (parent.isValid()) { + return isHiddenImpl(parent, Node::Collection); + } + + return false; +} + +} // namespace Akonadi + +bool EntityTreeModelPrivate::isHidden(const Akonadi::Collection &collection) const +{ + return isHiddenImpl(collection, Node::Collection); +} + +bool EntityTreeModelPrivate::isHidden(const Akonadi::Item &item) const +{ + return isHiddenImpl(item, Node::Item); +} + +static QSet getChildren(Collection::Id parent, const std::unordered_map &childParentMap) +{ + QSet children; + for (const auto &[childId, parentId] : childParentMap) { + if (parentId == parent) { + children.insert(childId); + children.unite(getChildren(childId, childParentMap)); + } + } + return children; +} + +void EntityTreeModelPrivate::collectionsFetched(const Akonadi::Collection::List &collections) +{ + Q_Q(EntityTreeModel); + QElapsedTimer t; + t.start(); + + QVectorIterator it(collections); + + QHash collectionsToInsert; + + while (it.hasNext()) { + const Collection collection = it.next(); + const Collection::Id collectionId = collection.id(); + if (isHidden(collection)) { + continue; + } + + auto collectionIt = m_collections.find(collectionId); + if (collectionIt != m_collections.end()) { + // This is probably the result of a parent of a previous collection already being in the model. + // Replace the dummy collection with the real one and move on. + + // This could also be the result of a monitor signal having already inserted the collection + // into this model. There's no way to tell, so we just emit dataChanged. + *collectionIt = collection; + + const QModelIndex collectionIndex = indexForCollection(collection); + dataChanged(collectionIndex, collectionIndex); + Q_EMIT q->collectionFetched(collectionId); + continue; + } + + // If we're monitoring collections somewhere in the tree we need to retrieve their ancestors now + if (collection.parentCollection() != m_rootCollection && m_monitor->collectionsMonitored().contains(collection)) { + retrieveAncestors(collection, false); + } + + collectionsToInsert.insert(collectionId, collection); + } + + // Build a list of subtrees to insert, with the root of the subtree on the left, and the complete subtree including root on the right + std::unordered_map> subTreesToInsert; + { + // Build a child-parent map that allows us to build the subtrees afterwards + std::unordered_map childParentMap; + for (const auto &col : collectionsToInsert) { + childParentMap.insert({col.id(), col.parentCollection().id()}); + + // Complete the subtree up to the last known parent + Collection parent = col.parentCollection(); + while (parent.isValid() && parent != m_rootCollection && !m_collections.contains(parent.id())) { + childParentMap.insert({parent.id(), parent.parentCollection().id()}); + + if (!collectionsToInsert.contains(parent.id())) { + collectionsToInsert.insert(parent.id(), parent); + } + parent = parent.parentCollection(); + } + } + + QSet parents; + + // Find toplevel parents of the subtrees + for (const auto &[childId, parentId] : childParentMap) { + // The child has a parent without parent (it's a toplevel node that is not yet in m_collections) + if (childParentMap.find(parentId) == childParentMap.cend()) { + Q_ASSERT(!m_collections.contains(childId)); + parents.insert(childId); + } + } + + // Find children of each subtree + for (const auto parentId : parents) { + QSet children; + // We add the parent itself as well so it can be inserted below as part of the same loop + children << parentId; + children += getChildren(parentId, childParentMap); + subTreesToInsert.insert_or_assign(parentId, std::move(children)); + } + } + + const int row = 0; + + for (const auto &[topCollectionId, subtree] : subTreesToInsert) { + qCDebug(DebugETM) << "Subtree: " << topCollectionId << subtree; + + Q_ASSERT(!m_collections.contains(topCollectionId)); + Collection topCollection = collectionsToInsert.value(topCollectionId); + Q_ASSERT(topCollection.isValid()); + + // The toplevels parent must already be part of the model + Q_ASSERT(m_collections.contains(topCollection.parentCollection().id())); + const QModelIndex parentIndex = indexForCollection(topCollection.parentCollection()); + + q->beginInsertRows(parentIndex, row, row); + Q_ASSERT(!subtree.empty()); + + for (const auto collectionId : subtree) { + const Collection collection = collectionsToInsert.take(collectionId); + Q_ASSERT(collection.isValid()); + + m_collections.insert(collectionId, collection); + + Q_ASSERT(collection.parentCollection().isValid()); + prependNode(new Node{Node::Collection, collectionId, collection.parentCollection().id()}); + } + q->endInsertRows(); + + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + for (const auto collectionId : subtree) { + const auto col = m_collections.value(collectionId); + if (!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedCollection(col)) { + fetchItems(col); + } else { + // Consider collections that don't contain relevant mimetypes to be populated + m_populatedCols.insert(collectionId); + Q_EMIT q_ptr->collectionPopulated(collectionId); + const auto idx = indexForCollection(Collection(collectionId)); + Q_ASSERT(idx.isValid()); + dataChanged(idx, idx); + } + } + } + } +} + +// Used by entitytreemodeltest +void EntityTreeModelPrivate::itemsFetched(const Akonadi::Item::List &items) +{ + Q_Q(EntityTreeModel); + const auto collectionId = q->sender()->property(s_fetchCollectionId).value(); + itemsFetched(collectionId, items); +} + +void EntityTreeModelPrivate::itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items) +{ + Q_Q(EntityTreeModel); + + if (!m_collections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items"; + return; + } + + const Collection collection = m_collections.value(collectionId); + + Q_ASSERT(collection.isValid()); + + // if there are any items at all, remove from set of collections known to be empty + if (!items.isEmpty()) { + m_collectionsWithoutItems.remove(collectionId); + } + + Item::List itemsToInsert; + for (const auto &item : items) { + if (isHidden(item)) { + continue; + } + + if ((!m_mimeChecker.hasWantedMimeTypes() || m_mimeChecker.isWantedItem(item))) { + // When listing virtual collections we might get results for items which are already in + // the model if their concrete collection has already been listed. + // In that case the collectionId should be different though. + + // As an additional complication, new items might be both part of fetch job results and + // part of monitor notifications. We only insert items which are not already in the model + // considering their (possibly virtual) parent. + bool isNewItem = true; + auto itemIt = m_items.find(item.id()); + if (itemIt != m_items.end()) { + const Akonadi::Collection::List parents = getParentCollections(item); + for (const Akonadi::Collection &parent : parents) { + if (parent.id() == collectionId) { + qCWarning(AKONADICORE_LOG) << "Fetched an item which is already in the model, id=" << item.id() << "collection id=" << collectionId; + // Update it in case the revision changed; + itemIt->value.apply(item); + isNewItem = false; + break; + } + } + } + + if (isNewItem) { + itemsToInsert << item; + } + } + } + + if (!itemsToInsert.isEmpty()) { + const Collection::Id colId = m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch ? m_rootCollection.id() + : m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections ? m_rootCollection.id() + : collectionId; + const int startRow = m_childEntities.value(colId).size(); + + Q_ASSERT(m_collections.contains(colId)); + + const QModelIndex parentIndex = indexForCollection(m_collections.value(colId)); + q->beginInsertRows(parentIndex, startRow, startRow + itemsToInsert.size() - 1); + for (const Item &item : std::as_const(itemsToInsert)) { + const Item::Id itemId = item.id(); + m_items.ref(itemId, item); + + m_childEntities[colId].append(new Node{Node::Item, itemId, collectionId}); + } + q->endInsertRows(); + } +} + +void EntityTreeModelPrivate::monitoredMimeTypeChanged(const QString &mimeType, bool monitored) +{ + beginResetModel(); + if (monitored) { + m_mimeChecker.addWantedMimeType(mimeType); + } else { + m_mimeChecker.removeWantedMimeType(mimeType); + } + endResetModel(); +} + +void EntityTreeModelPrivate::monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored) +{ + if (monitored) { + const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy); + fetchCollections(collection, CollectionFetchJob::Base); + fetchCollections(collection, fetchType); + } else { + // If a collection is dereferenced and no longer explicitly monitored it might still match other filters + if (!shouldBePartOfModel(collection)) { + monitoredCollectionRemoved(collection); + } + } +} + +void EntityTreeModelPrivate::monitoredItemsChanged(const Akonadi::Item &item, bool monitored) +{ + Q_UNUSED(item) + Q_UNUSED(monitored) + beginResetModel(); + endResetModel(); +} + +void EntityTreeModelPrivate::monitoredResourcesChanged(const QByteArray &resource, bool monitored) +{ + Q_UNUSED(resource) + Q_UNUSED(monitored) + beginResetModel(); + endResetModel(); +} + +bool EntityTreeModelPrivate::retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection) +{ + Q_Q(EntityTreeModel); + + Collection parentCollection = collection.parentCollection(); + + Q_ASSERT(parentCollection.isValid()); + Q_ASSERT(parentCollection != Collection::root()); + + Collection::List ancestors; + + while (parentCollection != Collection::root() && !m_collections.contains(parentCollection.id())) { + // Put a temporary node in the tree later. + ancestors.prepend(parentCollection); + + parentCollection = parentCollection.parentCollection(); + // If we got here through Collection added notification, the parent chain may be incomplete + // and if the model is still populating or the collection belongs to a yet-unknown subtree + // this will break here + if (!parentCollection.isValid()) { + break; + } + } + // if m_rootCollection is Collection::root(), we always have common ancestor and do the retrieval + // if we traversed up to Collection::root() but are looking at a subtree only (m_rootCollection != Collection::root()) + // we have no common ancestor, and we don't have to retrieve anything + if (parentCollection == Collection::root() && m_rootCollection != Collection::root()) { + return true; + } + + if (ancestors.isEmpty() && !insertBaseCollection) { + // Nothing to do, avoid emitting insert signals + return true; + } + + CollectionFetchJob *job = nullptr; + // We were unable to reach the top of the tree due to an incomplete ancestor chain, we will have + // to retrieve it from the server. + if (!parentCollection.isValid()) { + if (insertBaseCollection) { + job = new CollectionFetchJob(collection, CollectionFetchJob::Recursive, m_session); + } else { + job = new CollectionFetchJob(collection.parentCollection(), CollectionFetchJob::Recursive, m_session); + } + } else if (!ancestors.isEmpty()) { + // Fetch the real ancestors + job = new CollectionFetchJob(ancestors, CollectionFetchJob::Base, m_session); + } + + if (job) { + job->fetchScope().setListFilter(m_listFilter); + job->fetchScope().setIncludeStatistics(m_includeStatistics); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(ancestorsFetched(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *))); + } + + if (!parentCollection.isValid()) { + // We can't proceed to insert the fake collections to complete the tree because + // we do not have the complete ancestor chain. However, once the fetch job is + // finished the tree will be populated accordingly. + return false; + } + + // Q_ASSERT( parentCollection != m_rootCollection ); + const QModelIndex parent = indexForCollection(parentCollection); + + // Still prepending all collections for now. + int row = 0; + + // Although we insert several Collections here, we only need to notify though the model + // about the top-level one. The rest will be found automatically by the view. + q->beginInsertRows(parent, row, row); + + for (const auto &ancestor : std::as_const(ancestors)) { + Q_ASSERT(ancestor.parentCollection().isValid()); + m_collections.insert(ancestor.id(), ancestor); + + prependNode(new Node{Node::Collection, ancestor.id(), ancestor.parentCollection().id()}); + } + + if (insertBaseCollection) { + m_collections.insert(collection.id(), collection); + // Can't just use parentCollection because that doesn't necessarily refer to collection. + prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()}); + } + + q->endInsertRows(); + + return true; +} + +void EntityTreeModelPrivate::ancestorsFetched(const Akonadi::Collection::List &collectionList) +{ + for (const Collection &collection : collectionList) { + m_collections[collection.id()] = collection; + + const QModelIndex index = indexForCollection(collection); + Q_ASSERT(index.isValid()); + dataChanged(index, index); + } +} + +void EntityTreeModelPrivate::insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + Q_ASSERT(collection.isValid()); + Q_ASSERT(parent.isValid()); + + Q_Q(EntityTreeModel); + + const int row = 0; + const QModelIndex parentIndex = indexForCollection(parent); + q->beginInsertRows(parentIndex, row, row); + m_collections.insert(collection.id(), collection); + prependNode(new Node{Node::Collection, collection.id(), parent.id()}); + q->endInsertRows(); +} + +bool EntityTreeModelPrivate::hasChildCollection(const Collection &collection) const +{ + const auto &children = m_childEntities[collection.id()]; + for (const Node *node : children) { + if (node->type == Node::Collection) { + const Collection subcol = m_collections[node->id]; + if (shouldBePartOfModel(subcol)) { + return true; + } + } + } + return false; +} + +bool EntityTreeModelPrivate::isAncestorMonitored(const Collection &collection) const +{ + Akonadi::Collection parent = collection.parentCollection(); + while (parent.isValid()) { + if (m_monitor->collectionsMonitored().contains(parent)) { + return true; + } + parent = parent.parentCollection(); + } + return false; +} + +bool EntityTreeModelPrivate::shouldBePartOfModel(const Collection &collection) const +{ + if (isHidden(collection)) { + return false; + } + + // We want a parent collection if it has at least one child that matches the + // wanted mimetype + if (hasChildCollection(collection)) { + return true; + } + + // Explicitly monitored collection + if (m_monitor->collectionsMonitored().contains(collection)) { + return true; + } + + // We're explicitly monitoring collections, but didn't match the filter + if (!m_mimeChecker.hasWantedMimeTypes() && !m_monitor->collectionsMonitored().isEmpty()) { + // The collection should be included if one of the parents is monitored + return isAncestorMonitored(collection); + } + + // Some collection trees contain multiple mimetypes. Even though server side filtering ensures we + // only get the ones we're interested in from the job, we have to filter on collections received through signals too. + if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedCollection(collection)) { + return false; + } + + if (m_listFilter == CollectionFetchScope::Enabled) { + if (!collection.enabled()) { + return false; + } + } else if (m_listFilter == CollectionFetchScope::Display) { + if (!collection.shouldList(Collection::ListDisplay)) { + return false; + } + } else if (m_listFilter == CollectionFetchScope::Sync) { + if (!collection.shouldList(Collection::ListSync)) { + return false; + } + } else if (m_listFilter == CollectionFetchScope::Index) { + if (!collection.shouldList(Collection::ListIndex)) { + return false; + } + } + + return true; +} + +void EntityTreeModelPrivate::removeChildEntities(Collection::Id collectionId) +{ + const QList childList = m_childEntities.value(collectionId); + for (const Node *node : childList) { + if (node->type == Node::Item) { + m_items.unref(node->id); + } else { + removeChildEntities(node->id); + m_collections.remove(node->id); + m_populatedCols.remove(node->id); + } + } + + qDeleteAll(m_childEntities.take(collectionId)); +} + +QStringList EntityTreeModelPrivate::childCollectionNames(const Collection &collection) const +{ + return m_childEntities[collection.id()] | Views::filter([](const Node *node) { + return node->type == Node::Collection; + }) + | Views::transform([this](const Node *node) { + return m_collections.value(node->id).name(); + }) + | Actions::toQList; +} + +void EntityTreeModelPrivate::monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) +{ + // If the resource is removed while populating the model with it, we might still + // get some monitor signals. These stale/out-of-order signals can't be completely eliminated + // in the akonadi server due to implementation details, so we also handle such signals in the model silently + // in all the monitored slots. + // Stephen Kelly, 28, July 2009 + + // If a fetch job is started and a collection is added to akonadi after the fetch job is started, the + // new collection will be added to the fetch job results. It will also be notified through the monitor. + // We return early here in that case. + if (m_collections.contains(collection.id())) { + return; + } + + // If the resource is explicitly monitored all other checks are skipped. topLevelCollectionsFetched still checks the hidden attribute. + if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && collection.parentCollection() == Collection::root()) { + topLevelCollectionsFetched({collection}); + return; + } + + if (!shouldBePartOfModel(collection)) { + return; + } + + if (!m_collections.contains(parent.id())) { + // The collection we're interested in is contained in a collection we're not interested in. + // We download the ancestors of the collection we're interested in to complete the tree. + if (collection != Collection::root()) { + if (!retrieveAncestors(collection)) { + return; + } + } + } else { + insertCollection(collection, parent); + } + + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + fetchItems(collection); + } +} + +void EntityTreeModelPrivate::monitoredCollectionRemoved(const Akonadi::Collection &collection) +{ + // if an explicitly monitored collection is removed, we would also have to remove collections which were included to show it (as in the move case) + if ((collection == m_rootCollection) || m_monitor->collectionsMonitored().contains(collection)) { + beginResetModel(); + endResetModel(); + return; + } + + Collection::Id parentId = collection.parentCollection().id(); + if (parentId < 0) { + parentId = -1; + } + + if (!m_collections.contains(parentId)) { + return; + } + + // This may be a signal for a collection we've already removed by removing its ancestor. + // Or the collection may have been hidden. + if (!m_collections.contains(collection.id())) { + return; + } + + Q_Q(EntityTreeModel); + + Q_ASSERT(m_childEntities.contains(parentId)); + + const int row = indexOf(m_childEntities.value(parentId), collection.id()); + Q_ASSERT(row >= 0); + + Q_ASSERT(m_collections.contains(parentId)); + const Collection parentCollection = m_collections.value(parentId); + + m_populatedCols.remove(collection.id()); + + const QModelIndex parentIndex = indexForCollection(parentCollection); + + q->beginRemoveRows(parentIndex, row, row); + // Delete all descendant collections and items. + removeChildEntities(collection.id()); + // Remove deleted collection from its parent. + delete m_childEntities[parentId].takeAt(row); + // Remove deleted collection itself. + m_collections.remove(collection.id()); + q->endRemoveRows(); + + // After removing a collection, check whether it's parent should be removed too + if (!shouldBePartOfModel(parentCollection)) { + monitoredCollectionRemoved(parentCollection); + } +} + +void EntityTreeModelPrivate::monitoredCollectionMoved(const Akonadi::Collection &collection, + const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destCollection) +{ + if (isHidden(collection)) { + return; + } + + if (isHidden(sourceCollection)) { + if (isHidden(destCollection)) { + return; + } + + monitoredCollectionAdded(collection, destCollection); + return; + } else if (isHidden(destCollection)) { + monitoredCollectionRemoved(collection); + return; + } + + if (!m_collections.contains(collection.id())) { + return; + } + + if (m_monitor->collectionsMonitored().contains(collection)) { + // if we don't reset here, we would have to make sure that destination collection is actually available, + // and remove the sources parents if they were only included as parents of the moved collection + beginResetModel(); + endResetModel(); + return; + } + + Q_Q(EntityTreeModel); + + const QModelIndex srcParentIndex = indexForCollection(sourceCollection); + const QModelIndex destParentIndex = indexForCollection(destCollection); + + Q_ASSERT(collection.parentCollection().isValid()); + Q_ASSERT(destCollection.isValid()); + Q_ASSERT(collection.parentCollection() == destCollection); + + const int srcRow = indexOf(m_childEntities.value(sourceCollection.id()), collection.id()); + const int destRow = 0; // Prepend collections + + if (!q->beginMoveRows(srcParentIndex, srcRow, srcRow, destParentIndex, destRow)) { + qCWarning(AKONADICORE_LOG) << "Cannot move collection" << collection.id() << " from collection" << sourceCollection.id() << "to" << destCollection.id(); + return; + } + + Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow); + // collection has the correct parentCollection etc. We need to set it on the + // internal data structure to not corrupt things. + m_collections.insert(collection.id(), collection); + node->parent = destCollection.id(); + m_childEntities[destCollection.id()].prepend(node); + q->endMoveRows(); +} + +void EntityTreeModelPrivate::monitoredCollectionChanged(const Akonadi::Collection &collection) +{ + if (!m_collections.contains(collection.id())) { + // This can happen if + // * we get a change notification after removing the collection. + // * a collection of a non-monitored mimetype is changed elsewhere. Monitor does not + // filter by content mimetype of Collections so we get notifications for all of them. + + // We might match the filter now, retry adding the collection + monitoredCollectionAdded(collection, collection.parentCollection()); + return; + } + + if (!shouldBePartOfModel(collection)) { + monitoredCollectionRemoved(collection); + return; + } + + m_collections[collection.id()] = collection; + + if (!m_showRootCollection && collection == m_rootCollection) { + // If the root of the model is not Collection::root it might be modified. + // But it doesn't exist in the accessible model structure, so we need to early return + return; + } + + const QModelIndex index = indexForCollection(collection); + Q_ASSERT(index.isValid()); + dataChanged(index, index); +} + +void EntityTreeModelPrivate::monitoredCollectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics) +{ + if (!m_collections.contains(id)) { + return; + } + + m_collections[id].setStatistics(statistics); + + // if the item count becomes 0, add to set of collections we know to be empty + // otherwise remove if in there + if (statistics.count() == 0) { + m_collectionsWithoutItems.insert(id); + } else { + m_collectionsWithoutItems.remove(id); + } + + if (!m_showRootCollection && id == m_rootCollection.id()) { + // If the root of the model is not Collection::root it might be modified. + // But it doesn't exist in the accessible model structure, so we need to early return + return; + } + + const QModelIndex index = indexForCollection(m_collections[id]); + dataChanged(index, index); +} + +void EntityTreeModelPrivate::monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale 'added' notification for an item whose collection was already removed." << item.id() << item.remoteId(); + return; + } + + if (m_items.contains(item.id())) { + return; + } + + Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true); + + if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) { + return; + } + + // Adding items to not yet populated collections would block fetchMore, resulting in only new items showing up in the collection + // This is only a problem with lazy population, otherwise fetchMore is not used at all + if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collection.id())) { + return; + } + + int row; + QModelIndex parentIndex; + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) { + row = m_childEntities.value(collection.id()).size(); + parentIndex = indexForCollection(m_collections.value(collection.id())); + } else { + row = q->rowCount(); + } + q->beginInsertRows(parentIndex, row, row); + m_items.ref(item.id(), item); + Node *node = new Node{Node::Item, item.id(), collection.id()}; + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) { + m_childEntities[collection.id()].append(node); + } else { + m_childEntities[m_rootCollection.id()].append(node); + } + q->endInsertRows(); +} + +void EntityTreeModelPrivate::monitoredItemRemoved(const Akonadi::Item &item, const Akonadi::Collection &parentCollection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + if ((m_itemPopulation == EntityTreeModel::LazyPopulation) + && !m_populatedCols.contains(parentCollection.isValid() ? parentCollection.id() : item.parentCollection().id())) { + return; + } + + const Collection::List parents = getParentCollections(item); + if (parents.isEmpty()) { + return; + } + + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale 'removed' notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + for (const auto &collection : parents) { + Q_ASSERT(m_collections.contains(collection.id())); + Q_ASSERT(m_childEntities.contains(collection.id())); + + const int row = indexOf(m_childEntities.value(collection.id()), item.id()); + Q_ASSERT(row >= 0); + + const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id())); + + q->beginRemoveRows(parentIndex, row, row); + m_items.unref(item.id()); + delete m_childEntities[collection.id()].takeAt(row); + q->endRemoveRows(); + } +} + +void EntityTreeModelPrivate::monitoredItemChanged(const Akonadi::Item &item, const QSet & /*unused*/) +{ + if (isHidden(item)) { + return; + } + + if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) { + return; + } + + auto itemIt = m_items.find(item.id()); + if (itemIt == m_items.end()) { + qCWarning(AKONADICORE_LOG) << "Got a stale 'changed' notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + itemIt->value.apply(item); + // Notifications about itemChange are always dispatched for real collection + // and also all virtual collections the item belongs to. In order to preserve + // the original storage collection when we need to have special handling for + // notifications for virtual collections + if (item.parentCollection().isVirtual()) { + const Collection originalParent = itemIt->value.parentCollection(); + itemIt->value.setParentCollection(originalParent); + } + + const QModelIndexList indexes = indexesForItem(item); + for (const QModelIndex &index : indexes) { + if (index.isValid()) { + dataChanged(index, index); + } else { + qCWarning(AKONADICORE_LOG) << "item has invalid index:" << item.id() << item.remoteId(); + } + } +} + +void EntityTreeModelPrivate::monitoredItemMoved(const Akonadi::Item &item, + const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destCollection) +{ + if (isHidden(item)) { + return; + } + + if (isHidden(sourceCollection)) { + if (isHidden(destCollection)) { + return; + } + + monitoredItemAdded(item, destCollection); + return; + } else if (isHidden(destCollection)) { + monitoredItemRemoved(item, sourceCollection); + return; + } else { + monitoredItemRemoved(item, sourceCollection); + monitoredItemAdded(item, destCollection); + return; + } + // "Temporarily" commented out as it's likely the best course to + // avoid the dreaded "reset storm" (or layoutChanged storm). The + // whole itemMoved idea is great but not practical until all the + // other proxy models play nicely with it, right now they just + // transform moved signals in layout changed, which explodes into + // a reset of the source model inside of the message list (ouch!) +#if 0 + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale 'moved' notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + Q_ASSERT(m_collections.contains(sourceCollection.id())); + Q_ASSERT(m_collections.contains(destCollection.id())); + + const QModelIndex srcIndex = indexForCollection(sourceCollection); + const QModelIndex destIndex = indexForCollection(destCollection); + + // Where should it go? Always append items and prepend collections and reorganize them with separate reactions to Attributes? + + const Item::Id itemId = item.id(); + + const int srcRow = indexOf(m_childEntities.value(sourceCollection.id()), itemId); + const int destRow = q->rowCount(destIndex); + + Q_ASSERT(srcRow >= 0); + Q_ASSERT(destRow >= 0); + if (!q->beginMoveRows(srcIndex, srcRow, srcRow, destIndex, destRow)) { + qCWarning(AKONADICORE_LOG) << "Invalid move"; + return; + } + + Q_ASSERT(m_childEntities.contains(sourceCollection.id())); + Q_ASSERT(m_childEntities[sourceCollection.id()].size() > srcRow); + + Node *node = m_childEntities[sourceCollection.id()].takeAt(srcRow); + m_items.insert(item.id(), item); + node->parent = destCollection.id(); + m_childEntities[destCollection.id()].append(node); + q->endMoveRows(); +#endif +} + +void EntityTreeModelPrivate::monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + const Collection::Id collectionId = collection.id(); + const Item::Id itemId = item.id(); + + if (m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch && !m_collections.contains(collection.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale 'linked' notification for an item whose collection was already removed." << item.id() << item.remoteId(); + return; + } + + Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collectionId) : true); + + if (m_mimeChecker.hasWantedMimeTypes() && !m_mimeChecker.isWantedItem(item)) { + return; + } + + // Adding items to not yet populated collections would block fetchMore, resullting in only new items showing up in the collection + // This is only a problem with lazy population, otherwise fetchMore is not used at all + if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(collectionId)) { + return; + } + + QList &collectionEntities = m_childEntities[collectionId]; + + const int existingPosition = indexOf(collectionEntities, itemId); + + if (existingPosition > 0) { + qCWarning(AKONADICORE_LOG) << "Item with id " << itemId << " already in virtual collection with id " << collectionId; + return; + } + + const int row = collectionEntities.size(); + + const QModelIndex parentIndex = indexForCollection(m_collections.value(collectionId)); + + q->beginInsertRows(parentIndex, row, row); + m_items.ref(itemId, item); + collectionEntities.append(new Node{Node::Item, itemId, collectionId}); + q->endInsertRows(); +} + +void EntityTreeModelPrivate::monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + Q_Q(EntityTreeModel); + + if (isHidden(item)) { + return; + } + + if ((m_itemPopulation == EntityTreeModel::LazyPopulation) && !m_populatedCols.contains(item.parentCollection().id())) { + return; + } + + if (!m_items.contains(item.id())) { + qCWarning(AKONADICORE_LOG) << "Got a stale 'unlinked' notification for an item which was already removed." << item.id() << item.remoteId(); + return; + } + + Q_ASSERT(m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch ? m_collections.contains(collection.id()) : true); + const int row = indexOf(m_childEntities.value(collection.id()), item.id()); + if (row < 0 || row >= m_childEntities[collection.id()].size()) { + qCWarning(AKONADICORE_LOG) << "couldn't find index of unlinked item " << item.id() << collection.id() << row; + Q_ASSERT(false); + return; + } + + const QModelIndex parentIndex = indexForCollection(m_collections.value(collection.id())); + + q->beginRemoveRows(parentIndex, row, row); + delete m_childEntities[collection.id()].takeAt(row); + m_items.unref(item.id()); + q->endRemoveRows(); +} + +void EntityTreeModelPrivate::collectionFetchJobDone(KJob *job) +{ + m_pendingCollectionFetchJobs.remove(job); + auto cJob = static_cast(job); + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << cJob->collections(); + return; + } + + if (!m_collectionTreeFetched && m_pendingCollectionFetchJobs.isEmpty()) { + m_collectionTreeFetched = true; + Q_EMIT q_ptr->collectionTreeFetched(m_collections | Views::values | Actions::toQVector); + } + + qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec"; + qCDebug(DebugETM) << "was collection fetch job: collections:" << cJob->collections().size(); + if (!cJob->collections().isEmpty()) { + qCDebug(DebugETM) << "first fetched collection:" << cJob->collections().at(0).name(); + } +} + +void EntityTreeModelPrivate::itemFetchJobDone(Collection::Id collectionId, KJob *job) +{ + m_pendingCollectionRetrieveJobs.remove(collectionId); + + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Job error: " << job->errorString() << "for collection:" << collectionId; + return; + } + if (!m_collections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "Collection has been removed while fetching items"; + return; + } + auto iJob = static_cast(job); + qCDebug(DebugETM) << "Fetch job took " << jobTimeTracker.take(job).elapsed() << "msec"; + qCDebug(DebugETM) << "was item fetch job: items:" << iJob->count(); + + if (iJob->count() == 0) { + m_collectionsWithoutItems.insert(collectionId); + } else { + m_collectionsWithoutItems.remove(collectionId); + } + + m_populatedCols.insert(collectionId); + Q_EMIT q_ptr->collectionPopulated(collectionId); + + // If collections are not in the model, there will be no valid index for them. + if ((m_collectionFetchStrategy != EntityTreeModel::InvisibleCollectionFetch) && (m_collectionFetchStrategy != EntityTreeModel::FetchNoCollections) + && (m_showRootCollection || collectionId != m_rootCollection.id())) { + const QModelIndex index = indexForCollection(Collection(collectionId)); + Q_ASSERT(index.isValid()); + // To notify about the changed fetch and population state + dataChanged(index, index); + } +} + +void EntityTreeModelPrivate::pasteJobDone(KJob *job) +{ + if (job->error()) { + QString errorMsg; + if (qobject_cast(job)) { + errorMsg = i18nc("@info", "Could not copy item: %1", job->errorString()); + } else if (qobject_cast(job)) { + errorMsg = i18nc("@info", "Could not copy collection: %1", job->errorString()); + } else if (qobject_cast(job)) { + errorMsg = i18nc("@info", "Could not move item: %1", job->errorString()); + } else if (qobject_cast(job)) { + errorMsg = i18nc("@info", "Could not move collection: %1", job->errorString()); + } else if (qobject_cast(job)) { + errorMsg = i18nc("@info", "Could not link entity: %1", job->errorString()); + } + QMessageBox::critical(nullptr, i18nc("@title:window", "Error"), errorMsg); + } +} + +void EntityTreeModelPrivate::updateJobDone(KJob *job) +{ + if (job->error()) { + // TODO: handle job errors + qCWarning(AKONADICORE_LOG) << "Job error:" << job->errorString(); + } +} + +void EntityTreeModelPrivate::rootFetchJobDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + auto collectionJob = qobject_cast(job); + const Collection::List list = collectionJob->collections(); + + Q_ASSERT(list.size() == 1); + m_rootCollection = list.first(); + startFirstListJob(); +} + +void EntityTreeModelPrivate::startFirstListJob() +{ + Q_Q(EntityTreeModel); + + if (!m_collections.isEmpty()) { + return; + } + + // Even if the root collection is the invalid collection, we still need to start + // the first list job with Collection::root. + auto node = new Node{Node::Collection, m_rootCollection.id(), -1}; + if (m_showRootCollection) { + // Notify the outside that we're putting collection::root into the model. + q->beginInsertRows(QModelIndex(), 0, 0); + m_collections.insert(m_rootCollection.id(), m_rootCollection); + delete m_rootNode; + appendNode(node); + q->endInsertRows(); + } else { + // Otherwise store it silently because it's not part of the usable model. + delete m_rootNode; + m_rootNode = node; + m_needDeleteRootNode = true; + m_collections.insert(m_rootCollection.id(), m_rootCollection); + } + + const bool noMimetypes = !m_mimeChecker.hasWantedMimeTypes(); + const bool noResources = m_monitor->resourcesMonitored().isEmpty(); + const bool multipleCollections = m_monitor->collectionsMonitored().size() > 1; + const bool generalPopulation = !noMimetypes || noResources; + + const CollectionFetchJob::Type fetchType = getFetchType(m_collectionFetchStrategy); + + // Collections can only be monitored if no resources and no mimetypes are monitored + if (multipleCollections && noMimetypes && noResources) { + fetchCollections(m_monitor->collectionsMonitored(), CollectionFetchJob::Base); + fetchCollections(m_monitor->collectionsMonitored(), fetchType); + return; + } + + qCDebug(DebugETM) << "GEN" << generalPopulation << noMimetypes << noResources; + if (generalPopulation) { + fetchCollections(m_rootCollection, fetchType); + } + + // If the root collection is not collection::root, then it could have items, and they will need to be + // retrieved now. + // Only fetch items NOT if there is NoItemPopulation, or if there is Lazypopulation and the root is visible + // (if the root is not visible the lazy population can not be triggered) + if ((m_itemPopulation != EntityTreeModel::NoItemPopulation) && !((m_itemPopulation == EntityTreeModel::LazyPopulation) && m_showRootCollection)) { + if (m_rootCollection != Collection::root()) { + fetchItems(m_rootCollection); + } + } + + // Resources which are explicitly monitored won't have appeared yet if their mimetype didn't match. + // We fetch the top level collections and examine them for whether to add them. + // This fetches virtual collections into the tree. + if (!m_monitor->resourcesMonitored().isEmpty()) { + fetchTopLevelCollections(); + } +} + +void EntityTreeModelPrivate::fetchTopLevelCollections() const +{ + Q_Q(const EntityTreeModel); + auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, m_session); + q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), q, SLOT(topLevelCollectionsFetched(Akonadi::Collection::List))); + q->connect(job, SIGNAL(result(KJob *)), q, SLOT(collectionFetchJobDone(KJob *))); + qCDebug(DebugETM) << "EntityTreeModelPrivate::fetchTopLevelCollections"; + jobTimeTracker[job].start(); +} + +void EntityTreeModelPrivate::topLevelCollectionsFetched(const Akonadi::Collection::List &list) +{ + Q_Q(EntityTreeModel); + for (const Collection &collection : list) { + // These collections have been explicitly shown in the Monitor, + // but hidden trumps that for now. This may change in the future if we figure out a use for it. + if (isHidden(collection)) { + continue; + } + + if (m_monitor->resourcesMonitored().contains(collection.resource().toUtf8()) && !m_collections.contains(collection.id())) { + const QModelIndex parentIndex = indexForCollection(collection.parentCollection()); + // Prepending new collections. + const int row = 0; + q->beginInsertRows(parentIndex, row, row); + + m_collections.insert(collection.id(), collection); + Q_ASSERT(collection.parentCollection() == Collection::root()); + prependNode(new Node{Node::Collection, collection.id(), collection.parentCollection().id()}); + q->endInsertRows(); + + if (m_itemPopulation == EntityTreeModel::ImmediatePopulation) { + fetchItems(collection); + } + + Q_ASSERT(collection.isValid()); + fetchCollections(collection, CollectionFetchJob::Recursive); + } + } +} + +Akonadi::Collection::List EntityTreeModelPrivate::getParentCollections(const Item &item) const +{ + Collection::List list; + for (auto it = m_childEntities.constKeyValueBegin(), end = m_childEntities.constKeyValueEnd(); it != end; ++it) { + const auto &[parentId, childNodes] = *it; + int nodeIndex = indexOf(childNodes, item.id()); + if (nodeIndex != -1 && childNodes.at(nodeIndex)->type == Node::Item) { + list.push_back(m_collections.value(parentId)); + } + } + + return list; +} + +void EntityTreeModelPrivate::ref(Collection::Id id) +{ + m_monitor->d_ptr->ref(id); +} + +bool EntityTreeModelPrivate::shouldPurge(Collection::Id id) const +{ + // reference counted collections should never be purged + // they first have to be deref'ed until they reach 0. + // if the collection is buffered, keep it, otherwise we can safely purge this item + return !m_monitor->d_ptr->isMonitored(id); +} + +bool EntityTreeModelPrivate::isMonitored(Collection::Id id) const +{ + return m_monitor->d_ptr->isMonitored(id); +} + +bool EntityTreeModelPrivate::isBuffered(Collection::Id id) const +{ + return m_monitor->d_ptr->m_buffer.isBuffered(id); +} + +void EntityTreeModelPrivate::deref(Collection::Id id) +{ + const Collection::Id bumpedId = m_monitor->d_ptr->deref(id); + + if (bumpedId < 0) { + return; + } + + // The collection has already been removed, don't purge + if (!m_collections.contains(bumpedId)) { + return; + } + + if (shouldPurge(bumpedId)) { + purgeItems(bumpedId); + } +} + +QList::iterator EntityTreeModelPrivate::skipCollections(QList::iterator it, const QList::iterator &end, int *pos) +{ + for (; it != end; ++it) { + if ((*it)->type == Node::Item) { + break; + } + + ++(*pos); + } + + return it; +} + +QList::iterator +EntityTreeModelPrivate::removeItems(QList::iterator it, const QList::iterator &end, int *pos, const Collection &collection) +{ + Q_Q(EntityTreeModel); + + QList::iterator startIt = it; + + // figure out how many items we will delete + int start = *pos; + for (; it != end; ++it) { + if ((*it)->type != Node::Item) { + break; + } + + ++(*pos); + } + it = startIt; + + const QModelIndex parentIndex = indexForCollection(collection); + + q->beginRemoveRows(parentIndex, start, (*pos) - 1); + const int toDelete = (*pos) - start; + Q_ASSERT(toDelete > 0); + + QList &es = m_childEntities[collection.id()]; + // NOTE: .erase will invalidate all iterators besides "it"! + for (int i = 0; i < toDelete; ++i) { + Q_ASSERT(es.count(*it) == 1); + // don't keep implicitly shared data alive + Q_ASSERT(m_items.contains((*it)->id)); + m_items.unref((*it)->id); + // delete actual node + delete *it; + it = es.erase(it); + } + q->endRemoveRows(); + + return it; +} + +void EntityTreeModelPrivate::purgeItems(Collection::Id id) +{ + QList &childEntities = m_childEntities[id]; + + const Collection collection = m_collections.value(id); + Q_ASSERT(collection.isValid()); + + QList::iterator begin = childEntities.begin(); + QList::iterator end = childEntities.end(); + + int pos = 0; + while ((begin = skipCollections(begin, end, &pos)) != end) { + begin = removeItems(begin, end, &pos, collection); + end = childEntities.end(); + } + m_populatedCols.remove(id); + // if an empty collection is purged and we leave it in here, itemAdded will be ignored for the collection + // and the collection is never populated by fetchMore (but maybe by statistics changed?) + m_collectionsWithoutItems.remove(id); +} + +void EntityTreeModelPrivate::dataChanged(const QModelIndex &top, const QModelIndex &bottom) +{ + Q_Q(EntityTreeModel); + + QModelIndex rightIndex; + + const Node *node = static_cast(bottom.internalPointer()); + if (!node) { + return; + } + + if (node->type == Node::Collection) { + rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::CollectionTreeHeaders) - 1); + } + if (node->type == Node::Item) { + rightIndex = bottom.sibling(bottom.row(), q->entityColumnCount(EntityTreeModel::ItemListHeaders) - 1); + } + + Q_EMIT q->dataChanged(top, rightIndex); +} + +QModelIndex EntityTreeModelPrivate::indexForCollection(const Collection &collection) const +{ + Q_Q(const EntityTreeModel); + + if (!collection.isValid()) { + return QModelIndex(); + } + + if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { + return QModelIndex(); + } + + // The id of the parent of Collection::root is not guaranteed to be -1 as assumed by startFirstListJob, + // we ensure that we use -1 for the invalid Collection. + Collection::Id parentId = -1; + + if ((collection == m_rootCollection)) { + if (m_showRootCollection) { + return q->createIndex(0, 0, static_cast(m_rootNode)); + } + return QModelIndex(); + } + + if (collection == Collection::root()) { + parentId = -1; + } else if (collection.parentCollection().isValid()) { + parentId = collection.parentCollection().id(); + } else { + for (const auto &children : m_childEntities) { + const int row = indexOf(children, collection.id()); + if (row < 0) { + continue; + } + + Node *node = children.at(row); + return q->createIndex(row, 0, static_cast(node)); + } + return QModelIndex(); + } + + const int row = indexOf(m_childEntities.value(parentId), collection.id()); + + if (row < 0) { + return QModelIndex(); + } + + Node *node = m_childEntities.value(parentId).at(row); + + return q->createIndex(row, 0, static_cast(node)); +} + +QModelIndexList EntityTreeModelPrivate::indexesForItem(const Item &item) const +{ + Q_Q(const EntityTreeModel); + QModelIndexList indexes; + + if (m_collectionFetchStrategy == EntityTreeModel::FetchNoCollections) { + Q_ASSERT(m_childEntities.contains(m_rootCollection.id())); + QList nodeList = m_childEntities.value(m_rootCollection.id()); + const int row = indexOf(nodeList, item.id()); + Q_ASSERT(row >= 0); + Q_ASSERT(row < nodeList.size()); + Node *node = nodeList.at(row); + + indexes << q->createIndex(row, 0, static_cast(node)); + return indexes; + } + + const Collection::List collections = getParentCollections(item); + + indexes.reserve(collections.size()); + for (const Collection &collection : collections) { + const int row = indexOf(m_childEntities.value(collection.id()), item.id()); + Q_ASSERT(row >= 0); + Q_ASSERT(m_childEntities.contains(collection.id())); + QList nodeList = m_childEntities.value(collection.id()); + Q_ASSERT(row < nodeList.size()); + Node *node = nodeList.at(row); + + indexes << q->createIndex(row, 0, static_cast(node)); + } + + return indexes; +} + +void EntityTreeModelPrivate::beginResetModel() +{ + Q_Q(EntityTreeModel); + q->beginResetModel(); +} + +void EntityTreeModelPrivate::endResetModel() +{ + Q_Q(EntityTreeModel); + auto subjobs = m_session->findChildren(); + for (auto job : subjobs) { + job->disconnect(q); + } + m_collections.clear(); + m_collectionsWithoutItems.clear(); + m_populatedCols.clear(); + m_items.clear(); + m_pendingCollectionFetchJobs.clear(); + m_pendingCollectionRetrieveJobs.clear(); + m_collectionTreeFetched = false; + + for (const QList &list : std::as_const(m_childEntities)) { + qDeleteAll(list); + } + m_childEntities.clear(); + if (m_needDeleteRootNode) { + m_needDeleteRootNode = false; + delete m_rootNode; + } + m_rootNode = nullptr; + + q->endResetModel(); + fillModel(); +} + +void EntityTreeModelPrivate::monitoredItemsRetrieved(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + + Q_Q(EntityTreeModel); + + auto fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + Item::List list = fetchJob->items(); + + q->beginResetModel(); + for (const Item &item : list) { + m_childEntities[-1].append(new Node{Node::Item, item.id(), m_rootCollection.id()}); + m_items.ref(item.id(), item); + } + q->endResetModel(); +} + +void EntityTreeModelPrivate::fillModel() +{ + Q_Q(EntityTreeModel); + + m_mimeChecker.setWantedMimeTypes(m_monitor->mimeTypesMonitored()); + + const Collection::List collections = m_monitor->collectionsMonitored(); + + if (collections.isEmpty() && m_monitor->numMimeTypesMonitored() == 0 && m_monitor->numResourcesMonitored() == 0 && m_monitor->numItemsMonitored() != 0) { + m_rootCollection = Collection(-1); + m_collectionTreeFetched = true; + Q_EMIT q_ptr->collectionTreeFetched(collections); // there are no collections to fetch + + const auto items = m_monitor->itemsMonitoredEx() | Views::transform([](const auto id) { + return Item{id}; + }) + | Actions::toQVector; + auto itemFetch = new ItemFetchJob(items, m_session); + itemFetch->setFetchScope(m_monitor->itemFetchScope()); + itemFetch->fetchScope().setIgnoreRetrievalErrors(true); + q->connect(itemFetch, SIGNAL(finished(KJob *)), q, SLOT(monitoredItemsRetrieved(KJob *))); + return; + } + // In case there is only a single collection monitored, we can use this + // collection as root of the node tree, in all other cases + // Collection::root() is used + if (collections.size() == 1) { + m_rootCollection = collections.first(); + } else { + m_rootCollection = Collection::root(); + } + + if (m_rootCollection == Collection::root()) { + QTimer::singleShot(0, q, SLOT(startFirstListJob())); + } else { + Q_ASSERT(m_rootCollection.isValid()); + auto rootFetchJob = new CollectionFetchJob(m_rootCollection, CollectionFetchJob::Base, m_session); + q->connect(rootFetchJob, SIGNAL(result(KJob *)), SLOT(rootFetchJobDone(KJob *))); + qCDebug(DebugETM) << ""; + jobTimeTracker[rootFetchJob].start(); + } +} + +bool EntityTreeModelPrivate::canFetchMore(const QModelIndex &parent) const +{ + const Item item = parent.data(EntityTreeModel::ItemRole).value(); + + if (m_collectionFetchStrategy == EntityTreeModel::InvisibleCollectionFetch) { + return false; + } + + if (item.isValid()) { + // items can't have more rows. + // TODO: Should I use this for fetching more of an item, ie more payload parts? + return false; + } else { + // but collections can... + const Collection::Id colId = parent.data(EntityTreeModel::CollectionIdRole).toULongLong(); + + // But the root collection can't... + if (Collection::root().id() == colId) { + return false; + } + + // Collections which contain no items at all can't contain more + if (m_collectionsWithoutItems.contains(colId)) { + return false; + } + + // Don't start the same job multiple times. + if (m_pendingCollectionRetrieveJobs.contains(colId)) { + return false; + } + + // Can't fetch more if the collection's items have already been fetched + if (m_populatedCols.contains(colId)) { + return false; + } + + // Only try to fetch more from a collection if we don't already have items in it. + // Otherwise we'd spend all the time listing items in collections. + return m_childEntities.value(colId) | Actions::none(Node::isItem); + } +} + +QIcon EntityTreeModelPrivate::iconForName(const QString &name) const +{ + if (m_iconThemeName != QIcon::themeName()) { + m_iconThemeName = QIcon::themeName(); + m_iconCache.clear(); + } + + QIcon &icon = m_iconCache[name]; + if (icon.isNull()) { + icon = QIcon::fromTheme(name); + } + return icon; +} diff --git a/src/core/models/entitytreemodel_p.h b/src/core/models/entitytreemodel_p.h new file mode 100644 index 0000000..f20d852 --- /dev/null +++ b/src/core/models/entitytreemodel_p.h @@ -0,0 +1,396 @@ +/* + SPDX-FileCopyrightText: 2008 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +class KJob; + +#include "collectionfetchjob.h" +#include "item.h" +#include "itemfetchscope.h" +#include "mimetypechecker.h" + +#include "entitytreemodel.h" + +#include "akonaditests_export.h" + +#include + +Q_DECLARE_LOGGING_CATEGORY(DebugETM) + +namespace Akonadi +{ +class Monitor; +class AgentInstance; +} + +struct Node { + using Id = qint64; + enum Type : char { + Item, + Collection, + }; + + explicit Node(Type type, Id id, Akonadi::Collection::Id parentId) + : id(id) + , parent(parentId) + , type(type) + { + } + + static bool isItem(Node *node) + { + return node->type == Node::Item; + } + + static bool isCollection(Node *node) + { + return node->type == Node::Collection; + } + + Id id; + Akonadi::Collection::Id parent; + Type type; +}; + +template class RefCountedHash +{ + mutable Value *defaultValue = nullptr; + +public: + explicit RefCountedHash() = default; + Q_DISABLE_COPY_MOVE(RefCountedHash) + + ~RefCountedHash() + { + delete defaultValue; + } + + inline auto begin() + { + return mHash.begin(); + } + inline auto end() + { + return mHash.end(); + } + inline auto begin() const + { + return mHash.begin(); + } + inline auto end() const + { + return mHash.end(); + } + inline auto find(const Key &key) const + { + return mHash.find(key); + } + inline auto find(const Key &key) + { + return mHash.find(key); + } + + inline bool size() const + { + return mHash.size(); + } + inline bool isEmpty() const + { + return mHash.isEmpty(); + } + + inline void clear() + { + mHash.clear(); + } + inline bool contains(const Key &key) const + { + return mHash.contains(key); + } + + inline const Value &value(const Key &key) const + { + auto it = mHash.find(key); + if (it == mHash.end()) { + return defaultValue ? *defaultValue : *(defaultValue = new Value()); + } + return it->value; + } + + inline const Value &operator[](const Key &key) const + { + return value(key); + } + + inline auto ref(const Key &key, const Value &value) + { + auto it = mHash.find(key); + if (it != mHash.end()) { + ++(it->refCnt); + return it; + } else { + return mHash.insert(key, {1, std::move(value)}); + } + } + + inline void unref(const Key &key) + { + auto it = mHash.find(key); + if (it == mHash.end()) { + return; + } + --(it->refCnt); + if (it->refCnt == 0) { + mHash.erase(it); + } + } + +private: + template struct RefCountedValue { + uint8_t refCnt = 0; + V value; + }; + QHash> mHash; +}; + +namespace Akonadi +{ +/** + * @internal + */ +class AKONADI_TESTS_EXPORT EntityTreeModelPrivate +{ +public: + explicit EntityTreeModelPrivate(EntityTreeModel *parent); + ~EntityTreeModelPrivate(); + EntityTreeModel *const q_ptr; + + enum RetrieveDepth { + Base, + Recursive, + }; + + void init(Monitor *monitor); + + void prependNode(Node *node); + void appendNode(Node *node); + + void fetchCollections(const Collection &collection, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel); + void fetchCollections(const Collection::List &collections, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel); + void fetchCollections(Akonadi::CollectionFetchJob *job); + void fetchItems(const Collection &collection); + void collectionsFetched(const Akonadi::Collection::List &collections); + void itemsFetched(const Akonadi::Item::List &items); + void itemsFetched(const Collection::Id collectionId, const Akonadi::Item::List &items); + + void monitoredCollectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + void monitoredCollectionRemoved(const Akonadi::Collection &collection); + void monitoredCollectionChanged(const Akonadi::Collection &collection); + void monitoredCollectionStatisticsChanged(Akonadi::Collection::Id, const Akonadi::CollectionStatistics &statistics); + void + monitoredCollectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &sourceCollection, const Akonadi::Collection &destCollection); + + void monitoredItemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + void monitoredItemRemoved(const Akonadi::Item &item, const Akonadi::Collection &collection = Akonadi::Collection()); + void monitoredItemChanged(const Akonadi::Item &item, const QSet &); + void monitoredItemMoved(const Akonadi::Item &item, const Akonadi::Collection &, const Akonadi::Collection &); + + void monitoredItemLinked(const Akonadi::Item &item, const Akonadi::Collection &); + void monitoredItemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &); + + void monitoredMimeTypeChanged(const QString &mimeType, bool monitored); + void monitoredCollectionsChanged(const Akonadi::Collection &collection, bool monitored); + void monitoredItemsChanged(const Akonadi::Item &item, bool monitored); + void monitoredResourcesChanged(const QByteArray &resource, bool monitored); + + Collection::List getParentCollections(const Item &item) const; + void removeChildEntities(Collection::Id collectionId); + + /** + * Returns the list of names of the child collections of @p collection. + */ + QStringList childCollectionNames(const Collection &collection) const; + + /** + * Fetch parent collections and insert this @p collection and its parents into the node tree + * + * Returns whether the ancestor chain was complete and the parent collections were inserted into + * the tree. + */ + bool retrieveAncestors(const Akonadi::Collection &collection, bool insertBaseCollection = true); + void ancestorsFetched(const Akonadi::Collection::List &collectionList); + void insertCollection(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + void beginResetModel(); + void endResetModel(); + /** + * Start function for filling the Model, finds and fetches the root of the node tree + * Next relevant function for filling the model is startFirstListJob() + */ + void fillModel(); + + void changeFetchState(const Collection &parent); + void agentInstanceRemoved(const Akonadi::AgentInstance &instance); + + QIcon iconForName(const QString &name) const; + + QHash m_collections; + RefCountedHash m_items; + QHash> m_childEntities; + QSet m_populatedCols; + QSet m_collectionsWithoutItems; + + QVector m_pendingCutItems; + QVector m_pendingCutCollections; + mutable QSet m_pendingCollectionRetrieveJobs; + mutable QSet m_pendingCollectionFetchJobs; + + // Icon cache to workaround QIcon::fromTheme being very slow (bug #346644) + mutable QHash m_iconCache; + mutable QString m_iconThemeName; + + Monitor *m_monitor = nullptr; + Collection m_rootCollection; + Node *m_rootNode = nullptr; + bool m_needDeleteRootNode = false; + QString m_rootCollectionDisplayName; + QStringList m_mimeTypeFilter; + MimeTypeChecker m_mimeChecker; + EntityTreeModel::CollectionFetchStrategy m_collectionFetchStrategy = EntityTreeModel::FetchCollectionsRecursive; + EntityTreeModel::ItemPopulationStrategy m_itemPopulation = EntityTreeModel::ImmediatePopulation; + CollectionFetchScope::ListFilter m_listFilter = CollectionFetchScope::NoFilter; + bool m_includeStatistics = false; + bool m_showRootCollection = false; + bool m_collectionTreeFetched = false; + bool m_showSystemEntities = false; + Session *m_session = nullptr; + /** + * Called after the root collection was fetched by fillModel + * + * Initiates further fetching of collections depending on the monitored collections + * (in the monitor) and the m_collectionFetchStrategy. + * + * Further collections are either fetched directly with fetchCollections and + * fetchItems or, in case that collections or resources are monitored explicitly + * via fetchTopLevelCollections + */ + void startFirstListJob(); + + void serverStarted(); + + void monitoredItemsRetrieved(KJob *job); + void rootFetchJobDone(KJob *job); + void collectionFetchJobDone(KJob *job); + void itemFetchJobDone(Collection::Id collectionId, KJob *job); + void updateJobDone(KJob *job); + void pasteJobDone(KJob *job); + + /** + * Returns the index of the node in @p list with the id @p id. Returns -1 if not found. + */ + template int indexOf(const QList &nodes, Node::Id id) const + { + int i = 0; + for (const Node *node : nodes) { + if (node->id == id && node->type == Type) { + return i; + } + i++; + } + + return -1; + } + + Q_DECLARE_PUBLIC(EntityTreeModel) + + void fetchTopLevelCollections() const; + void topLevelCollectionsFetched(const Akonadi::Collection::List &collectionList); + + /** + @returns True if @p item or one of its descendants is hidden. + */ + bool isHidden(const Item &item) const; + bool isHidden(const Collection &collection) const; + + template bool isHiddenImpl(const T &entity, Node::Type type) const; + + void ref(Collection::Id id); + void deref(Collection::Id id); + + /** + * @returns true if the collection is actively monitored (referenced or buffered with refcounting enabled) + * + * purely for testing + */ + bool isMonitored(Collection::Id id) const; + + /** + * @returns true if the collection is buffered + * + * purely for testing + */ + bool isBuffered(Collection::Id id) const; + + /** + @returns true if the Collection with the id of @p id should be purged. + */ + bool shouldPurge(Collection::Id id) const; + + /** + Purges the items in the Collection @p id + */ + void purgeItems(Collection::Id id); + + /** + Removes the items starting from @p it and up to a maximum of @p end in Collection @p col. @p pos should be the index of @p it + in the m_childEntities before calling, and is updated to the position of the next Collection in m_childEntities afterward. + This is required to emit model remove signals properly. + + @returns an iterator pointing to the next Collection after @p it, or at @p end + */ + QList::iterator removeItems(QList::iterator it, const QList::iterator &end, int *pos, const Collection &col); + + /** + Skips over Collections in m_childEntities up to a maximum of @p end. @p it is an iterator pointing to the first Collection + in a block of Collections, and @p pos initially describes the index of @p it in m_childEntities and is updated to point to + the index of the next Item in the list. + + @returns an iterator pointing to the next Item after @p it, or at @p end + */ + QList::iterator skipCollections(QList::iterator it, const QList::iterator &end, int *pos); + + /** + Emits the data changed signal for the entire row as in the subclass, instead of just for the first column. + */ + void dataChanged(const QModelIndex &top, const QModelIndex &bottom); + + /** + * Returns the model index for the given @p collection. + */ + QModelIndex indexForCollection(const Collection &collection) const; + + /** + * Returns the model indexes for the given @p item. + */ + QModelIndexList indexesForItem(const Item &item) const; + + bool canFetchMore(const QModelIndex &parent) const; + + /** + * Returns true if the collection matches all filters and should be part of the model. + * This method checks all properties that could change by modifying the collection. + * Currently that includes: + * * hidden attribute + * * content mime types + */ + bool shouldBePartOfModel(const Collection &collection) const; + bool hasChildCollection(const Collection &collection) const; + bool isAncestorMonitored(const Collection &collection) const; +}; + +} + diff --git a/src/core/models/favoritecollectionsmodel.cpp b/src/core/models/favoritecollectionsmodel.cpp new file mode 100644 index 0000000..1b544bf --- /dev/null +++ b/src/core/models/favoritecollectionsmodel.cpp @@ -0,0 +1,485 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "favoritecollectionsmodel.h" +#include "akonadicore_debug.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "collectionmodifyjob.h" +#include "entitytreemodel.h" +#include "favoritecollectionattribute.h" +#include "mimetypechecker.h" +#include "pastehelper_p.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN FavoriteCollectionsModel::Private +{ +public: + Private(const KConfigGroup &group, FavoriteCollectionsModel *parent) + : q(parent) + , configGroup(group) + { + } + + QString labelForCollection(Collection::Id collectionId) const + { + if (labelMap.contains(collectionId)) { + return labelMap[collectionId]; + } + + return q->defaultFavoriteLabel(Collection{collectionId}); + } + + void insertIfAvailable(Collection::Id col) + { + if (collectionIds.contains(col)) { + select(col); + if (!referencedCollections.contains(col)) { + reference(col); + } + auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{col}); + if (idx.isValid()) { + auto c = q->data(idx, EntityTreeModel::CollectionRole).value(); + if (c.isValid() && !c.hasAttribute()) { + c.addAttribute(new FavoriteCollectionAttribute()); + new CollectionModifyJob(c, q); + } + } + } + } + + void insertIfAvailable(const QModelIndex &idx) + { + insertIfAvailable(idx.data(EntityTreeModel::CollectionIdRole).value()); + } + + /** + * Stuff changed (e.g. new rows inserted into sorted model), reload everything. + */ + void reload() + { + // don't clear the selection model here. Otherwise we mess up the users selection as collections get removed and re-inserted. + for (const Collection::Id &collectionId : std::as_const(collectionIds)) { + insertIfAvailable(collectionId); + } + // If a favorite folder was removed then surely it's gone from the selection model, so no need to do anything about that. + } + + void rowsInserted(const QModelIndex &parent, int begin, int end) + { + for (int row = begin; row <= end; row++) { + const QModelIndex child = q->sourceModel()->index(row, 0, parent); + if (!child.isValid()) { + continue; + } + insertIfAvailable(child); + const int childRows = q->sourceModel()->rowCount(child); + if (childRows > 0) { + rowsInserted(child, 0, childRows - 1); + } + } + } + + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) + { + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + const QModelIndex idx = topLeft.sibling(row, 0); + insertIfAvailable(idx); + } + } + + /** + * Selects the index in the internal selection model to make the collection visible in the model + */ + void select(Collection::Id collectionId) + { + const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (index.isValid()) { + q->selectionModel()->select(index, QItemSelectionModel::Select); + } + } + + void deselect(Collection::Id collectionId) + { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (idx.isValid()) { + q->selectionModel()->select(idx, QItemSelectionModel::Deselect); + } + } + + void reference(Collection::Id collectionId) + { + if (referencedCollections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "already referenced " << collectionId; + return; + } + const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (index.isValid()) { + if (q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionRefRole)) { + referencedCollections << collectionId; + } else { + qCWarning(AKONADICORE_LOG) << "failed to reference collection"; + } + q->sourceModel()->fetchMore(index); + } + } + + void dereference(Collection::Id collectionId) + { + if (!referencedCollections.contains(collectionId)) { + qCWarning(AKONADICORE_LOG) << "not referenced " << collectionId; + return; + } + const QModelIndex index = EntityTreeModel::modelIndexForCollection(q->sourceModel(), Collection(collectionId)); + if (index.isValid()) { + q->sourceModel()->setData(index, QVariant(), EntityTreeModel::CollectionDerefRole); + referencedCollections.remove(collectionId); + } + } + + void clearReferences() + { + for (const Collection::Id &collectionId : std::as_const(referencedCollections)) { + dereference(collectionId); + } + } + + /** + * Adds a collection to the favorite collections + */ + void add(Collection::Id collectionId) + { + if (collectionIds.contains(collectionId)) { + qCDebug(AKONADICORE_LOG) << "already in model " << collectionId; + return; + } + collectionIds << collectionId; + reference(collectionId); + select(collectionId); + const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId}); + if (idx.isValid()) { + auto col = q->data(idx, EntityTreeModel::CollectionRole).value(); + if (col.isValid() && !col.hasAttribute()) { + col.addAttribute(new FavoriteCollectionAttribute()); + new CollectionModifyJob(col, q); + } + } + } + + void remove(Collection::Id collectionId) + { + collectionIds.removeAll(collectionId); + labelMap.remove(collectionId); + dereference(collectionId); + deselect(collectionId); + const auto idx = EntityTreeModel::modelIndexForCollection(q, Collection{collectionId}); + if (idx.isValid()) { + auto col = q->data(idx, EntityTreeModel::CollectionRole).value(); + if (col.isValid() && col.hasAttribute()) { + col.removeAttribute(); + new CollectionModifyJob(col, q); + } + } + } + + void set(const QList &collections) + { + QList colIds = collectionIds; + for (const Collection::Id &col : collections) { + const int removed = colIds.removeAll(col); + const bool isNewCollection = removed <= 0; + if (isNewCollection) { + add(col); + } + } + // Remove what's left + for (Akonadi::Collection::Id colId : std::as_const(colIds)) { + remove(colId); + } + } + + void set(const Akonadi::Collection::List &collections) + { + QList colIds; + colIds.reserve(collections.count()); + for (const Akonadi::Collection &col : collections) { + colIds << col.id(); + } + set(colIds); + } + + void loadConfig() + { + const QList collections = configGroup.readEntry("FavoriteCollectionIds", QList()); + const QStringList labels = configGroup.readEntry("FavoriteCollectionLabels", QStringList()); + const int numberOfLabels(labels.size()); + for (int i = 0; i < collections.size(); ++i) { + if (i < numberOfLabels) { + labelMap[collections[i]] = labels[i]; + } + add(collections[i]); + } + } + + void saveConfig() + { + QStringList labels; + labels.reserve(collectionIds.count()); + for (const Collection::Id &collectionId : std::as_const(collectionIds)) { + labels << labelForCollection(collectionId); + } + + configGroup.writeEntry("FavoriteCollectionIds", collectionIds); + configGroup.writeEntry("FavoriteCollectionLabels", labels); + configGroup.config()->sync(); + } + + FavoriteCollectionsModel *const q; + + QList collectionIds; + QSet referencedCollections; + QHash labelMap; + KConfigGroup configGroup; +}; + +/* Implementation note: + * + * We use KSelectionProxyModel in order to make a flat list of selected folders from the folder tree. + * + * Attempts to use QSortFilterProxyModel make code somewhat simpler, + * but don't work since we then get a filtered tree, not a flat list. Stacking a KDescendantsProxyModel + * on top would likely remove explicitly selected parents when one of their child is selected too. + */ + +FavoriteCollectionsModel::FavoriteCollectionsModel(QAbstractItemModel *source, const KConfigGroup &group, QObject *parent) + : KSelectionProxyModel(new QItemSelectionModel(source, parent), parent) + , d(new Private(group, this)) +{ + setSourceModel(source); + setFilterBehavior(ExactSelection); + + d->loadConfig(); + // React to various changes in the source model + connect(source, &QAbstractItemModel::modelReset, this, [this]() { + d->reload(); + }); + connect(source, &QAbstractItemModel::layoutChanged, this, [this]() { + d->reload(); + }); + connect(source, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int begin, int end) { + d->rowsInserted(parent, begin, end); + }); + connect(source, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br) { + d->dataChanged(tl, br); + }); +} + +FavoriteCollectionsModel::~FavoriteCollectionsModel() +{ + delete d; +} + +void FavoriteCollectionsModel::setCollections(const Collection::List &collections) +{ + d->set(collections); + d->saveConfig(); +} + +void FavoriteCollectionsModel::addCollection(const Collection &collection) +{ + d->add(collection.id()); + d->saveConfig(); +} + +void FavoriteCollectionsModel::removeCollection(const Collection &collection) +{ + d->remove(collection.id()); + d->saveConfig(); +} + +Akonadi::Collection::List FavoriteCollectionsModel::collections() const +{ + Collection::List cols; + cols.reserve(d->collectionIds.count()); + for (const Collection::Id &colId : std::as_const(d->collectionIds)) { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(colId)); + const auto collection = sourceModel()->data(idx, EntityTreeModel::CollectionRole).value(); + cols << collection; + } + return cols; +} + +QList FavoriteCollectionsModel::collectionIds() const +{ + return d->collectionIds; +} + +void Akonadi::FavoriteCollectionsModel::setFavoriteLabel(const Collection &collection, const QString &label) +{ + Q_ASSERT(d->collectionIds.contains(collection.id())); + d->labelMap[collection.id()] = label; + d->saveConfig(); + + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(sourceModel(), collection); + + if (!idx.isValid()) { + return; + } + + const QModelIndex index = mapFromSource(idx); + Q_EMIT dataChanged(index, index); +} + +QVariant Akonadi::FavoriteCollectionsModel::data(const QModelIndex &index, int role) const +{ + if (index.column() == 0 && (role == Qt::DisplayRole || role == Qt::EditRole)) { + const QModelIndex sourceIndex = mapToSource(index); + const Collection::Id collectionId = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionIdRole).toLongLong(); + + return d->labelForCollection(collectionId); + } else { + return KSelectionProxyModel::data(index, role); + } +} + +bool FavoriteCollectionsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.isValid() && index.column() == 0 && role == Qt::EditRole) { + const QString newLabel = value.toString(); + if (newLabel.isEmpty()) { + return false; + } + const QModelIndex sourceIndex = mapToSource(index); + const auto collection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); + setFavoriteLabel(collection, newLabel); + return true; + } + return KSelectionProxyModel::setData(index, value, role); +} + +QString Akonadi::FavoriteCollectionsModel::favoriteLabel(const Akonadi::Collection &collection) +{ + if (!collection.isValid()) { + return QString(); + } + return d->labelForCollection(collection.id()); +} + +QString Akonadi::FavoriteCollectionsModel::defaultFavoriteLabel(const Akonadi::Collection &collection) +{ + if (!collection.isValid()) { + return QString(); + } + + const auto colIdx = EntityTreeModel::modelIndexForCollection(sourceModel(), Collection(collection.id())); + const QString nameOfCollection = colIdx.data().toString(); + + QModelIndex idx = colIdx.parent(); + QString accountName; + while (idx != QModelIndex()) { + accountName = idx.data(EntityTreeModel::OriginalCollectionNameRole).toString(); + idx = idx.parent(); + } + if (accountName.isEmpty()) { + return nameOfCollection; + } else { + return nameOfCollection + QStringLiteral(" (") + accountName + QLatin1Char(')'); + } +} + +QVariant FavoriteCollectionsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole) { + return i18n("Favorite Folders"); + } else { + return KSelectionProxyModel::headerData(section, orientation, role); + } +} + +bool FavoriteCollectionsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + Q_UNUSED(action) + Q_UNUSED(row) + Q_UNUSED(column) + if (data->hasFormat(QStringLiteral("text/uri-list"))) { + const QList urls = data->urls(); + + const QModelIndex sourceIndex = mapToSource(parent); + const auto destCollection = sourceModel()->data(sourceIndex, EntityTreeModel::CollectionRole).value(); + + MimeTypeChecker mimeChecker; + mimeChecker.setWantedMimeTypes(destCollection.contentMimeTypes()); + + for (const QUrl &url : urls) { + const Collection col = Collection::fromUrl(url); + if (col.isValid()) { + addCollection(col); + } else { + const Item item = Item::fromUrl(url); + if (item.isValid()) { + if (item.parentCollection().id() == destCollection.id() && action != Qt::CopyAction) { + qCDebug(AKONADICORE_LOG) << "Error: source and destination of move are the same."; + return false; + } +#if 0 + if (!mimeChecker.isWantedItem(item)) { + qCDebug(AKONADICORE_LOG) << "unwanted item" << mimeChecker.wantedMimeTypes() << item.mimeType(); + return false; + } +#endif + KJob *job = PasteHelper::pasteUriList(data, destCollection, action); + if (!job) { + return false; + } + connect(job, &KJob::result, this, &FavoriteCollectionsModel::pasteJobDone); + // Accept the event so that it doesn't propagate. + return true; + } + } + } + return true; + } + return false; +} + +QStringList FavoriteCollectionsModel::mimeTypes() const +{ + QStringList mts = KSelectionProxyModel::mimeTypes(); + if (!mts.contains(QLatin1String("text/uri-list"))) { + mts.append(QStringLiteral("text/uri-list")); + } + return mts; +} + +Qt::ItemFlags FavoriteCollectionsModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags fs = KSelectionProxyModel::flags(index); + if (!index.isValid()) { + fs |= Qt::ItemIsDropEnabled; + } + return fs; +} + +void FavoriteCollectionsModel::pasteJobDone(KJob *job) +{ + if (job->error()) { + qCDebug(AKONADICORE_LOG) << "Paste job error:" << job->errorString(); + } +} + +#include "moc_favoritecollectionsmodel.cpp" diff --git a/src/core/models/favoritecollectionsmodel.h b/src/core/models/favoritecollectionsmodel.h new file mode 100644 index 0000000..355cae6 --- /dev/null +++ b/src/core/models/favoritecollectionsmodel.h @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include + +class KConfigGroup; +class KJob; + +namespace Akonadi +{ +class EntityTreeModel; + +/** + * @short A model that lists a set of favorite collections. + * + * In some applications you want to provide fast access to a list + * of often used collections (e.g. Inboxes from different email accounts + * in a mail application). Therefore you can use the FavoriteCollectionsModel + * which stores the list of favorite collections in a given configuration + * file. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * EntityTreeModel *sourceModel = new EntityTreeModel( ... ); + * + * const KConfigGroup group = KGlobal::config()->group( "Favorite Collections" ); + * + * FavoriteCollectionsModel *model = new FavoriteCollectionsModel( sourceModel, group, this ); + * + * EntityListView *view = new EntityListView( this ); + * view->setModel( model ); + * + * @endcode + * + * @author Kevin Ottens + * @since 4.4 + */ +class AKONADICORE_EXPORT FavoriteCollectionsModel : public KSelectionProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new favorite collections model. + * + * @param model The source model where the favorite collections + * come from. + * @param group The config group that shall be used to save the + * selection of favorite collections. + * @param parent The parent object. + */ + FavoriteCollectionsModel(QAbstractItemModel *model, const KConfigGroup &group, QObject *parent = nullptr); + + /** + * Destroys the favorite collections model. + */ + ~FavoriteCollectionsModel() override; + + /** + * Returns the list of favorite collections. + * @deprecated Use collectionIds instead. + */ + Q_REQUIRED_RESULT AKONADICORE_DEPRECATED Collection::List collections() const; + + /** + * Returns the list of ids of favorite collections set on the FavoriteCollectionsModel. + * + * Note that if you want Collections with actual data + * you should use something like this instead: + * + * @code + * FavoriteCollectionsModel* favs = getFavsModel(); + * Collection::List cols; + * const int rowCount = favs->rowCount(); + * for (int row = 0; row < rowcount; ++row) { + * static const int column = 0; + * const QModelIndex index = favs->index(row, column); + * const Collection col = index.data(EntityTreeModel::CollectionRole).value(); + * cols << col; + * } + * @endcode + * + * Note that due to the asynchronous nature of the model, this method returns collection ids + * of collections which may not be in the model yet. If you want the ids of the collections + * that are actually in the model, use a loop similar to above with the CollectionIdRole. + */ + Q_REQUIRED_RESULT QList collectionIds() const; + + /** + * Return associate label for collection + */ + Q_REQUIRED_RESULT QString favoriteLabel(const Akonadi::Collection &col); + Q_REQUIRED_RESULT QString defaultFavoriteLabel(const Akonadi::Collection &col); + + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + Q_REQUIRED_RESULT QStringList mimeTypes() const override; + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + +public Q_SLOTS: + /** + * Sets the @p collections as favorite collections. + */ + void setCollections(const Akonadi::Collection::List &collections); + + /** + * Adds a @p collection to the list of favorite collections. + */ + void addCollection(const Akonadi::Collection &collection); + + /** + * Removes a @p collection from the list of favorite collections. + */ + void removeCollection(const Akonadi::Collection &collection); + + /** + * Sets a custom @p label that will be used when showing the + * favorite @p collection. + */ + void setFavoriteLabel(const Akonadi::Collection &collection, const QString &label); + +private Q_SLOTS: + void pasteJobDone(KJob *job); + +private: + /// @cond PRIVATE + using KSelectionProxyModel::setSourceModel; + + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/models/recursivecollectionfilterproxymodel.cpp b/src/core/models/recursivecollectionfilterproxymodel.cpp new file mode 100644 index 0000000..b9ff730 --- /dev/null +++ b/src/core/models/recursivecollectionfilterproxymodel.cpp @@ -0,0 +1,147 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + SPDX-FileCopyrightText: 2012-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "recursivecollectionfilterproxymodel.h" + +#include "collectionutils.h" +#include "entitytreemodel.h" +#include "mimetypechecker.h" + +using namespace Akonadi; + +namespace Akonadi +{ +class RecursiveCollectionFilterProxyModelPrivate +{ + Q_DECLARE_PUBLIC(RecursiveCollectionFilterProxyModel) + RecursiveCollectionFilterProxyModel *q_ptr; + +public: + explicit RecursiveCollectionFilterProxyModelPrivate(RecursiveCollectionFilterProxyModel *model) + : q_ptr(model) + { + } + + QSet includedMimeTypes; + Akonadi::MimeTypeChecker checker; + QString pattern; + bool checkOnlyChecked = false; + bool excludeUnifiedMailBox = false; +}; + +} // namespace Akonadi + +RecursiveCollectionFilterProxyModel::RecursiveCollectionFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new RecursiveCollectionFilterProxyModelPrivate(this)) +{ + setRecursiveFilteringEnabled(true); +} + +RecursiveCollectionFilterProxyModel::~RecursiveCollectionFilterProxyModel() +{ + delete d_ptr; +} + +bool RecursiveCollectionFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const RecursiveCollectionFilterProxyModel); + + const QModelIndex rowIndex = sourceModel()->index(sourceRow, 0, sourceParent); + const auto collection = rowIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); + if (!collection.isValid()) { + return false; + } + if (CollectionUtils::isUnifiedMailbox(collection)) { + return false; + } + const bool checked = (rowIndex.data(Qt::CheckStateRole).toInt() == Qt::Checked); + const bool isCheckable = sourceModel()->flags(rowIndex) & Qt::ItemIsUserCheckable; + if (isCheckable && (d->checkOnlyChecked && !checked)) { + return false; + } + + const bool collectionWanted = d->checker.isWantedCollection(collection); + if (collectionWanted) { + if (!d->pattern.isEmpty()) { + const QString text = rowIndex.data(Qt::DisplayRole).toString(); + return text.contains(d->pattern, Qt::CaseInsensitive); + } + } + return collectionWanted; +} + +void RecursiveCollectionFilterProxyModel::addContentMimeTypeInclusionFilter(const QString &mimeType) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes << mimeType; + d->checker.setWantedMimeTypes(d->includedMimeTypes.values()); + invalidateFilter(); +} + +void RecursiveCollectionFilterProxyModel::addContentMimeTypeInclusionFilters(const QStringList &mimeTypes) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes.unite(QSet(mimeTypes.begin(), mimeTypes.end())); + d->checker.setWantedMimeTypes(d->includedMimeTypes.values()); + invalidateFilter(); +} + +void RecursiveCollectionFilterProxyModel::clearFilters() +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes.clear(); + d->checker.setWantedMimeTypes(QStringList()); + invalidateFilter(); +} + +void RecursiveCollectionFilterProxyModel::setContentMimeTypeInclusionFilters(const QStringList &mimeTypes) +{ + Q_D(RecursiveCollectionFilterProxyModel); + d->includedMimeTypes = QSet(mimeTypes.begin(), mimeTypes.end()); + d->checker.setWantedMimeTypes(d->includedMimeTypes.values()); + invalidateFilter(); +} + +QStringList RecursiveCollectionFilterProxyModel::contentMimeTypeInclusionFilters() const +{ + Q_D(const RecursiveCollectionFilterProxyModel); + return d->includedMimeTypes.values(); +} + +int Akonadi::RecursiveCollectionFilterProxyModel::columnCount(const QModelIndex &index) const +{ + // Optimization: we know that we're not changing the number of columns, so skip QSortFilterProxyModel + return sourceModel()->columnCount(mapToSource(index)); +} + +void Akonadi::RecursiveCollectionFilterProxyModel::setSearchPattern(const QString &pattern) +{ + Q_D(RecursiveCollectionFilterProxyModel); + if (d->pattern != pattern) { + d->pattern = pattern; + invalidate(); + } +} + +void Akonadi::RecursiveCollectionFilterProxyModel::setIncludeCheckedOnly(bool checked) +{ + Q_D(RecursiveCollectionFilterProxyModel); + if (d->checkOnlyChecked != checked) { + d->checkOnlyChecked = checked; + invalidate(); + } +} + +void RecursiveCollectionFilterProxyModel::setExcludeUnifiedMailBox(bool exclude) +{ + Q_D(RecursiveCollectionFilterProxyModel); + if (d->excludeUnifiedMailBox != exclude) { + d->excludeUnifiedMailBox = exclude; + invalidate(); + } +} diff --git a/src/core/models/recursivecollectionfilterproxymodel.h b/src/core/models/recursivecollectionfilterproxymodel.h new file mode 100644 index 0000000..c716887 --- /dev/null +++ b/src/core/models/recursivecollectionfilterproxymodel.h @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + SPDX-FileCopyrightText: 2012-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +class RecursiveCollectionFilterProxyModelPrivate; + +/** + * @short A model to filter out collections of non-matching content types. + * + * @author Stephen Kelly + * @since 4.6 + */ +class AKONADICORE_EXPORT RecursiveCollectionFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new recursive collection filter proxy model. + * + * @param parent The parent object. + */ + explicit RecursiveCollectionFilterProxyModel(QObject *parent = nullptr); + + /** + * Destroys the recursive collection filter proxy model. + */ + ~RecursiveCollectionFilterProxyModel() override; + + /** + * Add content mime type to be shown by the filter. + * + * @param mimeType A mime type to be shown. + */ + void addContentMimeTypeInclusionFilter(const QString &mimeType); + + /** + * Add content mime types to be shown by the filter. + * + * @param mimeTypes A list of content mime types to be included. + */ + void addContentMimeTypeInclusionFilters(const QStringList &mimeTypes); + + /** + * Clears the current filters. + */ + void clearFilters(); + + /** + * Replace the content mime types to be shown by the filter. + * + * @param mimeTypes A list of content mime types to be included. + */ + void setContentMimeTypeInclusionFilters(const QStringList &mimeTypes); + + /** + * Returns the currently included mimetypes in the filter. + */ + Q_REQUIRED_RESULT QStringList contentMimeTypeInclusionFilters() const; + + /** + * Add search pattern + * @param pattern the search pattern to add + * @since 4.8.1 + */ + void setSearchPattern(const QString &pattern); + + /** + * Show only checked item + * @param checked only shows checked item if set as @c true + * @since 4.9 + */ + void setIncludeCheckedOnly(bool checked); + + /** + * Don't show unified mailbox + * @since 5.18.0 + */ + void setExcludeUnifiedMailBox(bool exclude); + +protected: + int columnCount(const QModelIndex &index) const override; + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +protected: + RecursiveCollectionFilterProxyModelPrivate *const d_ptr; + Q_DECLARE_PRIVATE(RecursiveCollectionFilterProxyModel) +}; + +} + diff --git a/src/core/models/selectionproxymodel.cpp b/src/core/models/selectionproxymodel.cpp new file mode 100644 index 0000000..83293d4 --- /dev/null +++ b/src/core/models/selectionproxymodel.cpp @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "selectionproxymodel.h" + +#include "entitytreemodel.h" + +using namespace Akonadi; + +namespace Akonadi +{ +class SelectionProxyModelPrivate +{ +public: + explicit SelectionProxyModelPrivate(SelectionProxyModel *selectionProxyModel) + : q_ptr(selectionProxyModel) + { + Q_Q(SelectionProxyModel); + const auto indexes = q->sourceRootIndexes(); + for (const auto &rootIndex : indexes) { + rootIndexAdded(rootIndex); + } + } + ~SelectionProxyModelPrivate() + { + Q_Q(SelectionProxyModel); + const auto indexes = q->sourceRootIndexes(); + for (const auto &rootIndex : indexes) { + rootIndexAboutToBeRemoved(rootIndex); + } + } + + /** + Increases the refcount of the Collection in @p newRootIndex + */ + void rootIndexAdded(const QModelIndex &newRootIndex) + { + Q_Q(SelectionProxyModel); + // newRootIndex is already in the sourceModel. + q->sourceModel()->setData(newRootIndex, QVariant(), EntityTreeModel::CollectionRefRole); + q->sourceModel()->fetchMore(newRootIndex); + } + + /** + Decreases the refcount of the Collection in @p removedRootIndex + */ + void rootIndexAboutToBeRemoved(const QModelIndex &removedRootIndex) + { + Q_Q(SelectionProxyModel); + q->sourceModel()->setData(removedRootIndex, QVariant(), EntityTreeModel::CollectionDerefRole); + } + + Q_DECLARE_PUBLIC(SelectionProxyModel) + SelectionProxyModel *q_ptr; +}; + +} // namespace Akonadi + +SelectionProxyModel::SelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent) + : KSelectionProxyModel(selectionModel, parent) + , d_ptr(new SelectionProxyModelPrivate(this)) +{ + connect(this, SIGNAL(rootIndexAdded(QModelIndex)), SLOT(rootIndexAdded(QModelIndex))); // clazy:exclude=old-style-connect + connect(this, SIGNAL(rootIndexAboutToBeRemoved(QModelIndex)), SLOT(rootIndexAboutToBeRemoved(QModelIndex))); // clazy:exclude=old-style-connect +} + +SelectionProxyModel::~SelectionProxyModel() +{ + delete d_ptr; +} + +#include "moc_selectionproxymodel.cpp" diff --git a/src/core/models/selectionproxymodel.h b/src/core/models/selectionproxymodel.h new file mode 100644 index 0000000..7e4aaf8 --- /dev/null +++ b/src/core/models/selectionproxymodel.h @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ +class SelectionProxyModelPrivate; + +/** + * @short A proxy model used to reference count selected Akonadi::Collection in a view + * + * Only selected Collections will be populated and monitored for changes. Unselected + * Collections will be ignored. + * + * This model extends KSelectionProxyModel to implement reference counting on the Collections + * in an EntityTreeModel. The EntityTreeModel must use LazyPopulation to enable + * SelectionProxyModel to work. + * + * By selecting a Collection, its reference count will be increased. A Collection in the + * EntityTreeModel which has a reference count of zero will ignore all signals from Monitor + * about items changed, inserted, removed etc, which can be expensive operations. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * // itemView + * // ^ + * // | + * // itemModel + * // | + * // flatModel + * // | + * // collectionView --> selectionModel + * // ^ ^ + * // | | + * // collectionFilter | + * // \______________model + * + * EntityTreeModel *model = new EntityTreeModel( ... ); + * + * // setup collection model + * EntityMimeTypeFilterModel *collectionFilter = new EntityMimeTypeFilterModel( this ); + * collectionFilter->setSourceModel( model ); + * collectionFilter->addMimeTypeInclusionFilter( Collection::mimeType() ); + * collectionFilter->setHeaderGroup( EntityTreeModel::CollectionTreeHeaders ); + * + * // setup collection view + * EntityTreeView *collectionView = new EntityTreeView( this ); + * collectionView->setModel( collectionFilter ); + * + * // setup selection model + * SelectionProxyModel *selectionModel = new SelectionProxyModel( collectionView->selectionModel(), this ); + * selectionModel->setSourceModel( model ); + * + * // setup item model + * KDescendantsProxyModel *flatModel = new KDescendantsProxyModel( this ); + * flatModel->setSourceModel( selectionModel ); + * + * EntityMimeTypeFilterModel *itemModel = new EntityMimeTypeFilterModel( this ); + * itemModel->setSourceModel( flatModel ); + * itemModel->setHeaderGroup( EntityTreeModel::ItemListHeaders ); + * itemModel->addMimeTypeExclusionFilter( Collection::mimeType() ); + * + * EntityListView *itemView = new EntityListView( this ); + * itemView->setModel( itemModel ); + * @endcode + * + * See \ref libakonadi_integration "Integration in your Application" for further guidance on the use of this class. + + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_EXPORT SelectionProxyModel : public KSelectionProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new selection proxy model. + * + * @param selectionModel The selection model of the source view. + * @param parent The parent object. + */ + explicit SelectionProxyModel(QItemSelectionModel *selectionModel, QObject *parent = nullptr); + ~SelectionProxyModel(); + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(SelectionProxyModel) + SelectionProxyModelPrivate *const d_ptr; + + Q_PRIVATE_SLOT(d_func(), void rootIndexAdded(const QModelIndex &)) + Q_PRIVATE_SLOT(d_func(), void rootIndexAboutToBeRemoved(const QModelIndex &)) + /// @endcond +}; + +} + diff --git a/src/core/models/statisticsproxymodel.cpp b/src/core/models/statisticsproxymodel.cpp new file mode 100644 index 0000000..a876547 --- /dev/null +++ b/src/core/models/statisticsproxymodel.cpp @@ -0,0 +1,322 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + 2016 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "statisticsproxymodel.h" +#include "akonadicore_debug.h" + +#include "collectionquotaattribute.h" +#include "collectionstatistics.h" +#include "collectionutils.h" +#include "entitydisplayattribute.h" +#include "entitytreemodel.h" + +#include +#include +#include + +#include +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN StatisticsProxyModel::Private +{ +public: + explicit Private(StatisticsProxyModel *parent) + : q(parent) + { + } + + void getCountRecursive(const QModelIndex &index, qint64 &totalSize) const + { + auto collection = qvariant_cast(index.data(EntityTreeModel::CollectionRole)); + // Do not assert on invalid collections, since a collection may be deleted + // in the meantime and deleted collections are invalid. + if (collection.isValid()) { + CollectionStatistics statistics = collection.statistics(); + totalSize += qMax(0LL, statistics.size()); + if (index.model()->hasChildren(index)) { + const int rowCount = index.model()->rowCount(index); + for (int row = 0; row < rowCount; row++) { + static const int column = 0; + getCountRecursive(index.model()->index(row, column, index), totalSize); + } + } + } + } + + int sourceColumnCount() const + { + return q->sourceModel()->columnCount(); + } + + QString toolTipForCollection(const QModelIndex &index, const Collection &collection) const + { + const QString bckColor = QApplication::palette().color(QPalette::ToolTipBase).name(); + const QString txtColor = QApplication::palette().color(QPalette::ToolTipText).name(); + + QString tip = QStringLiteral("\n"); + const QString textDirection = (QApplication::layoutDirection() == Qt::LeftToRight) ? QStringLiteral("left") : QStringLiteral("right"); + tip += QStringLiteral( + " \n" + " \n" + " \n") + .arg(txtColor, bckColor, index.data(Qt::DisplayRole).toString(), textDirection); + + tip += QStringLiteral( + " \n" + " \n") + .arg(iconPath) + .arg(icon_size_found); + + if (QApplication::layoutDirection() == Qt::LeftToRight) { + tip += tipInfo + QStringLiteral("" + "
\n" + "
\n" + " %3\n" + "
\n" + "
\n") + .arg(textDirection); + + QString tipInfo = QStringLiteral( + " %1: %2
\n" + " %3: %4

\n") + .arg(i18n("Total Messages")) + .arg(collection.statistics().count()) + .arg(i18n("Unread Messages")) + .arg(collection.statistics().unreadCount()); + + if (collection.hasAttribute()) { + const auto quota = collection.attribute(); + if (quota->currentValue() > -1 && quota->maximumValue() > 0) { + qreal percentage = (100.0 * quota->currentValue()) / quota->maximumValue(); + + if (qAbs(percentage) >= 0.01) { + QString percentStr = QString::number(percentage, 'f', 2); + tipInfo += QStringLiteral(" %1: %2%
\n").arg(i18n("Quota"), percentStr); + } + } + } + + KFormat formatter; + qint64 currentFolderSize(collection.statistics().size()); + tipInfo += QStringLiteral(" %1: %2
\n").arg(i18n("Storage Size"), formatter.formatByteSize(currentFolderSize)); + + qint64 totalSize = 0; + getCountRecursive(index, totalSize); + totalSize -= currentFolderSize; + if (totalSize > 0) { + tipInfo += QStringLiteral("%1: %2
").arg(i18n("Subfolder Storage Size"), formatter.formatByteSize(totalSize)); + } + + QString iconName = CollectionUtils::defaultIconName(collection); + if (collection.hasAttribute() && !collection.attribute()->iconName().isEmpty()) { + if (!collection.attribute()->activeIconName().isEmpty() && collection.statistics().unreadCount() > 0) { + iconName = collection.attribute()->activeIconName(); + } else { + iconName = collection.attribute()->iconName(); + } + } + + int iconSizes[] = {32, 22}; + int icon_size_found = 32; + + QString iconPath; + + for (int i = 0; i < 2; ++i) { + iconPath = KIconLoader::global()->iconPath(iconName, -iconSizes[i], true); + if (!iconPath.isEmpty()) { + icon_size_found = iconSizes[i]; + break; + } + } + + if (iconPath.isEmpty()) { + iconPath = KIconLoader::global()->iconPath(QStringLiteral("folder"), -32, false); + } + + QString tipIcon = QStringLiteral( + "
\n" + " \n" + "
\n" + "
").arg(textDirection) + tipIcon; + } else { + tip += tipIcon + QStringLiteral("").arg(textDirection) + tipInfo; + } + + tip += QLatin1String( + "
"); + + return tip; + } + + void _k_sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + + StatisticsProxyModel *const q; + + bool mToolTipEnabled = false; + bool mExtraColumnsEnabled = false; +}; + +void StatisticsProxyModel::Private::_k_sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + QModelIndex proxyTopLeft(q->mapFromSource(topLeft)); + QModelIndex proxyBottomRight(q->mapFromSource(bottomRight)); + // Emit data changed for the whole row (bug #222292) + if (mExtraColumnsEnabled && topLeft.column() == 0) { // in theory we could filter on roles, but ETM doesn't set any yet + const int lastColumn = q->columnCount() - 1; + proxyBottomRight = proxyBottomRight.sibling(proxyBottomRight.row(), lastColumn); + } + Q_EMIT q->dataChanged(proxyTopLeft, proxyBottomRight, roles); +} + +void StatisticsProxyModel::setSourceModel(QAbstractItemModel *model) +{ + if (sourceModel()) { + disconnect(sourceModel(), &QAbstractItemModel::dataChanged, this, nullptr); + } + KExtraColumnsProxyModel::setSourceModel(model); + if (model) { + // Disconnect the default handling of dataChanged in QIdentityProxyModel, so we can extend it to the whole row + disconnect(model, + SIGNAL(dataChanged(QModelIndex, QModelIndex, QVector)), // clazy:exclude=old-style-connect + this, + SLOT(_q_sourceDataChanged(QModelIndex, QModelIndex, QVector))); + connect(model, &QAbstractItemModel::dataChanged, this, [this](const auto &tl, const auto &br, const auto &roles) { + d->_k_sourceDataChanged(tl, br, roles); + }); + } +} + +StatisticsProxyModel::StatisticsProxyModel(QObject *parent) + : KExtraColumnsProxyModel(parent) + , d(new Private(this)) +{ + setExtraColumnsEnabled(true); +} + +StatisticsProxyModel::~StatisticsProxyModel() +{ + delete d; +} + +void StatisticsProxyModel::setToolTipEnabled(bool enable) +{ + d->mToolTipEnabled = enable; +} + +bool StatisticsProxyModel::isToolTipEnabled() const +{ + return d->mToolTipEnabled; +} + +void StatisticsProxyModel::setExtraColumnsEnabled(bool enable) +{ + if (d->mExtraColumnsEnabled == enable) { + return; + } + d->mExtraColumnsEnabled = enable; + if (enable) { + KExtraColumnsProxyModel::appendColumn(i18nc("number of unread entities in the collection", "Unread")); + KExtraColumnsProxyModel::appendColumn(i18nc("number of entities in the collection", "Total")); + KExtraColumnsProxyModel::appendColumn(i18nc("collection size", "Size")); + } else { + KExtraColumnsProxyModel::removeExtraColumn(2); + KExtraColumnsProxyModel::removeExtraColumn(1); + KExtraColumnsProxyModel::removeExtraColumn(0); + } +} + +bool StatisticsProxyModel::isExtraColumnsEnabled() const +{ + return d->mExtraColumnsEnabled; +} + +QVariant StatisticsProxyModel::extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + const QModelIndex firstColumn = index(row, 0, parent); + const auto collection = data(firstColumn, EntityTreeModel::CollectionRole).value(); + if (collection.isValid() && collection.statistics().count() >= 0) { + const CollectionStatistics stats = collection.statistics(); + if (extraColumn == 2) { + KFormat formatter; + return formatter.formatByteSize(stats.size()); + } else if (extraColumn == 1) { + return stats.count(); + } else if (extraColumn == 0) { + if (stats.unreadCount() > 0) { + return stats.unreadCount(); + } else { + return QString(); + } + } else { + qCWarning(AKONADICORE_LOG) << "We shouldn't get there for a column which is not total, unread or size."; + } + } + } break; + case Qt::TextAlignmentRole: { + return Qt::AlignRight; + } + default: + break; + } + return QVariant(); +} + +QVariant StatisticsProxyModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::ToolTipRole && d->mToolTipEnabled) { + const QModelIndex firstColumn = index.sibling(index.row(), 0); + const auto collection = data(firstColumn, EntityTreeModel::CollectionRole).value(); + + if (collection.isValid()) { + return d->toolTipForCollection(firstColumn, collection); + } + } + + return KExtraColumnsProxyModel::data(index, role); +} + +Qt::ItemFlags StatisticsProxyModel::flags(const QModelIndex &index_) const +{ + if (index_.column() >= d->sourceColumnCount()) { + const QModelIndex firstColumn = index_.sibling(index_.row(), 0); + return KExtraColumnsProxyModel::flags(firstColumn) + & (Qt::ItemIsSelectable | Qt::ItemIsDragEnabled // Allowed flags + | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled); + } + + return KExtraColumnsProxyModel::flags(index_); +} + +// Not sure this is still necessary.... +QModelIndexList StatisticsProxyModel::match(const QModelIndex &start, int role, const QVariant &value, int hits, Qt::MatchFlags flags) const +{ + if (role < Qt::UserRole) { + return KExtraColumnsProxyModel::match(start, role, value, hits, flags); + } + + QModelIndexList list; + QModelIndex proxyIndex; + const auto matches = sourceModel()->match(mapToSource(start), role, value, hits, flags); + for (const auto &idx : matches) { + proxyIndex = mapFromSource(idx); + if (proxyIndex.isValid()) { + list.push_back(proxyIndex); + } + } + + return list; +} + +#include "moc_statisticsproxymodel.cpp" diff --git a/src/core/models/statisticsproxymodel.h b/src/core/models/statisticsproxymodel.h new file mode 100644 index 0000000..99b054d --- /dev/null +++ b/src/core/models/statisticsproxymodel.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2009 Kevin Ottens + 2016 David Faure s + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short A proxy model that exposes collection statistics through extra columns. + * + * This class can be used on top of an EntityTreeModel to display extra columns + * summarizing statistics of collections. + * + * @code + * + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel( ... ); + * + * Akonadi::StatisticsProxyModel *proxy = new Akonadi::StatisticsProxyModel(); + * proxy->setSourceModel( model ); + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * view->setModel( proxy ); + * + * @endcode + * + * @author Kevin Ottens , now maintained by David Faure + * @since 4.4 + */ +class AKONADICORE_EXPORT StatisticsProxyModel : public KExtraColumnsProxyModel +{ + Q_OBJECT + +public: + /** + * Creates a new statistics proxy model. + * + * @param parent The parent object. + */ + explicit StatisticsProxyModel(QObject *parent = nullptr); + + /** + * Destroys the statistics proxy model. + */ + ~StatisticsProxyModel() override; + + /** + * @param enable Display tooltips + * By default, tooltips are disabled. + */ + void setToolTipEnabled(bool enable); + + /** + * Return true if we display tooltips, otherwise false + */ + Q_REQUIRED_RESULT bool isToolTipEnabled() const; + + /** + * @param enable Display extra statistics columns + * By default, the extra columns are enabled. + */ + void setExtraColumnsEnabled(bool enable); + + /** + * Return true if we display extra statistics columns, otherwise false + */ + Q_REQUIRED_RESULT bool isExtraColumnsEnabled() const; + + Q_REQUIRED_RESULT QVariant extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role) const override; + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + + Q_REQUIRED_RESULT virtual QModelIndexList match(const QModelIndex &start, + int role, + const QVariant &value, + int hits = 1, + Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWith | Qt::MatchWrap)) const override; + + void setSourceModel(QAbstractItemModel *model) override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/core/models/subscriptionmodel.cpp b/src/core/models/subscriptionmodel.cpp new file mode 100644 index 0000000..7c66338 --- /dev/null +++ b/src/core/models/subscriptionmodel.cpp @@ -0,0 +1,209 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionutils.h" +#include "entityhiddenattribute.h" +#include "entitytreemodel.h" +#include "specialcollectionattribute.h" +#include "subscriptionmodel_p.h" + +#include "akonadicore_debug.h" +#include +#include + +#include +#include + +using namespace Akonadi; +using namespace AkRanges; + +namespace +{ +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + FilterProxyModel() + { + setDynamicSortFilter(true); + } + + void setShowHidden(bool showHidden) + { + if (mShowHidden != showHidden) { + mShowHidden = showHidden; + invalidateFilter(); + } + } + + bool showHidden() const + { + return mShowHidden; + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override + { + const auto source_index = sourceModel()->index(source_row, 0, source_parent); + const auto col = source_index.data(EntityTreeModel::CollectionRole).value(); + if (mShowHidden) { + return true; + } + + return !col.hasAttribute(); + } + +private: + bool mShowHidden = false; +}; + +} // namespace + +/** + * @internal + */ +class SubscriptionModel::Private +{ +public: + explicit Private(Monitor *monitor) + : etm(monitor) + { + etm.setShowSystemEntities(true); // show hidden collections + etm.setItemPopulationStrategy(EntityTreeModel::NoItemPopulation); + etm.setCollectionFetchStrategy(EntityTreeModel::FetchCollectionsRecursive); + + proxy.setSourceModel(&etm); + } + + Collection::List changedSubscriptions(bool subscribed) const + { + return Views::range(subscriptions.constKeyValueBegin(), subscriptions.constKeyValueEnd()) | Views::filter([subscribed](const auto &val) { + return val.second == subscribed; + }) + | Views::transform([](const auto &val) { + return Collection{val.first}; + }) + | Actions::toQVector; + } + + bool isSubscribable(const Collection &col) + { + if (CollectionUtils::isStructural(col) || col.isVirtual() || CollectionUtils::isUnifiedMailbox(col)) { + return false; + } + if (col.hasAttribute()) { + return false; + } + if (col.contentMimeTypes().isEmpty()) { + return false; + } + return true; + } + +public: + EntityTreeModel etm; + FilterProxyModel proxy; + QHash subscriptions; +}; + +SubscriptionModel::SubscriptionModel(Monitor *monitor, QObject *parent) + : QIdentityProxyModel(parent) + , d(new Private(monitor)) +{ + QIdentityProxyModel::setSourceModel(&d->proxy); + + connect(&d->etm, &EntityTreeModel::collectionTreeFetched, this, &SubscriptionModel::modelLoaded); +} + +SubscriptionModel::~SubscriptionModel() = default; + +void SubscriptionModel::setSourceModel(QAbstractItemModel * /*sourceModel*/) +{ + // no-op +} + +QVariant SubscriptionModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case Qt::CheckStateRole: { + const auto col = index.data(EntityTreeModel::CollectionRole).value(); + if (!d->isSubscribable(col)) { + return QVariant(); + } + // Check if we have "override" for the subscription state stored + const auto it = d->subscriptions.constFind(col.id()); + if (it != d->subscriptions.cend()) { + return (*it) ? Qt::Checked : Qt::Unchecked; + } else { + // Fallback to the current state of the collection + return col.enabled() ? Qt::Checked : Qt::Unchecked; + } + } + case SubscriptionChangedRole: { + const auto col = index.data(EntityTreeModel::CollectionIdRole).toLongLong(); + return d->subscriptions.contains(col); + } + case Qt::FontRole: { + const auto col = index.data(EntityTreeModel::CollectionIdRole).toLongLong(); + auto font = QIdentityProxyModel::data(index, role).value(); + font.setBold(d->subscriptions.contains(col)); + return font; + } + } + + return QIdentityProxyModel::data(index, role); +} + +Qt::ItemFlags SubscriptionModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = QIdentityProxyModel::flags(index); + const auto col = index.data(EntityTreeModel::CollectionRole).value(); + if (d->isSubscribable(col)) { + return flags | Qt::ItemIsUserCheckable; + } + return flags; +} + +bool SubscriptionModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (role == Qt::CheckStateRole) { + const auto col = index.data(EntityTreeModel::CollectionRole).value(); + if (!d->isSubscribable(col)) { + return true; // No change + } + if (col.enabled() == (value == Qt::Checked)) { // No change compared to the underlying model + d->subscriptions.remove(col.id()); + } else { + d->subscriptions[col.id()] = (value == Qt::Checked); + } + Q_EMIT dataChanged(index, index); + return true; + } + return QIdentityProxyModel::setData(index, value, role); +} + +Akonadi::Collection::List SubscriptionModel::subscribed() const +{ + return d->changedSubscriptions(true); +} + +Akonadi::Collection::List SubscriptionModel::unsubscribed() const +{ + return d->changedSubscriptions(false); +} + +void SubscriptionModel::setShowHiddenCollections(bool showHidden) +{ + d->proxy.setShowHidden(showHidden); +} + +bool SubscriptionModel::showHiddenCollections() const +{ + return d->proxy.showHidden(); +} + +#include "subscriptionmodel.moc" diff --git a/src/core/models/subscriptionmodel_p.h b/src/core/models/subscriptionmodel_p.h new file mode 100644 index 0000000..4be0ad7 --- /dev/null +++ b/src/core/models/subscriptionmodel_p.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +#include "entitytreemodel.h" + +namespace Akonadi +{ +class Monitor; + +/** + * @internal + * + * A proxy model to be used on top of ETM to display a checkable tree of collections + * for user to select which collections should be locally subscribed. + * + * Used in SubscriptionDialog + */ +class AKONADICORE_EXPORT SubscriptionModel : public QIdentityProxyModel +{ + Q_OBJECT +public: + /** Additional roles. */ + enum Roles { + SubscriptionChangedRole = EntityTreeModel::UserRole + 1 ///< Indicate the subscription status has been changed. + }; + + /** + Create a new subscription model. + @param parent The parent object. + */ + explicit SubscriptionModel(Monitor *monitor, QObject *parent = nullptr); + + /** + Destructor. + */ + ~SubscriptionModel() override; + + /** + * Sets a source model for the SubscriptionModel. + * + * Should be based on an ETM with only collections. + */ + void setSourceModel(QAbstractItemModel *model) override; + + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + Q_REQUIRED_RESULT Collection::List subscribed() const; + Q_REQUIRED_RESULT Collection::List unsubscribed() const; + + /** + * @param showHidden shows hidden collections if set as @c true + * @since: 4.9 + */ + void setShowHiddenCollections(bool showHidden); + Q_REQUIRED_RESULT bool showHiddenCollections() const; + +Q_SIGNALS: + void modelLoaded(); + +private: + class Private; + QScopedPointer const d; +}; + +} + diff --git a/src/core/models/tagmodel.cpp b/src/core/models/tagmodel.cpp new file mode 100644 index 0000000..48bb220 --- /dev/null +++ b/src/core/models/tagmodel.cpp @@ -0,0 +1,167 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagmodel.h" +#include "tagattribute.h" +#include "tagmodel_p.h" + +#include +#include + +using namespace Akonadi; + +TagModel::TagModel(Monitor *recorder, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(new TagModelPrivate(this)) +{ + Q_D(TagModel); + d->init(recorder); +} + +TagModel::TagModel(Monitor *recorder, TagModelPrivate *dd, QObject *parent) + : QAbstractItemModel(parent) + , d_ptr(dd) +{ + Q_D(TagModel); + d->init(recorder); +} + +TagModel::~TagModel() +{ + delete d_ptr; +} + +int TagModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid() && parent.column() != 0) { + return 0; + } + + return 1; +} + +int TagModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const TagModel); + + Tag::Id parentTagId = -1; + if (parent.isValid()) { + parentTagId = d->mChildTags[parent.internalId()].at(parent.row()).id(); + } + + return d->mChildTags[parentTagId].count(); +} + +QVariant TagModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Vertical) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return i18n("Tag"); + } + } + + return QAbstractItemModel::headerData(section, orientation, role); +} + +QVariant TagModel::data(const QModelIndex &index, int role) const +{ + Q_D(const TagModel); + + if (!index.isValid()) { + return QVariant(); + } + const Tag tag = d->tagForIndex(index); + if (!tag.isValid()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: // fall-through + case NameRole: + return tag.name(); + case IdRole: + return tag.id(); + case GIDRole: + return tag.gid(); + case ParentRole: + return QVariant::fromValue(tag.parent()); + case TagRole: + return QVariant::fromValue(tag); + case Qt::DecorationRole: { + if (const auto attr = tag.attribute()) { + return QIcon::fromTheme(attr->iconName()); + } else { + return QVariant(); + } + } + } + + return QVariant(); +} + +QModelIndex TagModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_D(const TagModel); + + qint64 parentId = -1; + if (parent.isValid()) { + const Tag parentTag = d->tagForIndex(parent); + parentId = parentTag.id(); + } + + const Tag::List &children = d->mChildTags.value(parentId); + if (row >= children.count()) { + return QModelIndex(); + } + + return createIndex(row, column, static_cast(parentId)); +} + +QModelIndex TagModel::parent(const QModelIndex &child) const +{ + Q_D(const TagModel); + + if (!child.isValid()) { + return QModelIndex(); + } + + const qint64 parentId = child.internalId(); + return d->indexForTag(parentId); +} + +Qt::ItemFlags TagModel::flags(const QModelIndex &index) const +{ + Q_UNUSED(index) + + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable; +} + +bool TagModel::insertColumns(int /*column*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +bool TagModel::insertRows(int /*row*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +bool TagModel::removeColumns(int /*column*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +bool TagModel::removeRows(int /*row*/, int /*count*/, const QModelIndex & /*parent*/) +{ + return false; +} + +#include "moc_tagmodel.cpp" diff --git a/src/core/models/tagmodel.h b/src/core/models/tagmodel.h new file mode 100644 index 0000000..7b55924 --- /dev/null +++ b/src/core/models/tagmodel.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadicore_export.h" +#include "tag.h" + +namespace Akonadi +{ +class Monitor; +class TagModelPrivate; + +class AKONADICORE_EXPORT TagModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + NameRole, + TypeRole, + GIDRole, + ParentRole, + TagRole, + + UserRole = Qt::UserRole + 500, + TerminalUserRole = 2000, + EndRole = 65535 + }; + + explicit TagModel(Monitor *recorder, QObject *parent = nullptr); + ~TagModel() override; + + Q_REQUIRED_RESULT int columnCount(const QModelIndex &parent = QModelIndex()) const override; + Q_REQUIRED_RESULT int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + Q_REQUIRED_RESULT QVariant data(const QModelIndex &index, int role) const override; + Q_REQUIRED_RESULT QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + Q_REQUIRED_RESULT Qt::ItemFlags flags(const QModelIndex &index) const override; + /* + virtual Qt::DropActions supportedDropActions() const; + virtual QMimeData* mimeData( const QModelIndexList &indexes ) const; + virtual bool dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent ); + */ + + Q_REQUIRED_RESULT QModelIndex parent(const QModelIndex &child) const override; + Q_REQUIRED_RESULT QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + +protected: + Q_DECLARE_PRIVATE(TagModel) + TagModelPrivate *d_ptr; + + TagModel(Monitor *recorder, TagModelPrivate *dd, QObject *parent); + +Q_SIGNALS: + void populated(); + +private: + bool insertRows(int row, int count, const QModelIndex &index = QModelIndex()) override; + bool insertColumns(int column, int count, const QModelIndex &index = QModelIndex()) override; + bool removeColumns(int column, int count, const QModelIndex &index = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex &index = QModelIndex()) override; + // Used by FakeAkonadiServerCommand::connectForwardingSignals (tagmodeltest) + Q_PRIVATE_SLOT(d_func(), void tagsFetched(const Akonadi::Tag::List &tags)) + Q_PRIVATE_SLOT(d_func(), void monitoredTagAdded(const Akonadi::Tag &tag)) + Q_PRIVATE_SLOT(d_func(), void monitoredTagRemoved(const Akonadi::Tag &tag)) + Q_PRIVATE_SLOT(d_func(), void monitoredTagChanged(const Akonadi::Tag &tag)) +}; +} + diff --git a/src/core/models/tagmodel_p.cpp b/src/core/models/tagmodel_p.cpp new file mode 100644 index 0000000..4075da0 --- /dev/null +++ b/src/core/models/tagmodel_p.cpp @@ -0,0 +1,235 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vr ??til + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagmodel_p.h" +#include "tagmodel.h" + +#include "monitor.h" +#include "session.h" +#include "tagfetchjob.h" + +#include "akonadicore_debug.h" + +#include + +using namespace Akonadi; + +TagModelPrivate::TagModelPrivate(TagModel *parent) + : q_ptr(parent) +{ + // Root tag + mTags.insert(-1, Tag()); +} + +void TagModelPrivate::init(Monitor *monitor) +{ + Q_Q(TagModel); + + mMonitor = monitor; + mSession = mMonitor->session(); + + q->connect(mMonitor, &Monitor::tagAdded, q, [this](const Tag &tag) { + monitoredTagAdded(tag); + }); + q->connect(mMonitor, &Monitor::tagChanged, q, [this](const Tag &tag) { + monitoredTagChanged(tag); + }); + q->connect(mMonitor, &Monitor::tagRemoved, q, [this](const Tag &tag) { + monitoredTagRemoved(tag); + }); + + // Delay starting the job to allow unit-tests to set up fake stuff + QTimer::singleShot(0, q, [this] { + fillModel(); + }); +} + +void TagModelPrivate::fillModel() +{ + Q_Q(TagModel); + + auto fetchJob = new TagFetchJob(mSession); + fetchJob->setFetchScope(mMonitor->tagFetchScope()); + q->connect(fetchJob, &TagFetchJob::tagsReceived, q, [this](const auto &tags) { + tagsFetched(tags); + }); + q->connect(fetchJob, &KJob::result, q, [this](KJob *job) { + tagsFetchDone(job); + }); +} + +QModelIndex TagModelPrivate::indexForTag(const qint64 tagId) const +{ + Q_Q(const TagModel); + + if (!mTags.contains(tagId)) { + return QModelIndex(); + } + + const Tag tag = mTags.value(tagId); + if (!tag.isValid()) { + return QModelIndex(); + } + + const Tag::Id parentId = tag.parent().id(); + const int row = mChildTags.value(parentId).indexOf(tag); + if (row != -1) { + return q->createIndex(row, 0, static_cast(parentId)); + } + + return QModelIndex(); +} + +Tag TagModelPrivate::tagForIndex(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Tag(); + } + + const Tag::Id parentId = index.internalId(); + const Tag::List &children = mChildTags.value(parentId); + return children.value(index.row()); +} + +void TagModelPrivate::monitoredTagAdded(const Tag &tag) +{ + Q_Q(TagModel); + + const qint64 parentId = tag.parent().id(); + + // Parent not yet in model, defer for later + if (!mTags.contains(parentId)) { + Tag::List &list = mPendingTags[parentId]; + list.append(tag); + return; + } + + Tag::List &children = mChildTags[parentId]; + + q->beginInsertRows(indexForTag(parentId), children.count(), children.count()); + mTags.insert(tag.id(), tag); + children.append(tag); + q->endInsertRows(); + + // If there are any child tags waiting for this parent, insert them + if (mPendingTags.contains(tag.id())) { + const Tag::List pendingChildren = mPendingTags.take(tag.id()); + for (const Tag &pendingTag : pendingChildren) { + monitoredTagAdded(pendingTag); + } + } +} + +void TagModelPrivate::removeTagsRecursively(qint64 tagId) +{ + const Tag tag = mTags.value(tagId); + + // Remove all children first + const Tag::List childTags = mChildTags.take(tagId); + for (const Tag &child : childTags) { + removeTagsRecursively(child.id()); + } + + // Remove the actual tag + Tag::List &siblings = mChildTags[tag.parent().id()]; + siblings.removeOne(tag); + mTags.remove(tag.id()); +} + +void TagModelPrivate::monitoredTagRemoved(const Tag &tag) +{ + Q_Q(TagModel); + + if (!tag.isValid()) { + qCWarning(AKONADICORE_LOG) << "Attempting to remove root tag?"; + return; + } + + // Better lookup parent in our cache + auto iter = mTags.constFind(tag.id()); + if (iter == mTags.cend()) { + qCWarning(AKONADICORE_LOG) << "Got removal notification for unknown tag" << tag.id(); + return; + } + + const qint64 parentId = iter->parent().id(); + + const Tag::List &siblings = mChildTags[parentId]; + const int pos = siblings.indexOf(tag); + Q_ASSERT(pos != -1); + + q->beginRemoveRows(indexForTag(parentId), pos, pos); + removeTagsRecursively(tag.id()); + q->endRemoveRows(); +} + +void TagModelPrivate::monitoredTagChanged(const Tag &tag) +{ + Q_Q(TagModel); + + if (!mTags.contains(tag.id())) { + qCWarning(AKONADICORE_LOG) << "Got change notifications for unknown tag" << tag.id(); + return; + } + + const Tag oldTag = mTags.value(tag.id()); + // Replace existing tag in cache + mTags.insert(tag.id(), tag); + + // Check whether the tag has been reparented + const qint64 oldParent = oldTag.parent().id(); + const qint64 newParent = tag.parent().id(); + if (oldParent != newParent) { + const QModelIndex sourceParent = indexForTag(oldParent); + const int sourcePos = mChildTags.value(oldParent).indexOf(oldTag); + const QModelIndex destParent = indexForTag(newParent); + const int destPos = mChildTags.value(newParent).count(); + + q->beginMoveRows(sourceParent, sourcePos, sourcePos, destParent, destPos); + Tag::List &oldSiblings = mChildTags[oldParent]; + oldSiblings.removeAt(sourcePos); + Tag::List &newSiblings = mChildTags[newParent]; + newSiblings.append(tag); + q->endMoveRows(); + } else { + Tag::List &children = mChildTags[oldParent]; + const int sourcePos = children.indexOf(oldTag); + if (sourcePos != -1) { + children[sourcePos] = tag; + } + + const QModelIndex index = indexForTag(tag.id()); + Q_EMIT q->dataChanged(index, index); + } +} + +void TagModelPrivate::tagsFetched(const Tag::List &tags) +{ + for (const Tag &tag : tags) { + monitoredTagAdded(tag); + } +} + +void TagModelPrivate::tagsFetchDone(KJob *job) +{ + Q_Q(TagModel); + + if (job->error()) { + qCWarning(AKONADICORE_LOG) << job->errorString(); + return; + } + + if (!mPendingTags.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Fetched all tags from server, but there are still" << mPendingTags.count() << "orphan tags:"; + for (auto it = mPendingTags.cbegin(), e = mPendingTags.cend(); it != e; ++it) { + qCWarning(AKONADICORE_LOG) << "tagId = " << it.key() << "; with list count =" << it.value().count(); + } + + return; + } + + Q_EMIT q->populated(); +} diff --git a/src/core/models/tagmodel_p.h b/src/core/models/tagmodel_p.h new file mode 100644 index 0000000..a41d93e --- /dev/null +++ b/src/core/models/tagmodel_p.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "tag.h" + +class QModelIndex; +class KJob; + +namespace Akonadi +{ +class Monitor; +class TagModel; +class Session; + +class TagModelPrivate +{ +public: + explicit TagModelPrivate(TagModel *parent); + + void init(Monitor *recorder); + void fillModel(); + + void tagsFetchDone(KJob *job); + void tagsFetched(const Akonadi::Tag::List &tags); + void monitoredTagAdded(const Akonadi::Tag &tag); + void monitoredTagChanged(const Akonadi::Tag &tag); + void monitoredTagRemoved(const Akonadi::Tag &tag); + + Q_REQUIRED_RESULT QModelIndex indexForTag(qint64 tagId) const; + Q_REQUIRED_RESULT Tag tagForIndex(const QModelIndex &index) const; + + void removeTagsRecursively(qint64 parentTag); + + Monitor *mMonitor = nullptr; + Session *mSession = nullptr; + + QHash mChildTags; + QHash mTags; + + QHash mPendingTags; + +protected: + Q_DECLARE_PUBLIC(TagModel) + TagModel *q_ptr; +}; +} + diff --git a/src/core/models/trashfilterproxymodel.cpp b/src/core/models/trashfilterproxymodel.cpp new file mode 100644 index 0000000..d223398 --- /dev/null +++ b/src/core/models/trashfilterproxymodel.cpp @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "trashfilterproxymodel.h" +#include "entitydeletedattribute.h" +#include "entitytreemodel.h" +#include "item.h" + +using namespace Akonadi; + +class TrashFilterProxyModel::TrashFilterProxyModelPrivate +{ +public: + TrashFilterProxyModelPrivate() + { + } + bool mTrashIsShown = false; +}; + +TrashFilterProxyModel::TrashFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) + , d_ptr(new TrashFilterProxyModelPrivate()) +{ + setRecursiveFilteringEnabled(true); +} + +TrashFilterProxyModel::~TrashFilterProxyModel() +{ + delete d_ptr; +} + +void TrashFilterProxyModel::showTrash(bool enable) +{ + Q_D(TrashFilterProxyModel); + d->mTrashIsShown = enable; + invalidateFilter(); +} + +bool TrashFilterProxyModel::trashIsShown() const +{ + Q_D(const TrashFilterProxyModel); + return d->mTrashIsShown; +} + +bool TrashFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + Q_D(const TrashFilterProxyModel); + const QModelIndex &index = sourceModel()->index(sourceRow, 0, sourceParent); + const Item &item = index.data(EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + if (item.hasAttribute()) { + return d->mTrashIsShown; + } + } + const Collection &collection = index.data(EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + if (collection.hasAttribute()) { + return d->mTrashIsShown; + } + } + return !d->mTrashIsShown; +} diff --git a/src/core/models/trashfilterproxymodel.h b/src/core/models/trashfilterproxymodel.h new file mode 100644 index 0000000..4a5a747 --- /dev/null +++ b/src/core/models/trashfilterproxymodel.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Filter model which hides/shows entities marked as trash + * + * Filter model which either hides all entities marked as trash, or the ones not marked. + * Subentities of collections marked as trash are also shown in the trash. + * + * The Base model must be an EntityTreeModel and the EntityDeletedAttribute must be available. + * + * Example: + * + * @code + * + * ChangeRecorder *monitor = new Akonadi::ChangeRecorder( this ); + * monitor->itemFetchScope().fetchAttribute(true); + * + * Akonadi::EntityTreeModel *sourcemodel = new Akonadi::EntityTreeModel(monitor, this); + * + * TrashFilterProxyModel *model = new TrashFilterProxyModel(this); + * model->setDynamicSortFilter(true); + * model->setSourceModel(sourcemodel); + * + * @endcode + * + * @author Christian Mollekopf + * @since 4.8 + */ +class AKONADICORE_EXPORT TrashFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit TrashFilterProxyModel(QObject *parent = nullptr); + ~TrashFilterProxyModel() override; + + void showTrash(bool enable); + Q_REQUIRED_RESULT bool trashIsShown() const; + +protected: + /** + * Sort filter criterias, according to how expensive the operation is + */ + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + /// @cond PRIVATE + class TrashFilterProxyModelPrivate; + TrashFilterProxyModelPrivate *const d_ptr; + Q_DECLARE_PRIVATE(TrashFilterProxyModel) + /// @endcond +}; + +} + diff --git a/src/core/monitor.cpp b/src/core/monitor.cpp new file mode 100644 index 0000000..8916741 --- /dev/null +++ b/src/core/monitor.cpp @@ -0,0 +1,376 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "monitor.h" +#include "monitor_p.h" + +#include "changemediator_p.h" +#include "collectionfetchscope.h" +#include "itemfetchjob.h" +#include "session.h" + +#include + +#include + +using namespace Akonadi; +using namespace AkRanges; + +Monitor::Monitor(QObject *parent) + : QObject(parent) + , d_ptr(new MonitorPrivate(nullptr, this)) +{ + d_ptr->init(); + d_ptr->connectToNotificationManager(); + + ChangeMediator::registerMonitor(this); +} + +/// @cond PRIVATE +Monitor::Monitor(MonitorPrivate *d, QObject *parent) + : QObject(parent) + , d_ptr(d) +{ + d_ptr->init(); + d_ptr->connectToNotificationManager(); + + ChangeMediator::registerMonitor(this); +} +/// @endcond + +Monitor::~Monitor() +{ + ChangeMediator::unregisterMonitor(this); + + delete d_ptr; +} + +void Monitor::setCollectionMonitored(const Collection &collection, bool monitored) +{ + Q_D(Monitor); + if (!d->collections.contains(collection) && monitored) { + d->collections << collection; + d->pendingModification.startMonitoringCollection(collection.id()); + d->scheduleSubscriptionUpdate(); + } else if (!monitored) { + if (d->collections.removeAll(collection)) { + d->pendingModification.stopMonitoringCollection(collection.id()); + d->scheduleSubscriptionUpdate(); + } + } + + Q_EMIT collectionMonitored(collection, monitored); // NOLINT(readability-misleading-indentation): false positive +} + +void Monitor::setItemMonitored(const Item &item, bool monitored) +{ + Q_D(Monitor); + if (!d->items.contains(item.id()) && monitored) { + d->items.insert(item.id()); + d->pendingModification.startMonitoringItem(item.id()); + d->scheduleSubscriptionUpdate(); + } else if (!monitored) { + if (d->items.remove(item.id())) { + d->pendingModification.stopMonitoringItem(item.id()); + d->scheduleSubscriptionUpdate(); + } + } + + Q_EMIT itemMonitored(item, monitored); // NOLINT(readability-misleading-indentation): false positive +} + +void Monitor::setResourceMonitored(const QByteArray &resource, bool monitored) +{ + Q_D(Monitor); + if (!d->resources.contains(resource) && monitored) { + d->resources.insert(resource); + d->pendingModification.startMonitoringResource(resource); + d->scheduleSubscriptionUpdate(); + } else if (!monitored) { + if (d->resources.remove(resource)) { + d->pendingModification.stopMonitoringResource(resource); + d->scheduleSubscriptionUpdate(); + } + } + + Q_EMIT resourceMonitored(resource, monitored); // NOLINT(readability-misleading-indentation): false positive +} + +void Monitor::setMimeTypeMonitored(const QString &mimetype, bool monitored) +{ + Q_D(Monitor); + if (!d->mimetypes.contains(mimetype) && monitored) { + d->mimetypes.insert(mimetype); + d->pendingModification.startMonitoringMimeType(mimetype); + d->scheduleSubscriptionUpdate(); + } else if (!monitored) { + if (d->mimetypes.remove(mimetype)) { + d->pendingModification.stopMonitoringMimeType(mimetype); + d->scheduleSubscriptionUpdate(); + } + } + + Q_EMIT mimeTypeMonitored(mimetype, monitored); // NOLINT(readability-misleading-indentation): false positive +} + +void Monitor::setTagMonitored(const Akonadi::Tag &tag, bool monitored) +{ + Q_D(Monitor); + if (!d->tags.contains(tag.id()) && monitored) { + d->tags.insert(tag.id()); + d->pendingModification.startMonitoringTag(tag.id()); + d->scheduleSubscriptionUpdate(); + } else if (!monitored) { + if (d->tags.remove(tag.id())) { + d->pendingModification.stopMonitoringTag(tag.id()); + d->scheduleSubscriptionUpdate(); + } + } + + Q_EMIT tagMonitored(tag, monitored); // NOLINT(readability-misleading-indentation): false positive +} + +void Monitor::setTypeMonitored(Monitor::Type type, bool monitored) +{ + Q_D(Monitor); + if (!d->types.contains(type) && monitored) { + d->types.insert(type); + d->pendingModification.startMonitoringType(MonitorPrivate::monitorTypeToProtocol(type)); + d->scheduleSubscriptionUpdate(); + } else if (!monitored) { + if (d->types.remove(type)) { + d->pendingModification.stopMonitoringType(MonitorPrivate::monitorTypeToProtocol(type)); + d->scheduleSubscriptionUpdate(); + } + } + + Q_EMIT typeMonitored(type, monitored); // NOLINT(readability-misleading-indentation): false positive +} + +void Akonadi::Monitor::setAllMonitored(bool monitored) +{ + Q_D(Monitor); + if (d->monitorAll == monitored) { + return; + } + + d->monitorAll = monitored; + + d->pendingModification.setAllMonitored(monitored); + d->scheduleSubscriptionUpdate(); + + Q_EMIT allMonitored(monitored); +} + +void Monitor::setExclusive(bool exclusive) +{ + Q_D(Monitor); + d->exclusive = exclusive; + d->pendingModification.setIsExclusive(exclusive); + d->scheduleSubscriptionUpdate(); +} + +bool Monitor::exclusive() const +{ + Q_D(const Monitor); + return d->exclusive; +} + +void Monitor::ignoreSession(Session *session) +{ + Q_D(Monitor); + + if (!d->sessions.contains(session->sessionId())) { + d->sessions << session->sessionId(); + connect(session, &Session::destroyed, this, [d](QObject *o) { + d->slotSessionDestroyed(o); + }); + d->pendingModification.startIgnoringSession(session->sessionId()); + d->scheduleSubscriptionUpdate(); + } +} + +void Monitor::fetchCollection(bool enable) +{ + Q_D(Monitor); + d->fetchCollection = enable; +} + +void Monitor::fetchCollectionStatistics(bool enable) +{ + Q_D(Monitor); + d->fetchCollectionStatistics = enable; +} + +void Monitor::setItemFetchScope(const ItemFetchScope &fetchScope) +{ + Q_D(Monitor); + d->mItemFetchScope = fetchScope; + d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::ItemFetchScope; + d->scheduleSubscriptionUpdate(); +} + +ItemFetchScope &Monitor::itemFetchScope() +{ + Q_D(Monitor); + d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::ItemFetchScope; + d->scheduleSubscriptionUpdate(); + return d->mItemFetchScope; +} + +void Monitor::fetchChangedOnly(bool enable) +{ + Q_D(Monitor); + d->mFetchChangedOnly = enable; +} + +void Monitor::setCollectionFetchScope(const CollectionFetchScope &fetchScope) +{ + Q_D(Monitor); + d->mCollectionFetchScope = fetchScope; + d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::CollectionFetchScope; + d->scheduleSubscriptionUpdate(); +} + +CollectionFetchScope &Monitor::collectionFetchScope() +{ + Q_D(Monitor); + d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::CollectionFetchScope; + d->scheduleSubscriptionUpdate(); + return d->mCollectionFetchScope; +} + +void Monitor::setTagFetchScope(const TagFetchScope &fetchScope) +{ + Q_D(Monitor); + d->mTagFetchScope = fetchScope; + d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::TagFetchScope; + d->scheduleSubscriptionUpdate(); +} + +TagFetchScope &Monitor::tagFetchScope() +{ + Q_D(Monitor); + d->pendingModificationChanges |= Protocol::ModifySubscriptionCommand::TagFetchScope; + d->scheduleSubscriptionUpdate(); + return d->mTagFetchScope; +} + +Akonadi::Collection::List Monitor::collectionsMonitored() const +{ + Q_D(const Monitor); + return d->collections; +} + +QVector Monitor::itemsMonitoredEx() const +{ + Q_D(const Monitor); + QVector result; + result.reserve(d->items.size()); + std::copy(d->items.begin(), d->items.end(), std::back_inserter(result)); + return result; +} + +int Monitor::numItemsMonitored() const +{ + Q_D(const Monitor); + return d->items.size(); +} + +QVector Monitor::tagsMonitored() const +{ + Q_D(const Monitor); + QVector result; + result.reserve(d->tags.size()); + std::copy(d->tags.begin(), d->tags.end(), std::back_inserter(result)); + return result; +} + +QVector Monitor::typesMonitored() const +{ + Q_D(const Monitor); + QVector result; + result.reserve(d->types.size()); + std::copy(d->types.begin(), d->types.end(), std::back_inserter(result)); + return result; +} + +QStringList Monitor::mimeTypesMonitored() const +{ + Q_D(const Monitor); + return d->mimetypes | Actions::toQList; +} + +int Monitor::numMimeTypesMonitored() const +{ + Q_D(const Monitor); + return d->mimetypes.count(); +} + +QList Monitor::resourcesMonitored() const +{ + Q_D(const Monitor); + return d->resources | Actions::toQList; +} + +int Monitor::numResourcesMonitored() const +{ + Q_D(const Monitor); + return d->resources.count(); +} + +bool Monitor::isAllMonitored() const +{ + Q_D(const Monitor); + return d->monitorAll; +} + +void Monitor::setSession(Akonadi::Session *session) +{ + Q_D(Monitor); + if (session == d->session) { + return; + } + + if (!session) { + d->session = Session::defaultSession(); + } else { + d->session = session; + } + + d->itemCache->setSession(d->session); + d->collectionCache->setSession(d->session); + d->tagCache->setSession(d->session); + + // Reconnect with a new session + d->connectToNotificationManager(); +} + +Session *Monitor::session() const +{ + Q_D(const Monitor); + return d->session; +} + +void Monitor::setCollectionMoveTranslationEnabled(bool enabled) +{ + Q_D(Monitor); + d->collectionMoveTranslationEnabled = enabled; +} + +void Monitor::connectNotify(const QMetaMethod &signal) +{ + Q_D(Monitor); + d->updateListeners(signal, MonitorPrivate::AddListener); +} + +void Monitor::disconnectNotify(const QMetaMethod &signal) +{ + Q_D(Monitor); + d->updateListeners(signal, MonitorPrivate::RemoveListener); +} + +#include "moc_monitor.cpp" diff --git a/src/core/monitor.h b/src/core/monitor.h new file mode 100644 index 0000000..a75470a --- /dev/null +++ b/src/core/monitor.h @@ -0,0 +1,813 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" +#include "relation.h" +#include "tag.h" + +#include + +namespace Akonadi +{ +class CollectionFetchScope; +class CollectionStatistics; +class Item; +class ItemFetchScope; +class MonitorPrivate; +class Session; +class TagFetchScope; +class NotificationSubscriber; +class ChangeNotification; + +namespace Protocol +{ +class Command; +} + +/** + * @short Monitors an item or collection for changes. + * + * The Monitor emits signals if some of these objects are changed or + * removed or new ones are added to the Akonadi storage. + * + * There are various ways to filter these notifications. There are three types of filter + * evaluation: + * - (-) removal-only filter, ie. if the filter matches the notification is dropped, + * if not filter evaluation continues with the next one + * - (+) pass-exit filter, ie. if the filter matches the notification is delivered, + * if not evaluation is continued + * - (f) final filter, ie. evaluation ends here if the corresponding filter criteria is set, + * the notification is delivered depending on the result, evaluation is only continued + * if no filter criteria is defined + * + * The following filter are available, listed in evaluation order: + * (1) ignored sessions (-) + * (2) monitor everything (+) + * (3a) resource and mimetype filters (f) (items only) + * (3b) resource filters (f) (collections only) + * (4) item is monitored (+) + * (5) collection is monitored (+) + * + * Optionally, the changed objects can be fetched automatically from the server. + * To enable this, see itemFetchScope() and collectionFetchScope(). + * + * Note that as a consequence of rule 3a, it is not possible to monitor (more than zero resources + * OR more than zero mimetypes) AND more than zero collections. + * + * @todo Distinguish between monitoring collection properties and collection content. + * @todo Special case for collection content counts changed + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Monitor : public QObject +{ + Q_OBJECT + +public: + enum Type { + /** + * @internal This must be kept in sync with Akonadi::NotificationMessageV2::Type + */ + Collections = 1, + Items, + Tags, + Relations, + /** + * Listen to subscription changes of other Monitors connected to Akonadi. + * This is only for debugging purposes and should not be used in real + * applications. + * @since 5.4 + */ + Subscribers, + /** + * Listens to all notifications being emitted by the server and provides + * additional information about them. This is only for debugging purposes + * and should not be used in real applications. + * + * @note Enabling monitoring this type has performance impact on the + * Akonadi Server. + * + * @since 5.4 + */ + Notifications + }; + + /** + * Creates a new monitor. + * + * @param parent The parent object. + */ + explicit Monitor(QObject *parent = nullptr); + + /** + * Destroys the monitor. + */ + ~Monitor() override; + + /** + * Sets whether the specified collection shall be monitored for changes. If + * monitoring is turned on for the collection, all notifications for items + * in that collection will be emitted, and its child collections will also + * be monitored. Note that move notifications will be emitted if either one + * of the collections involved is being monitored. + * + * Note that if a session is being ignored, this takes precedence over + * setCollectionMonitored() on that session. + * + * @param collection The collection to monitor. + * If this collection is Collection::root(), all collections + * in the Akonadi storage will be monitored. + * @param monitored Whether to monitor the collection. + */ + void setCollectionMonitored(const Collection &collection, bool monitored = true); + + /** + * Sets whether the specified item shall be monitored for changes. + * + * Note that if a session is being ignored, this takes precedence over + * setItemMonitored() on that session. + * + * @param item The item to monitor. + * @param monitored Whether to monitor the item. + */ + void setItemMonitored(const Item &item, bool monitored = true); + + /** + * Sets whether the specified resource shall be monitored for changes. If + * monitoring is turned on for the resource, all notifications for + * collections and items in that resource will be emitted. + * + * Note that if a session is being ignored, this takes precedence over + * setResourceMonitored() on that session. + * + * @param resource The resource identifier. + * @param monitored Whether to monitor the resource. + */ + void setResourceMonitored(const QByteArray &resource, bool monitored = true); + + /** + * Sets whether items of the specified mime type shall be monitored for changes. + * If monitoring is turned on for the mime type, all notifications for items + * matching that mime type will be emitted, but notifications for collections + * matching that mime type will only be emitted if this is otherwise specified, + * for example by setCollectionMonitored(). + * + * Note that if a session is being ignored, this takes precedence over + * setMimeTypeMonitored() on that session. + * + * @param mimetype The mime type to monitor. + * @param monitored Whether to monitor the mime type. + */ + void setMimeTypeMonitored(const QString &mimetype, bool monitored = true); + + /** + * Sets whether the specified tag shall be monitored for changes. + * + * Same rules as for item monitoring apply. + * + * @param tag Tag to monitor. + * @param monitored Whether to monitor the tag. + * @since 4.13 + */ + void setTagMonitored(const Tag &tag, bool monitored = true); + + /** + * Sets whether given type (Collection, Item, Tag should be monitored). + * + * By default all types are monitored, but once you change one, you have + * to explicitly enable all other types you want to monitor. + * + * @param type Type to monitor. + * @param monitored Whether to monitor the type + * @since 4.13 + */ + void setTypeMonitored(Type type, bool monitored = true); + + /** + * Sets whether all items shall be monitored. + * @param monitored sets all items as monitored if set as @c true + * Note that if a session is being ignored, this takes precedence over + * setAllMonitored() on that session. + */ + void setAllMonitored(bool monitored = true); + + void setExclusive(bool exclusive = true); + Q_REQUIRED_RESULT bool exclusive() const; + + /** + * Ignores all change notifications caused by the given session. This + * overrides all other settings on this session. + * + * @param session The session you want to ignore. + */ + void ignoreSession(Session *session); + + /** + * Enables automatic fetching of changed collections from the Akonadi storage. + * + * @param enable @c true enables automatic fetching, @c false disables automatic fetching. + */ + void fetchCollection(bool enable); + + /** + * Enables automatic fetching of changed collection statistics information from + * the Akonadi storage. + * + * @param enable @c true to enables automatic fetching, @c false disables automatic fetching. + */ + void fetchCollectionStatistics(bool enable); + + /** + * Sets the item fetch scope. + * + * Controls how much of an item's data is fetched from the server, e.g. + * whether to fetch the full item payload or only meta data. + * + * @param fetchScope The new scope for item fetch operations. + * + * @see itemFetchScope() + */ + void setItemFetchScope(const ItemFetchScope &fetchScope); + + /** + * Instructs the monitor to fetch only those parts that were changed and + * were requested in the fetch scope. + * + * This is taken in account only for item modifications. + * Example usage: + * @code + * monitor->itemFetchScope().fetchFullPayload( true ); + * monitor->fetchChangedOnly(true); + * @endcode + * + * In the example if an item was changed, but its payload was not, the full + * payload will not be retrieved. + * If the item's payload was changed, the monitor retrieves the changed + * payload as well. + * + * The default is to fetch everything requested. + * + * @since 4.8 + * + * @param enable @c true to enable the feature, @c false means everything + * that was requested will be fetched. + */ + void fetchChangedOnly(bool enable); + + /** + * Returns the item fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the ItemFetchScope documentation + * for an example. + * + * @return a reference to the current item fetch scope + * + * @see setItemFetchScope() for replacing the current item fetch scope + */ + ItemFetchScope &itemFetchScope(); + + /** + * Sets the collection fetch scope. + * + * Controls which collections are monitored and how much of a collection's data + * is fetched from the server. + * + * @param fetchScope The new scope for collection fetch operations. + * + * @see collectionFetchScope() + * @since 4.4 + */ + void setCollectionFetchScope(const CollectionFetchScope &fetchScope); + + /** + * Returns the collection fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. See the CollectionFetchScope documentation + * for an example. + * + * @return a reference to the current collection fetch scope + * + * @see setCollectionFetchScope() for replacing the current collection fetch scope + * @since 4.4 + */ + CollectionFetchScope &collectionFetchScope(); + + /** + * Sets the tag fetch scope. + * + * Controls how much of an tag's data is fetched from the server. + * + * @param fetchScope The new scope for tag fetch operations. + * + * @see tagFetchScope() + */ + void setTagFetchScope(const TagFetchScope &fetchScope); + + /** + * Returns the tag fetch scope. + * + * Since this returns a reference it can be used to conveniently modify the + * current scope in-place, i.e. by calling a method on the returned reference + * without storing it in a local variable. + * + * @return a reference to the current tag fetch scope + * + * @see setTagFetchScope() for replacing the current tag fetch scope + */ + TagFetchScope &tagFetchScope(); + + /** + * Returns the list of collections being monitored. + * + * @since 4.3 + */ + Q_REQUIRED_RESULT Collection::List collectionsMonitored() const; + + /** + * Returns the set of items being monitored. + * + * Faster version (at least on 32-bit systems) of itemsMonitored(). + * + * @since 4.6 + */ + Q_REQUIRED_RESULT QVector itemsMonitoredEx() const; + + /** + * Returns the number of items being monitored. + * Optimization. + * @since 4.14.3 + */ + Q_REQUIRED_RESULT int numItemsMonitored() const; + + /** + * Returns the set of mimetypes being monitored. + * + * @since 4.3 + */ + Q_REQUIRED_RESULT QStringList mimeTypesMonitored() const; + + /** + * Returns the number of mimetypes being monitored. + * Optimization. + * @since 4.14.3 + */ + Q_REQUIRED_RESULT int numMimeTypesMonitored() const; + + /** + * Returns the set of tags being monitored. + * + * @since 4.13 + */ + Q_REQUIRED_RESULT QVector tagsMonitored() const; + + /** + * Returns the set of types being monitored. + * + * @since 4.13 + */ + Q_REQUIRED_RESULT QVector typesMonitored() const; + + /** + * Returns the set of identifiers for resources being monitored. + * + * @since 4.3 + */ + Q_REQUIRED_RESULT QList resourcesMonitored() const; + + /** + * Returns the number of resources being monitored. + * Optimization. + * @since 4.14.3 + */ + Q_REQUIRED_RESULT int numResourcesMonitored() const; + + /** + * Returns true if everything is being monitored. + * + * @since 4.3 + */ + Q_REQUIRED_RESULT bool isAllMonitored() const; + + /** + * Sets the session used by the Monitor to communicate with the %Akonadi server. + * If not set, the Akonadi::Session::defaultSession is used. + * @param session the session to be set + * @since 4.4 + */ + void setSession(Akonadi::Session *session); + + /** + * Returns the Session used by the monitor to communicate with Akonadi. + * + * @since 4.4 + */ + Q_REQUIRED_RESULT Session *session() const; + + /** + * Allows to enable/disable collection move translation. If enabled (the default), move + * notifications are automatically translated into add/remove notifications if the source/destination + * is outside of the monitored collection hierarchy. + * @param enabled enables collection move translation if set as @c true + * @since 4.9 + */ + void setCollectionMoveTranslationEnabled(bool enabled); + +Q_SIGNALS: + /** + * This signal is emitted if a monitored item has changed, e.g. item parts have been modified. + * + * @param item The changed item. + * @param partIdentifiers The identifiers of the item parts that has been changed. + */ + void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); + + /** + * This signal is emitted if flags of monitored items have changed. + * + * @param items Items that were changed + * @param addedFlags Flags that have been added to each item in @p items + * @param removedFlags Flags that have been removed from each item in @p items + * @since 4.11 + */ + void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags); + + /** + * This signal is emitted if tags of monitored items have changed. + * + * @param items Items that were changed + * @param addedTags Tags that have been added to each item in @p items. + * @param removedTags Tags that have been removed from each item in @p items + * @since 4.13 + */ + void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, const QSet &removedTags); + + /** + * This signal is emitted if relations of monitored items have changed. + * + * @param items Items that were changed + * @param addedRelations Relations that have been added to each item in @p items. + * @param removedRelations Relations that have been removed from each item in @p items + * @since 4.15 + */ + void + itemsRelationsChanged(const Akonadi::Item::List &items, const Akonadi::Relation::List &addedRelations, const Akonadi::Relation::List &removedRelations); + + /** + * This signal is emitted if a monitored item has been moved between two collections + * + * @param item The moved item. + * @param collectionSource The collection the item has been moved from. + * @param collectionDestination The collection the item has been moved to. + */ + void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination); + + /** + * This is signal is emitted when multiple monitored items have been moved between two collections + * + * @param items Moved items + * @param collectionSource The collection the items have been moved from. + * @param collectionDestination The collection the items have been moved to. + * + * @since 4.11 + */ + void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination); + + /** + * This signal is emitted if an item has been added to a monitored collection in the Akonadi storage. + * + * @param item The new item. + * @param collection The collection the item has been added to. + */ + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * This signal is emitted if + * - a monitored item has been removed from the Akonadi storage + * or + * - a item has been removed from a monitored collection. + * + * @param item The removed item. + */ + void itemRemoved(const Akonadi::Item &item); + + /** + * This signal is emitted if monitored items have been removed from Akonadi + * storage of items have been removed from a monitored collection. + * + * @param items Removed items + * + * @since 4.11 + */ + void itemsRemoved(const Akonadi::Item::List &items); + + /** + * This signal is emitted if a reference to an item is added to a virtual collection. + * @param item The linked item. + * @param collection The collection the item is linked to. + * + * @since 4.2 + */ + void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a reference to multiple items is added to a virtual collection + * + * @param items The linked items + * @param collection The collections the items are linked to + * + * @since 4.11 + */ + void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a reference to an item is removed from a virtual collection. + * @param item The unlinked item. + * @param collection The collection the item is unlinked from. + * + * @since 4.2 + */ + void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a reference to items is removed from a virtual collection + * + * @param items The unlinked items + * @param collection The collections the items are unlinked from + * + * @since 4.11 + */ + void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection); + + /** + * This signal is emitted if a new collection has been added to a monitored collection in the Akonadi storage. + * + * @param collection The new collection. + * @param parent The parent collection. + */ + void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + /** + * This signal is emitted if a monitored collection has been changed (properties or content). + * + * @param collection The changed collection. + */ + void collectionChanged(const Akonadi::Collection &collection); + + /** + * This signal is emitted if a monitored collection has been changed (properties or attributes). + * + * @param collection The changed collection. + * @param attributeNames The names of the collection attributes that have been changed. + * + * @since 4.4 + */ + void collectionChanged(const Akonadi::Collection &collection, const QSet &attributeNames); + + /** + * This signals is emitted if a monitored collection has been moved. + * + * @param collection The moved collection. + * @param source The previous parent collection. + * @param destination The new parent collection. + * + * @since 4.4 + */ + void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &destination); + + /** + * This signal is emitted if a monitored collection has been removed from the Akonadi storage. + * + * @param collection The removed collection. + */ + void collectionRemoved(const Akonadi::Collection &collection); + + /** + * This signal is emitted if a collection has been subscribed to by the user. + * It will be emitted even for unmonitored collections as the check for whether to + * monitor it has not been applied yet. + * + * @param collection The subscribed collection + * @param parent The parent collection of the subscribed collection. + * + * @since 4.6 + */ + void collectionSubscribed(const Akonadi::Collection &collection, const Akonadi::Collection &parent); + + /** + * This signal is emitted if a user unsubscribes from a collection. + * + * @param collection The unsubscribed collection + * + * @since 4.6 + */ + void collectionUnsubscribed(const Akonadi::Collection &collection); + + /** + * This signal is emitted if the statistics information of a monitored collection + * has changed. + * + * @param id The collection identifier of the changed collection. + * @param statistics The updated collection statistics, invalid if automatic + * fetching of statistics changes is disabled. + */ + void collectionStatisticsChanged(Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics); + + /** + * This signal is emitted if a tag has been added to Akonadi storage. + * + * @param tag The added tag + * @since 4.13 + */ + void tagAdded(const Akonadi::Tag &tag); + + /** + * This signal is emitted if a monitored tag is changed on the server. + * + * @param tag The changed tag. + * @since 4.13 + */ + void tagChanged(const Akonadi::Tag &tag); + + /** + * This signal is emitted if a monitored tag is removed from the server storage. + * + * The monitor will also emit itemTagsChanged() signal for all monitored items + * (if any) that were tagged by @p tag. + * + * @param tag The removed tag. + * @since 4.13 + */ + void tagRemoved(const Akonadi::Tag &tag); + + /** + * This signal is emitted if a relation has been added to Akonadi storage. + * + * The monitor will also emit itemRelationsChanged() signal for all monitored items + * hat are affected by @p relation. + * + * @param relation The added relation + * @since 4.13 + */ + void relationAdded(const Akonadi::Relation &relation); + + /** + * This signal is emitted if a monitored relation is removed from the server storage. + * + * The monitor will also emit itemRelationsChanged() signal for all monitored items + * that were affected by @p relation. + * + * @param relation The removed relation. + * @since 4.13 + */ + void relationRemoved(const Akonadi::Relation &relation); + + /** + * This signal is emitted when Subscribers are monitored and a new subscriber + * subscribers to the server. + * + * @param subscriber The new subscriber + * @since 5.4 + * + * @note Monitoring for subscribers and listening to this signal only makes + * sense if you want to globally debug Monitors. There is no reason to use + * this in regular applications. + */ + void notificationSubscriberAdded(const Akonadi::NotificationSubscriber &subscriber); + + /** + * This signal is emitted when Subscribers are monitored and an existing + * subscriber changes its subscription. + * + * @param subscriber The changed subscriber + * @since 5.4 + * + * @note Monitoring for subscribers and listening to this signal only makes + * sense if you want to globally debug Monitors. There is no reason to use + * this in regular applications. + */ + void notificationSubscriberChanged(const Akonadi::NotificationSubscriber &subscriber); + + /** + * This signal is emitted when Subscribers are monitored and an existing + * subscriber unsubscribes from the server. + * + * @param subscriber The removed subscriber + * @since 5.4 + * + * @note Monitoring for subscribers and listening to this signal only makes + * sense if you want to globally debug Monitors. There is no reason to use + * this in regular applications. + */ + void notificationSubscriberRemoved(const Akonadi::NotificationSubscriber &subscriber); + + /** + * This signal is emitted when Notifications are monitored and the server emits + * any change notification. + * + * @since 5.4 + * + * @note Getting introspection into all change notifications only makes sense + * if you want to globally debug Notifications. There is no reason to use + * this in regular applications. + */ + void debugNotification(const Akonadi::ChangeNotification ¬ification); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p collection explicitly. + * @param collection The collection + * @param monitored Whether the collection is now being monitored or not. + * + * @since 4.3 + */ + void collectionMonitored(const Akonadi::Collection &collection, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p item explicitly. + * @param item The item + * @param monitored Whether the item is now being monitored or not. + * + * @since 4.3 + */ + void itemMonitored(const Akonadi::Item &item, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring the resource with the identifier @p identifier explicitly. + * @param identifier The identifier of the resource. + * @param monitored Whether the resource is now being monitored or not. + * + * @since 4.3 + */ + void resourceMonitored(const QByteArray &identifier, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p mimeType explicitly. + * @param mimeType The mimeType. + * @param monitored Whether the mimeType is now being monitored or not. + * + * @since 4.3 + */ + void mimeTypeMonitored(const QString &mimeType, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring everything. + * @param monitored Whether everything is now being monitored or not. + * + * @since 4.3 + */ + void allMonitored(bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p tag explicitly. + * @param tag The tag. + * @param monitored Whether the tag is now being monitored or not. + * @since 4.13 + */ + void tagMonitored(const Akonadi::Tag &tag, bool monitored); + + /** + * This signal is emitted if the Monitor starts or stops monitoring @p type explicitly + * @param type The type. + * @param monitored Whether the type is now being monitored or not. + * @since 4.13 + */ + void typeMonitored(const Akonadi::Monitor::Type type, bool monitored); + + void monitorReady(); + +protected: + /// @cond PRIVATE + void connectNotify(const QMetaMethod &signal) override; + void disconnectNotify(const QMetaMethod &signal) override; + + friend class EntityTreeModel; + friend class EntityTreeModelPrivate; + MonitorPrivate *d_ptr; + explicit Monitor(MonitorPrivate *d, QObject *parent = nullptr); + /// @endcond + +private: + Q_DECLARE_PRIVATE(Monitor) + + /// @cond PRIVATE + Q_PRIVATE_SLOT(d_ptr, void handleCommands()) + Q_PRIVATE_SLOT(d_ptr, void invalidateCollectionCache(qint64)) + Q_PRIVATE_SLOT(d_ptr, void invalidateItemCache(qint64)) + Q_PRIVATE_SLOT(d_ptr, void invalidateTagCache(qint64)) + + friend class ResourceBasePrivate; + /// @endcond +}; + +} + diff --git a/src/core/monitor_p.cpp b/src/core/monitor_p.cpp new file mode 100644 index 0000000..4cabd48 --- /dev/null +++ b/src/core/monitor_p.cpp @@ -0,0 +1,1395 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +/// @cond PRIVATE + +#include "monitor_p.h" + +#include "akonadicore_debug.h" +#include "changemediator_p.h" +#include "changenotification.h" +#include "collectionfetchjob.h" +#include "collectionstatistics.h" +#include "itemfetchjob.h" +#include "notificationmanagerinterface.h" +#include "notificationsubscriber.h" +#include "protocolhelper_p.h" +#include "session.h" +#include "vectorhelper.h" + +#include + +#include + +using namespace Akonadi; +using namespace AkRanges; + +class operation; + +static const int PipelineSize = 5; + +MonitorPrivate::MonitorPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, Monitor *parent) + : q_ptr(parent) + , dependenciesFactory(dependenciesFactory_ ? dependenciesFactory_ : new ChangeNotificationDependenciesFactory) + , ntfConnection(nullptr) + , monitorAll(false) + , exclusive(false) + , mFetchChangedOnly(false) + , session(Session::defaultSession()) + , collectionCache(nullptr) + , itemCache(nullptr) + , tagCache(nullptr) + , mCommandBuffer(parent, "handleCommands") + , pendingModificationChanges(Protocol::ModifySubscriptionCommand::None) + , pendingModificationTimer(nullptr) + , monitorReady(false) + , fetchCollection(false) + , fetchCollectionStatistics(false) + , collectionMoveTranslationEnabled(true) + , useRefCounting(false) +{ +} + +MonitorPrivate::~MonitorPrivate() +{ + disconnectFromNotificationManager(); + delete dependenciesFactory; + delete collectionCache; + delete itemCache; + delete tagCache; +} + +void MonitorPrivate::init() +{ + // needs to be at least 3x pipeline size for the collection move case + collectionCache = dependenciesFactory->createCollectionCache(3 * PipelineSize, session); + // needs to be at least 1x pipeline size + itemCache = dependenciesFactory->createItemListCache(PipelineSize, session); + // 20 tags looks like a reasonable amount to keep around + tagCache = dependenciesFactory->createTagListCache(20, session); + + QObject::connect(collectionCache, &CollectionCache::dataAvailable, q_ptr, [this]() { + dataAvailable(); + }); + QObject::connect(itemCache, &ItemCache::dataAvailable, q_ptr, [this]() { + dataAvailable(); + }); + QObject::connect(tagCache, &TagCache::dataAvailable, q_ptr, [this]() { + dataAvailable(); + }); + QObject::connect(ServerManager::self(), &ServerManager::stateChanged, q_ptr, [this](auto state) { + serverStateChanged(state); + }); + + statisticsCompressionTimer.setSingleShot(true); + statisticsCompressionTimer.setInterval(500); + QObject::connect(&statisticsCompressionTimer, &QTimer::timeout, q_ptr, [this]() { + slotFlushRecentlyChangedCollections(); + }); +} + +bool MonitorPrivate::connectToNotificationManager() +{ + if (ntfConnection) { + ntfConnection->deleteLater(); + ntfConnection = nullptr; + } + + if (!session) { + return false; + } + + ntfConnection = dependenciesFactory->createNotificationConnection(session, &mCommandBuffer); + if (!ntfConnection) { + return false; + } + + slotUpdateSubscription(); + + ntfConnection->reconnect(); + + return true; +} + +void MonitorPrivate::disconnectFromNotificationManager() +{ + if (ntfConnection) { + ntfConnection->disconnect(q_ptr); + dependenciesFactory->destroyNotificationConnection(session, ntfConnection.data()); + } +} + +void MonitorPrivate::serverStateChanged(ServerManager::State state) +{ + if (state == ServerManager::Running) { + connectToNotificationManager(); + } +} + +void MonitorPrivate::invalidateCollectionCache(qint64 id) +{ + collectionCache->update(id, mCollectionFetchScope); +} + +void MonitorPrivate::invalidateItemCache(qint64 id) +{ + itemCache->update({id}, mItemFetchScope); + // Also invalidate content of all any pending notification for given item + for (auto it = pendingNotifications.begin(), end = pendingNotifications.end(); it != end; ++it) { + if ((*it)->type() == Protocol::Command::ItemChangeNotification) { + auto &ntf = Protocol::cmdCast(*it); + const auto items = ntf.items(); + if (std::any_of(items.cbegin(), items.cend(), [id](const Protocol::FetchItemsResponse &r) { + return r.id() == id; + })) { + ntf.setMustRetrieve(true); + } + } + } +} + +void MonitorPrivate::invalidateTagCache(qint64 id) +{ + tagCache->update({id}, mTagFetchScope); +} + +int MonitorPrivate::pipelineSize() const +{ + return PipelineSize; +} + +void MonitorPrivate::scheduleSubscriptionUpdate() +{ + if (pendingModificationTimer || !monitorReady) { + return; + } + + pendingModificationTimer = new QTimer(q_ptr); + pendingModificationTimer->setSingleShot(true); + pendingModificationTimer->setInterval(0); + pendingModificationTimer->start(); + q_ptr->connect(pendingModificationTimer, &QTimer::timeout, q_ptr, [this]() { + slotUpdateSubscription(); + }); +} + +void MonitorPrivate::slotUpdateSubscription() +{ + if (pendingModificationTimer) { + pendingModificationTimer->stop(); + std::exchange(pendingModificationTimer, nullptr)->deleteLater(); + } + + if (pendingModificationChanges & Protocol::ModifySubscriptionCommand::ItemFetchScope) { + pendingModification.setItemFetchScope(ProtocolHelper::itemFetchScopeToProtocol(mItemFetchScope)); + } + if (pendingModificationChanges & Protocol::ModifySubscriptionCommand::CollectionFetchScope) { + pendingModification.setCollectionFetchScope(ProtocolHelper::collectionFetchScopeToProtocol(mCollectionFetchScope)); + } + if (pendingModificationChanges & Protocol::ModifySubscriptionCommand::TagFetchScope) { + pendingModification.setTagFetchScope(ProtocolHelper::tagFetchScopeToProtocol(mTagFetchScope)); + } + pendingModificationChanges = Protocol::ModifySubscriptionCommand::None; + + if (ntfConnection) { + ntfConnection->sendCommand(3, Protocol::ModifySubscriptionCommandPtr::create(pendingModification)); + pendingModification = Protocol::ModifySubscriptionCommand(); + } +} + +bool MonitorPrivate::isLazilyIgnored(const Protocol::ChangeNotificationPtr &msg, bool allowModifyFlagsConversion) const +{ + if (msg->type() == Protocol::Command::CollectionChangeNotification) { + // Lazy fetching can only affects items. + return false; + } + + if (msg->type() == Protocol::Command::TagChangeNotification) { + const auto op = Protocol::cmdCast(msg).operation(); + return ((op == Protocol::TagChangeNotification::Add && !hasListeners(&Monitor::tagAdded)) + || (op == Protocol::TagChangeNotification::Modify && !hasListeners(&Monitor::tagChanged)) + || (op == Protocol::TagChangeNotification::Remove && !hasListeners(&Monitor::tagRemoved))); + } + + if (!fetchCollectionStatistics && msg->type() == Protocol::Command::ItemChangeNotification) { + const auto &itemNtf = Protocol::cmdCast(msg); + const auto op = itemNtf.operation(); + if ((op == Protocol::ItemChangeNotification::Add && !hasListeners(&Monitor::itemAdded)) + || (op == Protocol::ItemChangeNotification::Remove && !hasListeners(&Monitor::itemRemoved) && !hasListeners(&Monitor::itemsRemoved)) + || (op == Protocol::ItemChangeNotification::Modify && !hasListeners(&Monitor::itemChanged)) + || (op == Protocol::ItemChangeNotification::ModifyFlags + && !hasListeners(&Monitor::itemsFlagsChanged) + // Newly delivered ModifyFlags notifications will be converted to + // itemChanged(item, "FLAGS") for legacy clients. + && (!allowModifyFlagsConversion || !hasListeners(&Monitor::itemChanged))) + || (op == Protocol::ItemChangeNotification::ModifyTags && !hasListeners(&Monitor::itemsTagsChanged)) + || (op == Protocol::ItemChangeNotification::Move && !hasListeners(&Monitor::itemMoved) && !hasListeners(&Monitor::itemsMoved)) + || (op == Protocol::ItemChangeNotification::Link && !hasListeners(&Monitor::itemLinked) && !hasListeners(&Monitor::itemsLinked)) + || (op == Protocol::ItemChangeNotification::Unlink && !hasListeners(&Monitor::itemUnlinked) && !hasListeners(&Monitor::itemsUnlinked))) { + return true; + } + + if (!useRefCounting) { + return false; + } + + const Collection::Id parentCollectionId = itemNtf.parentCollection(); + + if ((op == Protocol::ItemChangeNotification::Add) || (op == Protocol::ItemChangeNotification::Remove) + || (op == Protocol::ItemChangeNotification::Modify) || (op == Protocol::ItemChangeNotification::ModifyFlags) + || (op == Protocol::ItemChangeNotification::ModifyTags) || (op == Protocol::ItemChangeNotification::Link) + || (op == Protocol::ItemChangeNotification::Unlink)) { + if (isMonitored(parentCollectionId)) { + return false; + } + } + + if (op == Protocol::ItemChangeNotification::Move) { + // We can't ignore the move. It must be transformed later into a removal or insertion. + return !isMonitored(parentCollectionId) && !isMonitored(itemNtf.parentDestCollection()); + } + return true; + } + + return false; +} + +void MonitorPrivate::checkBatchSupport(const Protocol::ChangeNotificationPtr &msg, bool &needsSplit, bool &batchSupported) const +{ + if (msg->type() != Protocol::Command::ItemChangeNotification) { + needsSplit = false; + batchSupported = false; + return; + } + + const auto &itemNtf = Protocol::cmdCast(msg); + const bool isBatch = (itemNtf.items().count() > 1); + + switch (itemNtf.operation()) { + case Protocol::ItemChangeNotification::Add: + case Protocol::ItemChangeNotification::Modify: + needsSplit = isBatch; + batchSupported = false; + return; + needsSplit = isBatch; + batchSupported = false; + return; + case Protocol::ItemChangeNotification::ModifyFlags: + batchSupported = hasListeners(&Monitor::itemsFlagsChanged); + needsSplit = isBatch && !batchSupported && hasListeners(&Monitor::itemChanged); + return; + case Protocol::ItemChangeNotification::ModifyTags: + case Protocol::ItemChangeNotification::ModifyRelations: + // Tags and relations were added after batch notifications, so they are always supported + batchSupported = true; + needsSplit = false; + return; + case Protocol::ItemChangeNotification::Move: + needsSplit = isBatch && hasListeners(&Monitor::itemMoved); + batchSupported = hasListeners(&Monitor::itemsMoved); + return; + case Protocol::ItemChangeNotification::Remove: + needsSplit = isBatch && hasListeners(&Monitor::itemRemoved); + batchSupported = hasListeners(&Monitor::itemsRemoved); + return; + case Protocol::ItemChangeNotification::Link: + needsSplit = isBatch && hasListeners(&Monitor::itemLinked); + batchSupported = hasListeners(&Monitor::itemsLinked); + return; + case Protocol::ItemChangeNotification::Unlink: + needsSplit = isBatch && hasListeners(&Monitor::itemUnlinked); + batchSupported = hasListeners(&Monitor::itemsUnlinked); + return; + default: + needsSplit = isBatch; + batchSupported = false; + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << itemNtf.operation() << "in item change notification"; + return; + } +} + +Protocol::ChangeNotificationList MonitorPrivate::splitMessage(const Protocol::ItemChangeNotification &msg, bool legacy) const +{ + Protocol::ChangeNotificationList list; + + Protocol::ItemChangeNotification baseMsg; + baseMsg.setSessionId(msg.sessionId()); + if (legacy && msg.operation() == Protocol::ItemChangeNotification::ModifyFlags) { + baseMsg.setOperation(Protocol::ItemChangeNotification::Modify); + baseMsg.setItemParts(QSet() << "FLAGS"); + } else { + baseMsg.setOperation(msg.operation()); + baseMsg.setItemParts(msg.itemParts()); + } + baseMsg.setParentCollection(msg.parentCollection()); + baseMsg.setParentDestCollection(msg.parentDestCollection()); + baseMsg.setResource(msg.resource()); + baseMsg.setDestinationResource(msg.destinationResource()); + baseMsg.setAddedFlags(msg.addedFlags()); + baseMsg.setRemovedFlags(msg.removedFlags()); + baseMsg.setAddedTags(msg.addedTags()); + baseMsg.setRemovedTags(msg.removedTags()); + + const auto &items = msg.items(); + list.reserve(items.count()); + for (const auto &item : items) { + auto copy = Protocol::ItemChangeNotificationPtr::create(baseMsg); + copy->setItems({Protocol::FetchItemsResponse(item)}); + list.push_back(std::move(copy)); + } + + return list; +} + +bool MonitorPrivate::fetchCollections() const +{ + return fetchCollection; +} + +bool MonitorPrivate::fetchItems() const +{ + return !mItemFetchScope.isEmpty(); +} + +bool MonitorPrivate::ensureDataAvailable(const Protocol::ChangeNotificationPtr &msg) +{ + if (msg->type() == Protocol::Command::TagChangeNotification) { + const auto &tagMsg = Protocol::cmdCast(msg); + if (tagMsg.metadata().contains("FETCH_TAG")) { + if (!tagCache->ensureCached({tagMsg.tag().id()}, mTagFetchScope)) { + return false; + } + } + return true; + } + + if (msg->type() == Protocol::Command::RelationChangeNotification) { + return true; + } + + if (msg->type() == Protocol::Command::SubscriptionChangeNotification) { + return true; + } + + if (msg->type() == Protocol::Command::DebugChangeNotification) { + return true; + } + + if (msg->type() == Protocol::Command::CollectionChangeNotification + && Protocol::cmdCast(msg).operation() == Protocol::CollectionChangeNotification::Remove) { + // For collection removals the collection is gone already, so we can't fetch it, + // but we have to at least obtain the ancestor chain. + const qint64 parentCollection = Protocol::cmdCast(msg).parentCollection(); + return parentCollection <= -1 || collectionCache->ensureCached(parentCollection, mCollectionFetchScope); + } + + bool allCached = true; + if (fetchCollections()) { + const qint64 parentCollection = (msg->type() == Protocol::Command::ItemChangeNotification) + ? Protocol::cmdCast(msg).parentCollection() + : (msg->type() == Protocol::Command::CollectionChangeNotification) + ? Protocol::cmdCast(msg).parentCollection() + : -1; + if (parentCollection > -1 && !collectionCache->ensureCached(parentCollection, mCollectionFetchScope)) { + allCached = false; + } + + qint64 parentDestCollection = -1; + + if ((msg->type() == Protocol::Command::ItemChangeNotification) + && (Protocol::cmdCast(msg).operation() == Protocol::ItemChangeNotification::Move)) { + parentDestCollection = Protocol::cmdCast(msg).parentDestCollection(); + } else if ((msg->type() == Protocol::Command::CollectionChangeNotification) + && (Protocol::cmdCast(msg).operation() == Protocol::CollectionChangeNotification::Move)) { + parentDestCollection = Protocol::cmdCast(msg).parentDestCollection(); + } + if (parentDestCollection > -1 && !collectionCache->ensureCached(parentDestCollection, mCollectionFetchScope)) { + allCached = false; + } + } + + if (msg->isRemove()) { + return allCached; + } + + if (msg->type() == Protocol::Command::ItemChangeNotification && fetchItems()) { + const auto &itemNtf = Protocol::cmdCast(msg); + if (mFetchChangedOnly + && (itemNtf.operation() == Protocol::ItemChangeNotification::Modify || itemNtf.operation() == Protocol::ItemChangeNotification::ModifyFlags)) { + const auto changedParts = itemNtf.itemParts(); + const auto requestedParts = mItemFetchScope.payloadParts(); + const auto requestedAttrs = mItemFetchScope.attributes(); + QSet missingParts; + QSet missingAttributes; + for (const QByteArray &part : changedParts) { + const auto partName = part.mid(4); + if (part.startsWith("PLD:") && // krazy:exclude=strings since QByteArray + (!mItemFetchScope.fullPayload() || !requestedParts.contains(partName))) { + missingParts.insert(partName); + } else if (part.startsWith("ATR:") && // krazy:exclude=strings since QByteArray + (!mItemFetchScope.allAttributes() || !requestedAttrs.contains(partName))) { + missingAttributes.insert(partName); + } + } + + if (!missingParts.isEmpty() || !missingAttributes.isEmpty()) { + ItemFetchScope scope(mItemFetchScope); + scope.fetchFullPayload(false); + for (const auto &part : requestedParts) { + scope.fetchPayloadPart(part, false); + } + for (const auto &attr : requestedAttrs) { + scope.fetchAttribute(attr, false); + } + for (const auto &part : missingParts) { + scope.fetchPayloadPart(part, true); + } + for (const auto &attr : missingAttributes) { + scope.fetchAttribute(attr, true); + } + + if (!itemCache->ensureCached(Protocol::ChangeNotification::itemsToUids(itemNtf.items()), scope)) { + return false; + } + } + + return allCached; + } + + // Make sure all tags for ModifyTags operation are in cache too + if (itemNtf.operation() == Protocol::ItemChangeNotification::ModifyTags) { + if (!tagCache->ensureCached((itemNtf.addedTags() + itemNtf.removedTags()) | Actions::toQList, mTagFetchScope)) { + return false; + } + } + + if (itemNtf.metadata().contains("FETCH_ITEM") || itemNtf.mustRetrieve()) { + if (!itemCache->ensureCached(Protocol::ChangeNotification::itemsToUids(itemNtf.items()), mItemFetchScope)) { + return false; + } + } + + return allCached; + + } else if (msg->type() == Protocol::Command::CollectionChangeNotification && fetchCollections()) { + const auto &colMsg = Protocol::cmdCast(msg); + if (colMsg.metadata().contains("FETCH_COLLECTION")) { + if (!collectionCache->ensureCached(colMsg.collection().id(), mCollectionFetchScope)) { + return false; + } + } + + return allCached; + } + + return allCached; +} + +bool MonitorPrivate::emitNotification(const Protocol::ChangeNotificationPtr &msg) +{ + bool someoneWasListening = false; + if (msg->type() == Protocol::Command::TagChangeNotification) { + const auto &tagNtf = Protocol::cmdCast(msg); + const bool fetched = tagNtf.metadata().contains("FETCH_TAG"); + Tag tag; + if (fetched) { + const auto tags = tagCache->retrieve({tagNtf.tag().id()}); + tag = tags.isEmpty() ? Tag() : tags.at(0); + } else { + tag = ProtocolHelper::parseTag(tagNtf.tag()); + } + someoneWasListening = emitTagNotification(tagNtf, tag); + } else if (msg->type() == Protocol::Command::RelationChangeNotification) { + const auto &relNtf = Protocol::cmdCast(msg); + const Relation rel = ProtocolHelper::parseRelationFetchResult(relNtf.relation()); + someoneWasListening = emitRelationNotification(relNtf, rel); + } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { + const auto &colNtf = Protocol::cmdCast(msg); + const Collection parent = collectionCache->retrieve(colNtf.parentCollection()); + Collection destParent; + if (colNtf.operation() == Protocol::CollectionChangeNotification::Move) { + destParent = collectionCache->retrieve(colNtf.parentDestCollection()); + } + + const bool fetched = colNtf.metadata().contains("FETCH_COLLECTION"); + // For removals this will retrieve an invalid collection. We'll deal with that in emitCollectionNotification + const Collection col = fetched ? collectionCache->retrieve(colNtf.collection().id()) : ProtocolHelper::parseCollection(colNtf.collection(), true); + // It is possible that the retrieval fails also in the non-removal case (e.g. because the item was meanwhile removed while + // the changerecorder stored the notification or the notification was in the queue). In order to drop such invalid notifications we have to ignore them. + if (col.isValid() || colNtf.operation() == Protocol::CollectionChangeNotification::Remove || !fetchCollections()) { + someoneWasListening = emitCollectionNotification(colNtf, col, parent, destParent); + } + } else if (msg->type() == Protocol::Command::ItemChangeNotification) { + const auto &itemNtf = Protocol::cmdCast(msg); + const Collection parent = collectionCache->retrieve(itemNtf.parentCollection()); + Collection destParent; + if (itemNtf.operation() == Protocol::ItemChangeNotification::Move) { + destParent = collectionCache->retrieve(itemNtf.parentDestCollection()); + } + const bool fetched = itemNtf.metadata().contains("FETCH_ITEM") || itemNtf.mustRetrieve(); + // For removals this will retrieve an empty set. We'll deal with that in emitItemNotification + Item::List items; + if (fetched && fetchItems()) { + items = itemCache->retrieve(Protocol::ChangeNotification::itemsToUids(itemNtf.items())); + } else { + const auto &ntfItems = itemNtf.items(); + items.reserve(ntfItems.size()); + for (const auto &ntfItem : ntfItems) { + items.push_back(ProtocolHelper::parseItemFetchResult(ntfItem, &mItemFetchScope)); + } + } + // It is possible that the retrieval fails also in the non-removal case (e.g. because the item was meanwhile removed while + // the changerecorder stored the notification or the notification was in the queue). In order to drop such invalid notifications we have to ignore them. + if (!items.isEmpty() || itemNtf.operation() == Protocol::ItemChangeNotification::Remove || !fetchItems()) { + someoneWasListening = emitItemsNotification(itemNtf, items, parent, destParent); + } + } else if (msg->type() == Protocol::Command::SubscriptionChangeNotification) { + const auto &subNtf = Protocol::cmdCast(msg); + NotificationSubscriber subscriber; + subscriber.setSubscriber(subNtf.subscriber()); + subscriber.setSessionId(subNtf.sessionId()); + subscriber.setMonitoredCollections(subNtf.collections()); + subscriber.setMonitoredItems(subNtf.items()); + subscriber.setMonitoredTags(subNtf.tags()); + QSet monitorTypes; + Q_FOREACH (auto type, subNtf.types()) { + if (type == Protocol::ModifySubscriptionCommand::NoType) { + continue; + } + monitorTypes.insert([](Protocol::ModifySubscriptionCommand::ChangeType type) { + switch (type) { + case Protocol::ModifySubscriptionCommand::ItemChanges: + return Monitor::Items; + case Protocol::ModifySubscriptionCommand::CollectionChanges: + return Monitor::Collections; + case Protocol::ModifySubscriptionCommand::TagChanges: + return Monitor::Tags; + case Protocol::ModifySubscriptionCommand::RelationChanges: + return Monitor::Relations; + case Protocol::ModifySubscriptionCommand::SubscriptionChanges: + return Monitor::Subscribers; + case Protocol::ModifySubscriptionCommand::ChangeNotifications: + return Monitor::Notifications; + default: + Q_ASSERT(false); + return Monitor::Items; // unreachable + } + }(type)); + } + subscriber.setMonitoredTypes(monitorTypes); + subscriber.setMonitoredMimeTypes(subNtf.mimeTypes()); + subscriber.setMonitoredResources(subNtf.resources()); + subscriber.setIgnoredSessions(subNtf.ignoredSessions()); + subscriber.setIsAllMonitored(subNtf.allMonitored()); + subscriber.setIsExclusive(subNtf.exclusive()); + subscriber.setItemFetchScope(ProtocolHelper::parseItemFetchScope(subNtf.itemFetchScope())); + subscriber.setCollectionFetchScope(ProtocolHelper::parseCollectionFetchScope(subNtf.collectionFetchScope())); + someoneWasListening = emitSubscriptionChangeNotification(subNtf, subscriber); + } else if (msg->type() == Protocol::Command::DebugChangeNotification) { + const auto &changeNtf = Protocol::cmdCast(msg); + ChangeNotification notification; + notification.setListeners(changeNtf.listeners()); + notification.setTimestamp(QDateTime::fromMSecsSinceEpoch(changeNtf.timestamp())); + notification.setNotification(changeNtf.notification()); + switch (changeNtf.notification()->type()) { + case Protocol::Command::ItemChangeNotification: + notification.setType(ChangeNotification::Items); + break; + case Protocol::Command::CollectionChangeNotification: + notification.setType(ChangeNotification::Collection); + break; + case Protocol::Command::TagChangeNotification: + notification.setType(ChangeNotification::Tag); + break; + case Protocol::Command::RelationChangeNotification: + notification.setType(ChangeNotification::Relation); + break; + case Protocol::Command::SubscriptionChangeNotification: + notification.setType(ChangeNotification::Subscription); + break; + default: + Q_ASSERT(false); // huh? + return false; + } + + someoneWasListening = emitDebugChangeNotification(changeNtf, notification); + } + + return someoneWasListening; +} + +void MonitorPrivate::updatePendingStatistics(const Protocol::ChangeNotificationPtr &msg) +{ + if (msg->type() == Protocol::Command::ItemChangeNotification) { + const auto &itemNtf = Protocol::cmdCast(msg); + notifyCollectionStatisticsWatchers(itemNtf.parentCollection(), itemNtf.resource()); + // FIXME use the proper resource of the target collection, for cross resource moves + notifyCollectionStatisticsWatchers(itemNtf.parentDestCollection(), itemNtf.destinationResource()); + } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { + const auto &colNtf = Protocol::cmdCast(msg); + if (colNtf.operation() == Protocol::CollectionChangeNotification::Remove) { + // no need for statistics updates anymore + recentlyChangedCollections.remove(colNtf.collection().id()); + } + } +} + +void MonitorPrivate::slotSessionDestroyed(QObject *object) +{ + auto objectSession = qobject_cast(object); + if (objectSession) { + sessions.removeAll(objectSession->sessionId()); + pendingModification.stopIgnoringSession(objectSession->sessionId()); + scheduleSubscriptionUpdate(); + } +} + +void MonitorPrivate::slotStatisticsChangedFinished(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error on fetching collection statistics: " << job->errorText(); + } else { + auto statisticsJob = static_cast(job); + Q_ASSERT(statisticsJob->collection().isValid()); + Q_EMIT q_ptr->collectionStatisticsChanged(statisticsJob->collection().id(), statisticsJob->statistics()); + } +} + +void MonitorPrivate::slotFlushRecentlyChangedCollections() +{ + for (Collection::Id collection : std::as_const(recentlyChangedCollections)) { + Q_ASSERT(collection >= 0); + if (fetchCollectionStatistics) { + fetchStatistics(collection); + } else { + static const CollectionStatistics dummyStatistics; + Q_EMIT q_ptr->collectionStatisticsChanged(collection, dummyStatistics); + } + } + recentlyChangedCollections.clear(); +} + +int MonitorPrivate::translateAndCompress(QQueue ¬ificationQueue, const Protocol::ChangeNotificationPtr &msg) +{ + // Always handle tags and relations + if (msg->type() == Protocol::Command::TagChangeNotification || msg->type() == Protocol::Command::RelationChangeNotification) { + notificationQueue.enqueue(msg); + return 1; + } + + // We have to split moves into insert or remove if the source or destination + // is not monitored. + if (!msg->isMove()) { + notificationQueue.enqueue(msg); + return 1; + } + + bool sourceWatched = false; + bool destWatched = false; + + if (msg->type() == Protocol::Command::ItemChangeNotification) { + const auto &itemNtf = Protocol::cmdCast(msg); + if (useRefCounting) { + sourceWatched = isMonitored(itemNtf.parentCollection()); + destWatched = isMonitored(itemNtf.parentDestCollection()); + } else { + if (!resources.isEmpty()) { + sourceWatched = resources.contains(itemNtf.resource()); + destWatched = isMoveDestinationResourceMonitored(itemNtf); + } + if (!sourceWatched) { + sourceWatched = isCollectionMonitored(itemNtf.parentCollection()); + } + if (!destWatched) { + destWatched = isCollectionMonitored(itemNtf.parentDestCollection()); + } + } + } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { + const auto &colNtf = Protocol::cmdCast(msg); + if (!resources.isEmpty()) { + sourceWatched = resources.contains(colNtf.resource()); + destWatched = isMoveDestinationResourceMonitored(colNtf); + } + if (!sourceWatched) { + sourceWatched = isCollectionMonitored(colNtf.parentCollection()); + } + if (!destWatched) { + destWatched = isCollectionMonitored(colNtf.parentDestCollection()); + } + } else { + Q_ASSERT(false); + return 0; + } + + if (!sourceWatched && !destWatched) { + return 0; + } + + if ((sourceWatched && destWatched) || (!collectionMoveTranslationEnabled && msg->type() == Protocol::Command::CollectionChangeNotification)) { + notificationQueue.enqueue(msg); + return 1; + } + + if (sourceWatched) { + if (msg->type() == Protocol::Command::ItemChangeNotification) { + auto removalMessage = Protocol::ItemChangeNotificationPtr::create(Protocol::cmdCast(msg)); + removalMessage->setOperation(Protocol::ItemChangeNotification::Remove); + removalMessage->setParentDestCollection(-1); + notificationQueue.enqueue(removalMessage); + return 1; + } else { + auto removalMessage = Protocol::CollectionChangeNotificationPtr::create(Protocol::cmdCast(msg)); + removalMessage->setOperation(Protocol::CollectionChangeNotification::Remove); + removalMessage->setParentDestCollection(-1); + notificationQueue.enqueue(removalMessage); + return 1; + } + } + + // Transform into an insertion + if (msg->type() == Protocol::Command::ItemChangeNotification) { + auto insertionMessage = Protocol::ItemChangeNotificationPtr::create(Protocol::cmdCast(msg)); + insertionMessage->setOperation(Protocol::ItemChangeNotification::Add); + insertionMessage->setParentCollection(insertionMessage->parentDestCollection()); + insertionMessage->setParentDestCollection(-1); + // We don't support batch insertion, so we have to do it one by one + const auto split = splitMessage(*insertionMessage, false); + for (const Protocol::ChangeNotificationPtr &insertion : split) { + notificationQueue.enqueue(insertion); + } + return split.count(); + } else if (msg->type() == Protocol::Command::CollectionChangeNotification) { + auto insertionMessage = Protocol::CollectionChangeNotificationPtr::create(Protocol::cmdCast(msg)); + insertionMessage->setOperation(Protocol::CollectionChangeNotification::Add); + insertionMessage->setParentCollection(insertionMessage->parentDestCollection()); + insertionMessage->setParentDestCollection(-1); + notificationQueue.enqueue(insertionMessage); + return 1; + } + + Q_ASSERT(false); + return 0; +} + +void MonitorPrivate::handleCommands() +{ + Q_Q(Monitor); + + CommandBufferLocker lock(&mCommandBuffer); + CommandBufferNotifyBlocker notify(&mCommandBuffer); + while (!mCommandBuffer.isEmpty()) { + const auto cmd = mCommandBuffer.dequeue(); + lock.unlock(); + const auto command = cmd.command; + + if (command->isResponse()) { + switch (command->type()) { + case Protocol::Command::Hello: { + qCDebug(AKONADICORE_LOG) << q_ptr << "Connected to notification bus"; + QByteArray subname; + if (!q->objectName().isEmpty()) { + subname = q->objectName().toLatin1(); + } else { + subname = session->sessionId(); + } + subname += " - " + QByteArray::number(quintptr(q)); + qCDebug(AKONADICORE_LOG) << q_ptr << "Subscribing as \"" << subname << "\""; + auto subCmd = Protocol::CreateSubscriptionCommandPtr::create(subname, session->sessionId()); + ntfConnection->sendCommand(2, subCmd); + break; + } + + case Protocol::Command::CreateSubscription: { + auto msubCmd = Protocol::ModifySubscriptionCommandPtr::create(); + for (const auto &col : std::as_const(collections)) { + msubCmd->startMonitoringCollection(col.id()); + } + for (const auto &res : std::as_const(resources)) { + msubCmd->startMonitoringResource(res); + } + for (auto itemId : std::as_const(items)) { + msubCmd->startMonitoringItem(itemId); + } + for (auto tagId : std::as_const(tags)) { + msubCmd->startMonitoringTag(tagId); + } + for (auto type : std::as_const(types)) { + msubCmd->startMonitoringType(monitorTypeToProtocol(type)); + } + for (const auto &mimetype : std::as_const(mimetypes)) { + msubCmd->startMonitoringMimeType(mimetype); + } + for (const auto &session : std::as_const(sessions)) { + msubCmd->startIgnoringSession(session); + } + msubCmd->setAllMonitored(monitorAll); + msubCmd->setIsExclusive(exclusive); + msubCmd->setItemFetchScope(ProtocolHelper::itemFetchScopeToProtocol(mItemFetchScope)); + msubCmd->setCollectionFetchScope(ProtocolHelper::collectionFetchScopeToProtocol(mCollectionFetchScope)); + msubCmd->setTagFetchScope(ProtocolHelper::tagFetchScopeToProtocol(mTagFetchScope)); + pendingModification = Protocol::ModifySubscriptionCommand(); + ntfConnection->sendCommand(3, msubCmd); + break; + } + + case Protocol::Command::ModifySubscription: + // TODO: Handle errors + if (!monitorReady) { + monitorReady = true; + Q_EMIT q_ptr->monitorReady(); + } + break; + + default: + qCWarning(AKONADICORE_LOG) << "Received an unexpected response on Notification stream: " << Protocol::debugString(command); + break; + } + } else { + switch (command->type()) { + case Protocol::Command::ItemChangeNotification: + case Protocol::Command::CollectionChangeNotification: + case Protocol::Command::TagChangeNotification: + case Protocol::Command::RelationChangeNotification: + case Protocol::Command::SubscriptionChangeNotification: + case Protocol::Command::DebugChangeNotification: + slotNotify(command.staticCast()); + break; + default: + qCWarning(AKONADICORE_LOG) << "Received an unexpected message on Notification stream:" << Protocol::debugString(command); + break; + } + } + + lock.relock(); + } + notify.unblock(); + lock.unlock(); +} + +/* + + server notification --> ?accepted --> pendingNotifications --> ?dataAvailable --> emit + | | + x --> discard x --> pipeline + + fetchJobDone --> pipeline ?dataAvailable --> emit + */ + +void MonitorPrivate::slotNotify(const Protocol::ChangeNotificationPtr &msg) +{ + int appendedMessages = 0; + int modifiedMessages = 0; + int erasedMessages = 0; + + invalidateCaches(msg); + updatePendingStatistics(msg); + bool needsSplit = true; + bool supportsBatch = false; + + if (isLazilyIgnored(msg, true)) { + return; + } + + checkBatchSupport(msg, needsSplit, supportsBatch); + + const bool isModifyFlags = (msg->type() == Protocol::Command::ItemChangeNotification + && Protocol::cmdCast(msg).operation() == Protocol::ItemChangeNotification::ModifyFlags); + if (supportsBatch || (!needsSplit && !supportsBatch && !isModifyFlags) || msg->type() == Protocol::Command::CollectionChangeNotification) { + // Make sure the batch msg is always queued before the split notifications + const int oldSize = pendingNotifications.size(); + const int appended = translateAndCompress(pendingNotifications, msg); + if (appended > 0) { + appendedMessages += appended; + } else { + ++modifiedMessages; + } + // translateAndCompress can remove an existing "modify" when msg is a "delete". + // Or it can merge two ModifyFlags and return false. + // We need to detect such removals, for ChangeRecorder. + if (pendingNotifications.count() != oldSize + appended) { + ++erasedMessages; // this count isn't exact, but it doesn't matter + } + } else if (needsSplit) { + // If it's not queued at least make sure we fetch all the items from split + // notifications in one go. + if (msg->type() == Protocol::Command::ItemChangeNotification) { + const auto items = Protocol::cmdCast(msg).items(); + itemCache->ensureCached(Protocol::ChangeNotification::itemsToUids(items), mItemFetchScope); + } + } + + // if the message contains more items, but we need to emit single-item notification, + // split the message into one message per item and queue them + // if the message contains only one item, but batches are not supported + // (and thus neither is flagsModified), splitMessage() will convert the + // notification to regular Modify with "FLAGS" part changed + if (needsSplit || (!needsSplit && !supportsBatch && isModifyFlags)) { + // Make sure inter-resource move notifications are translated into + // Add/Remove notifications + if (msg->type() == Protocol::Command::ItemChangeNotification) { + const auto &itemNtf = Protocol::cmdCast(msg); + if (itemNtf.operation() == Protocol::ItemChangeNotification::Move && itemNtf.resource() != itemNtf.destinationResource()) { + if (needsSplit) { + const Protocol::ChangeNotificationList split = splitMessage(itemNtf, !supportsBatch); + for (const auto &splitMsg : split) { + appendedMessages += translateAndCompress(pendingNotifications, splitMsg); + } + } else { + appendedMessages += translateAndCompress(pendingNotifications, msg); + } + } else { + const Protocol::ChangeNotificationList split = splitMessage(itemNtf, !supportsBatch); + pendingNotifications << (split | Actions::toQList); + appendedMessages += split.count(); + } + } + } + + // tell ChangeRecorder (even if 0 appended, the compression could have made changes to existing messages) + if (appendedMessages > 0 || modifiedMessages > 0 || erasedMessages > 0) { + if (erasedMessages > 0) { + notificationsErased(); + } else { + notificationsEnqueued(appendedMessages); + } + } + + dispatchNotifications(); +} + +void MonitorPrivate::flushPipeline() +{ + while (!pipeline.isEmpty()) { + const auto msg = pipeline.head(); + if (ensureDataAvailable(msg)) { + // dequeue should be before emit, otherwise stuff might happen (like dataAvailable + // being called again) and we end up dequeuing an empty pipeline + pipeline.dequeue(); + emitNotification(msg); + } else { + break; + } + } +} + +void MonitorPrivate::dataAvailable() +{ + flushPipeline(); + dispatchNotifications(); +} + +void MonitorPrivate::dispatchNotifications() +{ + // Note that this code is not used in a ChangeRecorder (pipelineSize==0) + while (pipeline.size() < pipelineSize() && !pendingNotifications.isEmpty()) { + const auto msg = pendingNotifications.dequeue(); + const bool avail = ensureDataAvailable(msg); + if (avail && pipeline.isEmpty()) { + emitNotification(msg); + } else { + pipeline.enqueue(msg); + } + } +} + +static Relation::List extractRelations(const QSet &rels) +{ + Relation::List relations; + if (rels.isEmpty()) { + return relations; + } + + relations.reserve(rels.size()); + for (const auto &rel : rels) { + relations.push_back(Relation(rel.type.toLatin1(), Akonadi::Item(rel.leftId), Akonadi::Item(rel.rightId))); + } + return relations; +} + +bool MonitorPrivate::emitItemsNotification(const Protocol::ItemChangeNotification &msg, + const Item::List &items, + const Collection &collection, + const Collection &collectionDest) +{ + Collection col = collection; + Collection colDest = collectionDest; + if (!col.isValid()) { + col = Collection(msg.parentCollection()); + col.setResource(QString::fromUtf8(msg.resource())); + } + if (!colDest.isValid()) { + colDest = Collection(msg.parentDestCollection()); + // HACK: destination resource is delivered in the parts field... + if (!msg.itemParts().isEmpty()) { + colDest.setResource(QString::fromLatin1(*(msg.itemParts().cbegin()))); + } + } + + Relation::List addedRelations; + Relation::List removedRelations; + if (msg.operation() == Protocol::ItemChangeNotification::ModifyRelations) { + addedRelations = extractRelations(msg.addedRelations()); + removedRelations = extractRelations(msg.removedRelations()); + } + + Tag::List addedTags; + Tag::List removedTags; + if (msg.operation() == Protocol::ItemChangeNotification::ModifyTags) { + addedTags = tagCache->retrieve(msg.addedTags() | Actions::toQList); + removedTags = tagCache->retrieve(msg.removedTags() | Actions::toQList); + } + + Item::List its = items; + for (auto it = its.begin(), end = its.end(); it != end; ++it) { + if (msg.operation() == Protocol::ItemChangeNotification::Move) { + it->setParentCollection(colDest); + } else { + it->setParentCollection(col); + } + } + bool handled = false; + switch (msg.operation()) { + case Protocol::ItemChangeNotification::Add: + return emitToListeners(&Monitor::itemAdded, its.first(), col); + case Protocol::ItemChangeNotification::Modify: + return emitToListeners(&Monitor::itemChanged, its.first(), msg.itemParts()); + case Protocol::ItemChangeNotification::ModifyFlags: + return emitToListeners(&Monitor::itemsFlagsChanged, its, msg.addedFlags(), msg.removedFlags()); + case Protocol::ItemChangeNotification::Move: + handled |= emitToListeners(&Monitor::itemMoved, its.first(), col, colDest); + handled |= emitToListeners(&Monitor::itemsMoved, its, col, colDest); + return handled; + case Protocol::ItemChangeNotification::Remove: + handled |= emitToListeners(&Monitor::itemRemoved, its.first()); + handled |= emitToListeners(&Monitor::itemsRemoved, its); + return handled; + case Protocol::ItemChangeNotification::Link: + handled |= emitToListeners(&Monitor::itemLinked, its.first(), col); + handled |= emitToListeners(&Monitor::itemsLinked, its, col); + return handled; + case Protocol::ItemChangeNotification::Unlink: + handled |= emitToListeners(&Monitor::itemUnlinked, its.first(), col); + handled |= emitToListeners(&Monitor::itemsUnlinked, its, col); + return handled; + case Protocol::ItemChangeNotification::ModifyTags: + return emitToListeners(&Monitor::itemsTagsChanged, its, addedTags | Actions::toQSet, removedTags | Actions::toQSet); + case Protocol::ItemChangeNotification::ModifyRelations: + return emitToListeners(&Monitor::itemsRelationsChanged, its, addedRelations, removedRelations); + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in item change notification"; + return false; + } +} + +bool MonitorPrivate::emitCollectionNotification(const Protocol::CollectionChangeNotification &msg, + const Collection &col, + const Collection &par, + const Collection &dest) +{ + Collection parent = par; + if (!parent.isValid()) { + parent = Collection(msg.parentCollection()); + } + Collection destination = dest; + if (!destination.isValid()) { + destination = Collection(msg.parentDestCollection()); + } + + Collection collection = col; + Q_ASSERT(collection.isValid()); + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Failed to get valid Collection for a Collection change!"; + return true; // prevent Monitor disconnecting from a signal + } + + if (msg.operation() == Protocol::CollectionChangeNotification::Move) { + collection.setParentCollection(destination); + } else { + collection.setParentCollection(parent); + } + + bool handled = false; + switch (msg.operation()) { + case Protocol::CollectionChangeNotification::Add: + return emitToListeners(&Monitor::collectionAdded, collection, parent); + case Protocol::CollectionChangeNotification::Modify: + handled |= emitToListeners(QOverload::of(&Monitor::collectionChanged), collection); + handled |= + emitToListeners(QOverload &>::of(&Monitor::collectionChanged), collection, msg.changedParts()); + return handled; + case Protocol::CollectionChangeNotification::Move: + return emitToListeners(&Monitor::collectionMoved, collection, parent, destination); + case Protocol::CollectionChangeNotification::Remove: + return emitToListeners(&Monitor::collectionRemoved, collection); + case Protocol::CollectionChangeNotification::Subscribe: + return emitToListeners(&Monitor::collectionSubscribed, collection, parent); + case Protocol::CollectionChangeNotification::Unsubscribe: + return emitToListeners(&Monitor::collectionUnsubscribed, collection); + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in collection change notification"; + return false; + } +} + +bool MonitorPrivate::emitTagNotification(const Protocol::TagChangeNotification &msg, const Tag &tag) +{ + Q_UNUSED(msg) + switch (msg.operation()) { + case Protocol::TagChangeNotification::Add: + return emitToListeners(&Monitor::tagAdded, tag); + case Protocol::TagChangeNotification::Modify: + return emitToListeners(&Monitor::tagChanged, tag); + case Protocol::TagChangeNotification::Remove: + return emitToListeners(&Monitor::tagRemoved, tag); + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in tag change notification"; + return false; + } +} + +bool MonitorPrivate::emitRelationNotification(const Protocol::RelationChangeNotification &msg, const Relation &relation) +{ + if (!relation.isValid()) { + return false; + } + + switch (msg.operation()) { + case Protocol::RelationChangeNotification::Add: + return emitToListeners(&Monitor::relationAdded, relation); + case Protocol::RelationChangeNotification::Remove: + return emitToListeners(&Monitor::relationRemoved, relation); + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in tag change notification"; + return false; + } +} + +bool MonitorPrivate::emitSubscriptionChangeNotification(const Protocol::SubscriptionChangeNotification &msg, const Akonadi::NotificationSubscriber &subscriber) +{ + if (!subscriber.isValid()) { + return false; + } + + switch (msg.operation()) { + case Protocol::SubscriptionChangeNotification::Add: + return emitToListeners(&Monitor::notificationSubscriberAdded, subscriber); + case Protocol::SubscriptionChangeNotification::Modify: + return emitToListeners(&Monitor::notificationSubscriberChanged, subscriber); + case Protocol::SubscriptionChangeNotification::Remove: + return emitToListeners(&Monitor::notificationSubscriberRemoved, subscriber); + default: + qCDebug(AKONADICORE_LOG) << "Unknown operation type" << msg.operation() << "in subscription change notification"; + return false; + } +} + +bool MonitorPrivate::emitDebugChangeNotification(const Protocol::DebugChangeNotification &msg, const ChangeNotification &ntf) +{ + Q_UNUSED(msg) + + if (!ntf.isValid()) { + return false; + } + + return emitToListeners(&Monitor::debugNotification, ntf); +} + +void MonitorPrivate::invalidateCaches(const Protocol::ChangeNotificationPtr &msg) +{ + // remove invalidates + // modify removes the cache entry, as we need to re-fetch + // And subscription modify the visibility of the collection by the collectionFetchScope. + switch (msg->type()) { + case Protocol::Command::CollectionChangeNotification: { + const auto &colNtf = Protocol::cmdCast(msg); + switch (colNtf.operation()) { + case Protocol::CollectionChangeNotification::Modify: + case Protocol::CollectionChangeNotification::Move: + case Protocol::CollectionChangeNotification::Subscribe: + collectionCache->update(colNtf.collection().id(), mCollectionFetchScope); + break; + case Protocol::CollectionChangeNotification::Remove: + collectionCache->invalidate(colNtf.collection().id()); + break; + default: + break; + } + } break; + case Protocol::Command::ItemChangeNotification: { + const auto &itemNtf = Protocol::cmdCast(msg); + switch (itemNtf.operation()) { + case Protocol::ItemChangeNotification::Modify: + case Protocol::ItemChangeNotification::ModifyFlags: + case Protocol::ItemChangeNotification::ModifyTags: + case Protocol::ItemChangeNotification::ModifyRelations: + case Protocol::ItemChangeNotification::Move: + itemCache->update(Protocol::ChangeNotification::itemsToUids(itemNtf.items()), mItemFetchScope); + break; + case Protocol::ItemChangeNotification::Remove: + itemCache->invalidate(Protocol::ChangeNotification::itemsToUids(itemNtf.items())); + break; + default: + break; + } + } break; + case Protocol::Command::TagChangeNotification: { + const auto &tagNtf = Protocol::cmdCast(msg); + switch (tagNtf.operation()) { + case Protocol::TagChangeNotification::Modify: + tagCache->update({tagNtf.tag().id()}, mTagFetchScope); + break; + case Protocol::TagChangeNotification::Remove: + tagCache->invalidate({tagNtf.tag().id()}); + break; + default: + break; + } + } break; + default: + break; + } +} + +void MonitorPrivate::invalidateCache(const Collection &col) +{ + collectionCache->update(col.id(), mCollectionFetchScope); +} + +void MonitorPrivate::ref(Collection::Id id) +{ + if (!refCountMap.contains(id)) { + refCountMap.insert(id, 0); + } + ++refCountMap[id]; + + if (m_buffer.isBuffered(id)) { + m_buffer.purge(id); + } +} + +Akonadi::Collection::Id MonitorPrivate::deref(Collection::Id id) +{ + Q_ASSERT(refCountMap.contains(id)); + if (--refCountMap[id] == 0) { + refCountMap.remove(id); + return m_buffer.buffer(id); + } + return -1; +} + +void MonitorPrivate::PurgeBuffer::purge(Collection::Id id) +{ + m_buffer.removeOne(id); +} + +Akonadi::Collection::Id MonitorPrivate::PurgeBuffer::buffer(Collection::Id id) +{ + // Ensure that we don't put a duplicate @p id into the buffer. + purge(id); + + Collection::Id bumpedId = -1; + if (m_buffer.size() == MAXBUFFERSIZE) { + bumpedId = m_buffer.dequeue(); + purge(bumpedId); + } + + m_buffer.enqueue(id); + + return bumpedId; +} + +int MonitorPrivate::PurgeBuffer::buffersize() +{ + return MAXBUFFERSIZE; +} + +bool MonitorPrivate::isMonitored(Collection::Id colId) const +{ + if (!useRefCounting) { + return true; + } + return refCountMap.contains(colId) || m_buffer.isBuffered(colId); +} + +void MonitorPrivate::notifyCollectionStatisticsWatchers(Collection::Id collection, const QByteArray &resource) +{ + if (collection > 0 && (monitorAll || isCollectionMonitored(collection) || resources.contains(resource))) { + recentlyChangedCollections.insert(collection); + if (!statisticsCompressionTimer.isActive()) { + statisticsCompressionTimer.start(); + } + } +} + +Protocol::ModifySubscriptionCommand::ChangeType MonitorPrivate::monitorTypeToProtocol(Monitor::Type type) +{ + switch (type) { + case Monitor::Collections: + return Protocol::ModifySubscriptionCommand::CollectionChanges; + case Monitor::Items: + return Protocol::ModifySubscriptionCommand::ItemChanges; + case Monitor::Tags: + return Protocol::ModifySubscriptionCommand::TagChanges; + case Monitor::Relations: + return Protocol::ModifySubscriptionCommand::RelationChanges; + case Monitor::Subscribers: + return Protocol::ModifySubscriptionCommand::SubscriptionChanges; + case Monitor::Notifications: + return Protocol::ModifySubscriptionCommand::ChangeNotifications; + default: + Q_ASSERT(false); + return Protocol::ModifySubscriptionCommand::NoType; + } +} + +void MonitorPrivate::updateListeners(QMetaMethod signal, ListenerAction action) +{ +#define UPDATE_LISTENERS(sig) \ + if (signal == QMetaMethod::fromSignal(sig)) { \ + updateListener(sig, action); \ + return; \ + } + + UPDATE_LISTENERS(&Monitor::itemChanged) + UPDATE_LISTENERS(&Monitor::itemChanged) + UPDATE_LISTENERS(&Monitor::itemsFlagsChanged) + UPDATE_LISTENERS(&Monitor::itemsTagsChanged) + UPDATE_LISTENERS(&Monitor::itemsRelationsChanged) + UPDATE_LISTENERS(&Monitor::itemMoved) + UPDATE_LISTENERS(&Monitor::itemsMoved) + UPDATE_LISTENERS(&Monitor::itemAdded) + UPDATE_LISTENERS(&Monitor::itemRemoved) + UPDATE_LISTENERS(&Monitor::itemsRemoved) + UPDATE_LISTENERS(&Monitor::itemLinked) + UPDATE_LISTENERS(&Monitor::itemsLinked) + UPDATE_LISTENERS(&Monitor::itemUnlinked) + UPDATE_LISTENERS(&Monitor::itemsUnlinked) + UPDATE_LISTENERS(&Monitor::collectionAdded) + + UPDATE_LISTENERS(QOverload::of(&Monitor::collectionChanged)) + UPDATE_LISTENERS((QOverload &>::of(&Monitor::collectionChanged))) + UPDATE_LISTENERS(&Monitor::collectionMoved) + UPDATE_LISTENERS(&Monitor::collectionRemoved) + UPDATE_LISTENERS(&Monitor::collectionSubscribed) + UPDATE_LISTENERS(&Monitor::collectionUnsubscribed) + UPDATE_LISTENERS(&Monitor::collectionStatisticsChanged) + + UPDATE_LISTENERS(&Monitor::tagAdded) + UPDATE_LISTENERS(&Monitor::tagChanged) + UPDATE_LISTENERS(&Monitor::tagRemoved) + + UPDATE_LISTENERS(&Monitor::relationAdded) + UPDATE_LISTENERS(&Monitor::relationRemoved) + + UPDATE_LISTENERS(&Monitor::notificationSubscriberAdded) + UPDATE_LISTENERS(&Monitor::notificationSubscriberChanged) + UPDATE_LISTENERS(&Monitor::notificationSubscriberRemoved) + UPDATE_LISTENERS(&Monitor::debugNotification) + +#undef UPDATE_LISTENERS +} + +/// @endcond diff --git a/src/core/monitor_p.h b/src/core/monitor_p.h new file mode 100644 index 0000000..428f72d --- /dev/null +++ b/src/core/monitor_p.h @@ -0,0 +1,398 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "changenotificationdependenciesfactory_p.h" +#include "collection.h" +#include "collectionfetchscope.h" +#include "collectionstatisticsjob.h" +#include "commandbuffer_p.h" +#include "connection_p.h" +#include "entitycache_p.h" +#include "item.h" +#include "itemfetchscope.h" +#include "job.h" +#include "monitor.h" +#include "servermanager.h" +#include "tagfetchscope.h" + +#include "private/protocol_p.h" + +#include +#include + +#include +#include +#include + +namespace Akonadi +{ +class Monitor; +class ChangeNotification; + +// A helper struct to wrap pointer to member function (which cannot be contained +// in a regular pointer) +struct SignalId { + constexpr SignalId() = default; + + using Unit = uint; + static constexpr int Size = sizeof(&Monitor::itemAdded) / sizeof(Unit); + Unit data[sizeof(&Monitor::itemAdded) / sizeof(Unit)] = {0}; + + inline bool operator==(SignalId other) const + { + for (int i = Size - 1; i >= 0; --i) { + if (data[i] != other.data[i]) { + return false; + } + } + return true; + } +}; + +inline uint qHash(SignalId sig) +{ + // The 4 LSBs of the address should be enough to give us a good hash + return sig.data[SignalId::Size - 1]; +} + +/** + * @internal + */ +class AKONADICORE_EXPORT MonitorPrivate +{ +public: + enum ListenerAction { + AddListener, + RemoveListener, + }; + + MonitorPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_, Monitor *parent); + virtual ~MonitorPrivate(); + void init(); + + Monitor *q_ptr; + Q_DECLARE_PUBLIC(Monitor) + ChangeNotificationDependenciesFactory *dependenciesFactory = nullptr; + QPointer ntfConnection; + Collection::List collections; + QSet resources; + QSet items; + QSet tags; + QSet types; + QSet mimetypes; + bool monitorAll; + bool exclusive; + QList sessions; + ItemFetchScope mItemFetchScope; + TagFetchScope mTagFetchScope; + CollectionFetchScope mCollectionFetchScope; + bool mFetchChangedOnly; + Session *session = nullptr; + CollectionCache *collectionCache = nullptr; + ItemListCache *itemCache = nullptr; + TagListCache *tagCache = nullptr; + QMimeDatabase mimeDatabase; + QHash listeners; + + CommandBuffer mCommandBuffer; + + Protocol::ModifySubscriptionCommand::ModifiedParts pendingModificationChanges; + Protocol::ModifySubscriptionCommand pendingModification; + QTimer *pendingModificationTimer; + bool monitorReady; + + // The waiting list + QQueue pendingNotifications; + // The messages for which data is currently being fetched + QQueue pipeline; + // In a pure Monitor, the pipeline contains items that were dequeued from pendingNotifications. + // The ordering [pipeline] [pendingNotifications] is kept at all times. + // [] [A B C] -> [A B] [C] -> [B] [C] -> [B C] [] -> [C] [] -> [] + // In a ChangeRecorder, the pipeline contains one item only, and not dequeued yet. + // [] [A B C] -> [A] [A B C] -> [] [A B C] -> (changeProcessed) [] [B C] -> [B] [B C] etc... + + bool fetchCollection; + bool fetchCollectionStatistics; + bool collectionMoveTranslationEnabled; + + // Virtual methods for ChangeRecorder + virtual void notificationsEnqueued(int) + { + } + virtual void notificationsErased() + { + } + + // Virtual so it can be overridden in FakeMonitor. + virtual bool connectToNotificationManager(); + void disconnectFromNotificationManager(); + + void dispatchNotifications(); + void flushPipeline(); + + bool ensureDataAvailable(const Protocol::ChangeNotificationPtr &msg); + /** + * Sends out the change notification @p msg. + * @param msg the change notification to send + * @return @c true if the notification was actually send to someone, @c false if no one was listening. + */ + virtual bool emitNotification(const Protocol::ChangeNotificationPtr &msg); + void updatePendingStatistics(const Protocol::ChangeNotificationPtr &msg); + void invalidateCaches(const Protocol::ChangeNotificationPtr &msg); + + /** Used by ResourceBase to inform us about collection changes before the notifications are emitted, + needed to avoid the missing RID race on change replay. + */ + void invalidateCache(const Collection &col); + + /// Virtual so that ChangeRecorder can set it to 0 and handle the pipeline itself + virtual int pipelineSize() const; + + // private Q_SLOTS + void dataAvailable(); + void slotSessionDestroyed(QObject *object); + void slotStatisticsChangedFinished(KJob *job); + void slotFlushRecentlyChangedCollections(); + + /** + Returns whether a message was appended to @p notificationQueue + */ + int translateAndCompress(QQueue ¬ificationQueue, const Protocol::ChangeNotificationPtr &msg); + + void handleCommands(); + + virtual void slotNotify(const Protocol::ChangeNotificationPtr &msg); + + /** + * Sends out a change notification for an item. + * @return @c true if the notification was actually send to someone, @c false if no one was listening. + */ + bool emitItemsNotification(const Protocol::ItemChangeNotification &msg, + const Item::List &items = Item::List(), + const Collection &collection = Collection(), + const Collection &collectionDest = Collection()); + /** + * Sends out a change notification for a collection. + * @return @c true if the notification was actually send to someone, @c false if no one was listening. + */ + bool emitCollectionNotification(const Protocol::CollectionChangeNotification &msg, + const Collection &col = Collection(), + const Collection &par = Collection(), + const Collection &dest = Collection()); + + bool emitTagNotification(const Protocol::TagChangeNotification &msg, const Tag &tags); + + bool emitRelationNotification(const Protocol::RelationChangeNotification &msg, const Relation &relation); + + bool emitSubscriptionChangeNotification(const Protocol::SubscriptionChangeNotification &msg, const NotificationSubscriber &subscriber); + + bool emitDebugChangeNotification(const Protocol::DebugChangeNotification &msg, const ChangeNotification &ntf); + + void serverStateChanged(Akonadi::ServerManager::State state); + + /** + * This method is called by the ChangeMediator to enforce an invalidation of the passed collection. + */ + void invalidateCollectionCache(qint64 collectionId); + + /** + * This method is called by the ChangeMediator to enforce an invalidation of the passed item. + */ + void invalidateItemCache(qint64 itemId); + + /** + * This method is called by the ChangeMediator to enforce an invalidation of the passed tag. + */ + void invalidateTagCache(qint64 tagId); + + void scheduleSubscriptionUpdate(); + void slotUpdateSubscription(); + + void updateListeners(QMetaMethod signal, ListenerAction action); + + template void updateListener(Signal signal, ListenerAction action) + { + auto it = listeners.find(signalId(signal)); + if (action == AddListener) { + if (it == listeners.end()) { + it = listeners.insert(signalId(signal), 0); + } + ++(*it); + } else { + if (--(*it) == 0) { + listeners.erase(it); + } + } + } + + static Protocol::ModifySubscriptionCommand::ChangeType monitorTypeToProtocol(Monitor::Type type); + + /** + @brief Class used to determine when to purge items in a Collection + + The buffer method can be used to buffer a Collection. This may cause another Collection + to be purged if it is removed from the buffer. + + The purge method is used to purge a Collection from the buffer, but not the model. + This is used for example, to not buffer Collections anymore if they get referenced, + and to ensure that one Collection does not appear twice in the buffer. + + Check whether a Collection is buffered using the isBuffered method. + */ + class AKONADI_TESTS_EXPORT PurgeBuffer + { + // Buffer the most recent 10 unreferenced Collections + static const int MAXBUFFERSIZE = 10; + + public: + explicit PurgeBuffer() + { + } + + /** + Adds @p id to the Collections to be buffered + + @returns The collection id which was removed form the buffer or -1 if none. + */ + Collection::Id buffer(Collection::Id id); + + /** + Removes @p id from the Collections being buffered + */ + void purge(Collection::Id id); + + bool isBuffered(Collection::Id id) const + { + return m_buffer.contains(id); + } + + static int buffersize(); + + private: + QQueue m_buffer; + } m_buffer; + + QHash refCountMap; + bool useRefCounting; + void ref(Collection::Id id); + Collection::Id deref(Collection::Id id); + + /** + * Returns true if the collection is monitored by monitor. + * + * A collection is always monitored if useRefCounting is false. + * If ref counting is used, the collection is only monitored, + * if the collection is either in refCountMap or m_buffer. + * If ref counting is used and the collection is not in refCountMap or m_buffer, + * no updates for the contained items are emitted, because they are lazily ignored. + */ + bool isMonitored(Collection::Id colId) const; + +private: + // collections that need a statistics update + QSet recentlyChangedCollections; + QTimer statisticsCompressionTimer; + + /** + @returns True if @p msg should be ignored. Otherwise appropriate signals are emitted for it. + */ + bool isLazilyIgnored(const Protocol::ChangeNotificationPtr &msg, bool allowModifyFlagsConversion = false) const; + + /** + Sets @p needsSplit to True when @p msg contains more than one item and there's at least one + listener that does not support batch operations. Sets @p batchSupported to True when + there's at least one listener that supports batch operations. + */ + void checkBatchSupport(const Protocol::ChangeNotificationPtr &msg, bool &needsSplit, bool &batchSupported) const; + + Protocol::ChangeNotificationList splitMessage(const Protocol::ItemChangeNotification &msg, bool legacy) const; + + bool isCollectionMonitored(Collection::Id collection) const + { + if (collection < 0) { + return false; + } + if (collections.contains(Collection(collection))) { + return true; + } + if (collections.contains(Collection::root())) { + return true; + } + return false; + } + + bool isMimeTypeMonitored(const QString &mimetype) const + { + if (mimetypes.contains(mimetype)) { + return true; + } + + const QMimeType mimeType = mimeDatabase.mimeTypeForName(mimetype); + if (!mimeType.isValid()) { + return false; + } + + for (const QString &mt : mimetypes) { + if (mimeType.inherits(mt)) { + return true; + } + } + + return false; + } + + template bool isMoveDestinationResourceMonitored(const T &msg) const + { + if (msg.operation() != T::Move) { + return false; + } + return resources.contains(msg.destinationResource()); + } + + void fetchStatistics(Collection::Id colId) + { + auto job = new CollectionStatisticsJob(Collection(colId), session); + QObject::connect(job, &KJob::result, q_ptr, [this](KJob *job) { + slotStatisticsChangedFinished(job); + }); + } + + void notifyCollectionStatisticsWatchers(Collection::Id collection, const QByteArray &resource); + bool fetchCollections() const; + bool fetchItems() const; + + // A hack to "cast" pointer to member function to something we can easily + // use as a key in the hashtable + template constexpr SignalId signalId(Signal signal) const + { + union { + Signal in; + SignalId out; + } h = {signal}; + return h.out; + } + + template bool hasListeners(Signal signal) const + { + auto it = listeners.find(signalId(signal)); + return it != listeners.end(); + } + + template bool emitToListeners(Signal signal, Args... args) + { + if (hasListeners(signal)) { + Q_EMIT(q_ptr->*signal)(std::forward(args)...); + return true; + } + return false; + } +}; + +} + diff --git a/src/core/notificationsource_p.cpp b/src/core/notificationsource_p.cpp new file mode 100644 index 0000000..8b96a3c --- /dev/null +++ b/src/core/notificationsource_p.cpp @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationsource_p.h" +#include "notificationsourceinterface.h" + +using namespace Akonadi; + +NotificationSource::NotificationSource(QObject *source) + : QObject(source) +{ + Q_ASSERT(source); +} + +NotificationSource::~NotificationSource() +{ +} + +QString NotificationSource::identifier() const +{ + auto source = qobject_cast(parent()); + return source->path(); +} + +void NotificationSource::setAllMonitored(bool allMonitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setAllMonitored", Q_ARG(bool, allMonitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setExclusive(bool exclusive) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setExclusive", Q_ARG(bool, exclusive)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setMonitoredCollection(Collection::Id id, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredCollection", Q_ARG(qlonglong, id), Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setMonitoredItem(Item::Id id, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredItem", Q_ARG(qlonglong, id), Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setMonitoredResource(const QByteArray &resource, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredResource", Q_ARG(QByteArray, resource), Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setMonitoredMimeType(const QString &mimeType, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredMimeType", Q_ARG(QString, mimeType), Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setIgnoredSession(const QByteArray &session, bool ignored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setIgnoredSession", Q_ARG(QByteArray, session), Q_ARG(bool, ignored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setMonitoredTag(Tag::Id id, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredTag", Q_ARG(qlonglong, id), Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setMonitoredType(Protocol::ChangeNotification::Type type, bool monitored) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setMonitoredType", Q_ARG(Akonadi::Protocol::ChangeNotification::Type, type), Q_ARG(bool, monitored)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +void NotificationSource::setSession(const QByteArray &session) +{ + const bool ok = QMetaObject::invokeMethod(parent(), "setSession", Q_ARG(QByteArray, session)); + Q_ASSERT(ok); + Q_UNUSED(ok) +} + +QObject *NotificationSource::source() const +{ + return parent(); +} diff --git a/src/core/notificationsource_p.h b/src/core/notificationsource_p.h new file mode 100644 index 0000000..e5ddec9 --- /dev/null +++ b/src/core/notificationsource_p.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonaditests_export.h" + +#include "collection.h" +#include "item.h" +#include "tag.h" + +#include "private/protocol_p.h" + +namespace Akonadi +{ +class AKONADI_TESTS_EXPORT NotificationSource : public QObject +{ + Q_OBJECT + +public: + explicit NotificationSource(QObject *source); + ~NotificationSource(); + + QString identifier() const; + + void setAllMonitored(bool allMonitored); + void setExclusive(bool exclusive); + void setMonitoredCollection(Collection::Id id, bool monitored); + void setMonitoredItem(Item::Id id, bool monitored); + void setMonitoredResource(const QByteArray &resource, bool monitored); + void setMonitoredMimeType(const QString &mimeType, bool monitored); + void setMonitoredTag(Tag::Id id, bool monitored); + void setMonitoredType(Protocol::ChangeNotification::Type type, bool monitored); + void setIgnoredSession(const QByteArray &session, bool monitored); + void setSession(const QByteArray &session); + + QObject *source() const; +}; + +} + diff --git a/src/core/notificationsubscriber.cpp b/src/core/notificationsubscriber.cpp new file mode 100644 index 0000000..e07f844 --- /dev/null +++ b/src/core/notificationsubscriber.cpp @@ -0,0 +1,200 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationsubscriber.h" +#include "collectionfetchscope.h" +#include "itemfetchscope.h" +#include "tagfetchscope.h" + +namespace Akonadi +{ +class AKONADICORE_NO_EXPORT NotificationSubscriber::Private : public QSharedData +{ +public: + QByteArray subscriber; + QByteArray sessionId; + QSet collections; + QSet items; + QSet tags; + QSet types; + QSet mimeTypes; + QSet resources; + QSet ignoredSessions; + ItemFetchScope itemFetchScope; + CollectionFetchScope collectionFetchScope; + TagFetchScope tagFetchScope; + bool isAllMonitored = false; + bool isExclusive = false; +}; + +} // namespace Akonadi + +using namespace Akonadi; + +NotificationSubscriber::NotificationSubscriber() + : d(new Private) +{ +} + +NotificationSubscriber::NotificationSubscriber(const NotificationSubscriber &other) + : d(other.d) +{ +} + +NotificationSubscriber::~NotificationSubscriber() +{ +} + +NotificationSubscriber &NotificationSubscriber::operator=(const NotificationSubscriber &other) +{ + d = other.d; + return *this; +} + +bool NotificationSubscriber::isValid() const +{ + return !d->subscriber.isEmpty(); +} + +QByteArray NotificationSubscriber::subscriber() const +{ + return d->subscriber; +} + +void NotificationSubscriber::setSubscriber(const QByteArray &subscriber) +{ + d->subscriber = subscriber; +} + +QByteArray NotificationSubscriber::sessionId() const +{ + return d->sessionId; +} + +void NotificationSubscriber::setSessionId(const QByteArray &sessionId) +{ + d->sessionId = sessionId; +} + +QSet NotificationSubscriber::monitoredCollections() const +{ + return d->collections; +} + +void NotificationSubscriber::setMonitoredCollections(const QSet &collections) +{ + d->collections = collections; +} + +QSet NotificationSubscriber::monitoredItems() const +{ + return d->items; +} + +void NotificationSubscriber::setMonitoredItems(const QSet &items) +{ + d->items = items; +} + +QSet NotificationSubscriber::monitoredTags() const +{ + return d->tags; +} + +void NotificationSubscriber::setMonitoredTags(const QSet &tags) +{ + d->tags = tags; +} + +QSet NotificationSubscriber::monitoredTypes() const +{ + return d->types; +} + +void NotificationSubscriber::setMonitoredTypes(const QSet &types) +{ + d->types = types; +} + +QSet NotificationSubscriber::monitoredMimeTypes() const +{ + return d->mimeTypes; +} + +void NotificationSubscriber::setMonitoredMimeTypes(const QSet &mimeTypes) +{ + d->mimeTypes = mimeTypes; +} + +QSet NotificationSubscriber::monitoredResources() const +{ + return d->resources; +} + +void NotificationSubscriber::setMonitoredResources(const QSet &resources) +{ + d->resources = resources; +} + +QSet NotificationSubscriber::ignoredSessions() const +{ + return d->ignoredSessions; +} + +void NotificationSubscriber::setIgnoredSessions(const QSet &ignoredSessions) +{ + d->ignoredSessions = ignoredSessions; +} + +bool NotificationSubscriber::isAllMonitored() const +{ + return d->isAllMonitored; +} + +void NotificationSubscriber::setIsAllMonitored(bool isAllMonitored) +{ + d->isAllMonitored = isAllMonitored; +} + +bool NotificationSubscriber::isExclusive() const +{ + return d->isExclusive; +} + +void NotificationSubscriber::setIsExclusive(bool isExclusive) +{ + d->isExclusive = isExclusive; +} + +ItemFetchScope NotificationSubscriber::itemFetchScope() const +{ + return d->itemFetchScope; +} + +void NotificationSubscriber::setItemFetchScope(const ItemFetchScope &itemFetchScope) +{ + d->itemFetchScope = itemFetchScope; +} + +CollectionFetchScope NotificationSubscriber::collectionFetchScope() const +{ + return d->collectionFetchScope; +} + +void NotificationSubscriber::setCollectionFetchScope(const CollectionFetchScope &fetchScope) +{ + d->collectionFetchScope = fetchScope; +} + +TagFetchScope NotificationSubscriber::tagFetchScope() const +{ + return d->tagFetchScope; +} + +void NotificationSubscriber::setTagFetchScope(const TagFetchScope &tagFetchScope) +{ + d->tagFetchScope = tagFetchScope; +} diff --git a/src/core/notificationsubscriber.h b/src/core/notificationsubscriber.h new file mode 100644 index 0000000..0e3c3d7 --- /dev/null +++ b/src/core/notificationsubscriber.h @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "monitor.h" +#include + +namespace Akonadi +{ +class AKONADICORE_EXPORT NotificationSubscriber +{ +public: + explicit NotificationSubscriber(); + NotificationSubscriber(const NotificationSubscriber &other); + ~NotificationSubscriber(); + + NotificationSubscriber &operator=(const NotificationSubscriber &other); + + bool isValid() const; + + QByteArray subscriber() const; + void setSubscriber(const QByteArray &subscriber); + + QByteArray sessionId() const; + void setSessionId(const QByteArray &sessionId); + + QSet monitoredCollections() const; + void setMonitoredCollections(const QSet &collections); + + QSet monitoredItems() const; + void setMonitoredItems(const QSet &items); + + QSet monitoredTags() const; + void setMonitoredTags(const QSet &tags); + + QSet monitoredTypes() const; + void setMonitoredTypes(const QSet &type); + + QSet monitoredMimeTypes() const; + void setMonitoredMimeTypes(const QSet &mimeTypes); + + QSet monitoredResources() const; + void setMonitoredResources(const QSet &resources); + + QSet ignoredSessions() const; + void setIgnoredSessions(const QSet &ignoredSessions); + + bool isAllMonitored() const; + void setIsAllMonitored(bool isAllMonitored); + + bool isExclusive() const; + void setIsExclusive(bool isExclusive); + + ItemFetchScope itemFetchScope() const; + void setItemFetchScope(const ItemFetchScope &itemFetchScope); + + CollectionFetchScope collectionFetchScope() const; + void setCollectionFetchScope(const CollectionFetchScope &collectionFetchScope); + + TagFetchScope tagFetchScope() const; + void setTagFetchScope(const TagFetchScope &tagFetchScope); + +private: + class Private; + QSharedDataPointer d; +}; + +} diff --git a/src/core/partfetcher.cpp b/src/core/partfetcher.cpp new file mode 100644 index 0000000..7d3740c --- /dev/null +++ b/src/core/partfetcher.cpp @@ -0,0 +1,170 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "partfetcher.h" + +#include "entitytreemodel.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" +#include "session.h" +#include + +Q_DECLARE_METATYPE(QSet) + +using namespace Akonadi; + +namespace Akonadi +{ +class PartFetcherPrivate +{ + PartFetcherPrivate(PartFetcher *partFetcher, const QModelIndex &index, const QByteArray &partName) + : m_persistentIndex(index) + , m_partName(partName) + , q_ptr(partFetcher) + { + } + + void fetchJobDone(KJob *job); + + void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + QPersistentModelIndex m_persistentIndex; + QByteArray m_partName; + Item m_item; + + Q_DECLARE_PUBLIC(PartFetcher) + PartFetcher *q_ptr; +}; + +} // namespace Akonadi + +void PartFetcherPrivate::fetchJobDone(KJob *job) +{ + Q_Q(PartFetcher); + if (job->error()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Unable to fetch item for index")); + q->emitResult(); + return; + } + + auto fetchJob = qobject_cast(job); + + const Item::List list = fetchJob->items(); + + Q_ASSERT(list.size() == 1); + + // If m_persistentIndex comes from a selection proxy model, it could become + // invalid if the user clicks around a lot. + if (!m_persistentIndex.isValid()) { + q->setError(KJob::UserDefinedError); + q->setErrorText(i18n("Index is no longer available")); + q->emitResult(); + return; + } + + const auto loadedParts = m_persistentIndex.data(EntityTreeModel::LoadedPartsRole).value>(); + + Q_ASSERT(!loadedParts.contains(m_partName)); + + Item item = m_persistentIndex.data(EntityTreeModel::ItemRole).value(); + + item.apply(list.at(0)); + + auto model = const_cast(m_persistentIndex.model()); + + Q_ASSERT(model); + + QVariant itemVariant = QVariant::fromValue(item); + model->setData(m_persistentIndex, itemVariant, EntityTreeModel::ItemRole); + + m_item = item; + + q->emitResult(); +} + +PartFetcher::PartFetcher(const QModelIndex &index, const QByteArray &partName, QObject *parent) + : KJob(parent) + , d_ptr(new PartFetcherPrivate(this, index, partName)) +{ +} + +PartFetcher::~PartFetcher() +{ + delete d_ptr; +} + +void PartFetcher::start() +{ + Q_D(PartFetcher); + + const QModelIndex index = d->m_persistentIndex; + + const auto loadedParts = index.data(EntityTreeModel::LoadedPartsRole).value>(); + + if (loadedParts.contains(d->m_partName)) { + d->m_item = d->m_persistentIndex.data(EntityTreeModel::ItemRole).value(); + emitResult(); + return; + } + + const auto availableParts = index.data(EntityTreeModel::AvailablePartsRole).value>(); + if (!availableParts.contains(d->m_partName)) { + setError(UserDefinedError); + setErrorText(i18n("Payload part '%1' is not available for this index", QString::fromLatin1(d->m_partName))); + emitResult(); + return; + } + + auto session = qobject_cast(qvariant_cast(index.data(EntityTreeModel::SessionRole))); + + if (!session) { + setError(UserDefinedError); + setErrorText(i18n("No session available for this index")); + emitResult(); + return; + } + + const auto item = index.data(EntityTreeModel::ItemRole).value(); + + if (!item.isValid()) { + setError(UserDefinedError); + setErrorText(i18n("No item available for this index")); + emitResult(); + return; + } + + ItemFetchScope scope; + scope.fetchPayloadPart(d->m_partName); + auto itemFetchJob = new Akonadi::ItemFetchJob(item, session); + itemFetchJob->setFetchScope(scope); + connect(itemFetchJob, &KJob::result, this, [d](KJob *job) { + d->fetchJobDone(job); + }); +} + +QModelIndex PartFetcher::index() const +{ + Q_D(const PartFetcher); + + return d->m_persistentIndex; +} + +QByteArray PartFetcher::partName() const +{ + Q_D(const PartFetcher); + + return d->m_partName; +} + +Item PartFetcher::item() const +{ + Q_D(const PartFetcher); + + return d->m_item; +} + +#include "moc_partfetcher.cpp" diff --git a/src/core/partfetcher.h b/src/core/partfetcher.h new file mode 100644 index 0000000..59b33a2 --- /dev/null +++ b/src/core/partfetcher.h @@ -0,0 +1,106 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadicore_export.h" + +class QModelIndex; + +namespace Akonadi +{ +class Item; +class PartFetcherPrivate; + +/** + * @short Convenience class for getting payload parts from an Akonadi Model. + * + * This class can be used to retrieve individual payload parts from an EntityTreeModel, + * and fetch them asynchronously from the Akonadi storage if necessary. + * + * The requested part is emitted though the partFetched signal. + * + * Example: + * + * @code + * + * const QModelIndex index = view->selectionModel()->currentIndex(); + * + * PartFetcher *fetcher = new PartFetcher( index, Akonadi::MessagePart::Envelope ); + * connect( fetcher, SIGNAL(result(KJob*)), SLOT(fetchResult(KJob*)) ); + * fetcher->start(); + * + * ... + * + * MyClass::fetchResult( KJob *job ) + * { + * if ( job->error() ) { + * qDebug() << job->errorText(); + * return; + * } + * + * PartFetcher *fetcher = qobject_cast( job ); + * + * const Item item = fetcher->item(); + * // do something with the item + * } + * + * @endcode + * + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADICORE_EXPORT PartFetcher : public KJob +{ + Q_OBJECT + +public: + /** + * Creates a new part fetcher. + * + * @param index The index of the item to fetch the part from. + * @param partName The name of the payload part to fetch. + * @param parent The parent object. + */ + PartFetcher(const QModelIndex &index, const QByteArray &partName, QObject *parent = nullptr); + + /** + * Destroys the part fetcher. + */ + ~PartFetcher() override; + + /** + * Starts the fetch operation. + */ + void start() override; + + /** + * Returns the index of the item the part was fetched from. + */ + QModelIndex index() const; + + /** + * Returns the name of the part that has been fetched. + */ + QByteArray partName() const; + + /** + * Returns the item that contains the fetched payload part. + */ + Item item() const; + +private: + /// @cond PRIVATE + Q_DECLARE_PRIVATE(Akonadi::PartFetcher) + PartFetcherPrivate *const d_ptr; + + /// @endcond +}; + +} + diff --git a/src/core/pastehelper.cpp b/src/core/pastehelper.cpp new file mode 100644 index 0000000..d5992de --- /dev/null +++ b/src/core/pastehelper.cpp @@ -0,0 +1,324 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "pastehelper_p.h" + +#include "collectioncopyjob.h" +#include "collectionfetchjob.h" +#include "collectionmovejob.h" +#include "item.h" +#include "itemcopyjob.h" +#include "itemcreatejob.h" +#include "itemmodifyjob.h" +#include "itemmovejob.h" +#include "linkjob.h" +#include "session.h" +#include "transactionsequence.h" +#include "unlinkjob.h" + +#include "akonadicore_debug.h" + +#include +#include + +#include +#include + +#include + +using namespace Akonadi; + +class PasteHelperJob : public Akonadi::TransactionSequence +{ + Q_OBJECT + +public: + explicit PasteHelperJob(Qt::DropAction action, + const Akonadi::Item::List &items, + const Akonadi::Collection::List &collections, + const Akonadi::Collection &destination, + QObject *parent = nullptr); + virtual ~PasteHelperJob(); + +private Q_SLOTS: + void onDragSourceCollectionFetched(KJob *job); + +private: + void runActions(); + void runItemsActions(); + void runCollectionsActions(); + +private: + Akonadi::Item::List mItems; + Akonadi::Collection::List mCollections; + Akonadi::Collection mDestCollection; + Qt::DropAction mAction; +}; + +PasteHelperJob::PasteHelperJob(Qt::DropAction action, + const Item::List &items, + const Collection::List &collections, + const Collection &destination, + QObject *parent) + : TransactionSequence(parent) + , mItems(items) + , mCollections(collections) + , mDestCollection(destination) + , mAction(action) +{ + // FIXME: The below code disables transactions in order to avoid data loss due to nested + // transactions (copy and colcopy in the server doesn't see the items retrieved into the cache and copies empty payloads). + // Remove once this is fixed properly, see the other FIXME comments. + setProperty("transactionsDisabled", true); + + Collection dragSourceCollection; + if (!items.isEmpty() && items.first().parentCollection().isValid()) { + // Check if all items have the same parent collection ID + const Collection parent = items.first().parentCollection(); + if (!std::any_of(items.cbegin(), items.cend(), [parent](const Item &item) { + return item.parentCollection() != parent; + })) { + dragSourceCollection = parent; + } + } + + if (dragSourceCollection.isValid()) { + // Disable autocommitting, because starting a Link/Unlink/Copy/Move job + // after the transaction has ended leaves the job hanging + setAutomaticCommittingEnabled(false); + + auto fetch = new CollectionFetchJob(dragSourceCollection, CollectionFetchJob::Base, this); + QObject::connect(fetch, &KJob::finished, this, &PasteHelperJob::onDragSourceCollectionFetched); + } else { + runActions(); + } +} + +PasteHelperJob::~PasteHelperJob() +{ +} + +void PasteHelperJob::onDragSourceCollectionFetched(KJob *job) +{ + auto fetch = qobject_cast(job); + qCDebug(AKONADICORE_LOG) << fetch->error() << fetch->collections().count(); + if (fetch->error() || fetch->collections().count() != 1) { + runActions(); + commit(); + return; + } + + // If the source collection is virtual, treat copy and move actions differently + const Collection sourceCollection = fetch->collections().at(0); + qCDebug(AKONADICORE_LOG) << "FROM: " << sourceCollection.id() << sourceCollection.name() << sourceCollection.isVirtual(); + qCDebug(AKONADICORE_LOG) << "DEST: " << mDestCollection.id() << mDestCollection.name() << mDestCollection.isVirtual(); + qCDebug(AKONADICORE_LOG) << "ACTN:" << mAction; + if (sourceCollection.isVirtual()) { + switch (mAction) { + case Qt::CopyAction: + if (mDestCollection.isVirtual()) { + new LinkJob(mDestCollection, mItems, this); + } else { + new ItemCopyJob(mItems, mDestCollection, this); + } + break; + case Qt::MoveAction: + new UnlinkJob(sourceCollection, mItems, this); + if (mDestCollection.isVirtual()) { + new LinkJob(mDestCollection, mItems, this); + } else { + new ItemCopyJob(mItems, mDestCollection, this); + } + break; + case Qt::LinkAction: + new LinkJob(mDestCollection, mItems, this); + break; + default: + Q_ASSERT(false); + } + runCollectionsActions(); + commit(); + } else { + runActions(); + } + + commit(); +} + +void PasteHelperJob::runActions() +{ + runItemsActions(); + runCollectionsActions(); +} + +void PasteHelperJob::runItemsActions() +{ + if (mItems.isEmpty()) { + return; + } + + switch (mAction) { + case Qt::CopyAction: + new ItemCopyJob(mItems, mDestCollection, this); + break; + case Qt::MoveAction: + new ItemMoveJob(mItems, mDestCollection, this); + break; + case Qt::LinkAction: + new LinkJob(mDestCollection, mItems, this); + break; + default: + Q_ASSERT(false); // WTF?! + } +} + +void PasteHelperJob::runCollectionsActions() +{ + if (mCollections.isEmpty()) { + return; + } + + switch (mAction) { + case Qt::CopyAction: + for (const Collection &col : std::as_const(mCollections)) { // FIXME: remove once we have a batch job for collections as well + new CollectionCopyJob(col, mDestCollection, this); + } + break; + case Qt::MoveAction: + for (const Collection &col : std::as_const(mCollections)) { // FIXME: remove once we have a batch job for collections as well + new CollectionMoveJob(col, mDestCollection, this); + } + break; + case Qt::LinkAction: + // Not supported for collections + break; + default: + Q_ASSERT(false); // WTF?! + } +} + +bool PasteHelper::canPaste(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action) +{ + if (!mimeData || !collection.isValid()) { + return false; + } + + // check that the target collection has the rights to + // create the pasted items resp. collections + Collection::Rights neededRights = Collection::ReadOnly; + if (mimeData->hasUrls()) { + const QList urls = mimeData->urls(); + for (const QUrl &url : urls) { + const QUrlQuery query(url); + if (query.hasQueryItem(QStringLiteral("item"))) { + if (action == Qt::LinkAction) { + neededRights |= Collection::CanLinkItem; + } else { + neededRights |= Collection::CanCreateItem; + } + } else if (query.hasQueryItem(QStringLiteral("collection"))) { + neededRights |= Collection::CanCreateCollection; + } + } + + if ((collection.rights() & neededRights) == 0) { + return false; + } + + // check that the target collection supports the mime types of the + // items/collections that shall be pasted + bool supportsMimeTypes = true; + for (const QUrl &url : std::as_const(urls)) { + const QUrlQuery query(url); + // collections do not provide mimetype information, so ignore this check + if (query.hasQueryItem(QStringLiteral("collection"))) { + continue; + } + + const QString mimeType = query.queryItemValue(QStringLiteral("type")); + if (!collection.contentMimeTypes().contains(mimeType)) { + supportsMimeTypes = false; + break; + } + } + + return supportsMimeTypes; + } + + return false; +} + +KJob *PasteHelper::paste(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action, Session *session) +{ + if (!canPaste(mimeData, collection, action)) { + return nullptr; + } + + // we try to drop data not coming with the akonadi:// url + // find a type the target collection supports + const QStringList lstFormats = mimeData->formats(); + for (const QString &type : lstFormats) { + if (!collection.contentMimeTypes().contains(type)) { + continue; + } + + QByteArray item = mimeData->data(type); + // HACK for some unknown reason the data is sometimes 0-terminated... + if (!item.isEmpty() && item.at(item.size() - 1) == 0) { + item.resize(item.size() - 1); + } + + Item it; + it.setMimeType(type); + it.setPayloadFromData(item); + + auto job = new ItemCreateJob(it, collection); + return job; + } + + if (!mimeData->hasUrls()) { + return nullptr; + } + + // data contains an url list + return pasteUriList(mimeData, collection, action, session); +} + +KJob *PasteHelper::pasteUriList(const QMimeData *mimeData, const Collection &destination, Qt::DropAction action, Session *session) +{ + if (!mimeData->hasUrls()) { + return nullptr; + } + + if (!canPaste(mimeData, destination, action)) { + return nullptr; + } + + const QList urls = mimeData->urls(); + Collection::List collections; + Item::List items; + for (const QUrl &url : urls) { + const QUrlQuery query(url); + const Collection collection = Collection::fromUrl(url); + if (collection.isValid()) { + collections.append(collection); + } + Item item = Item::fromUrl(url); + if (query.hasQueryItem(QStringLiteral("parent"))) { + item.setParentCollection(Collection(query.queryItemValue(QStringLiteral("parent")).toLongLong())); + } + if (item.isValid()) { + items.append(item); + } + // TODO: handle non Akonadi URLs? + } + + auto job = new PasteHelperJob(action, items, collections, destination, session); + + return job; +} + +#include "pastehelper.moc" diff --git a/src/core/pastehelper_p.h b/src/core/pastehelper_p.h new file mode 100644 index 0000000..3fabb5f --- /dev/null +++ b/src/core/pastehelper_p.h @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" + +class KJob; +class QMimeData; + +namespace Akonadi +{ +class Session; + +/** + @internal + + Helper methods for pasting/droping content into a collection. + + @todo Use in item/collection models as well for dnd +*/ +namespace PasteHelper +{ +/** + Check whether the given mime data can be pasted into the given collection. + @param mimeData The pasted/dropped data. + @param collection The collection to paste/drop into. + @param action Indicate whether this is a copy, a move or link. +*/ +AKONADICORE_EXPORT bool canPaste(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action); + +/** + Paste/drop the given mime data into the given collection. + @param mimeData The pasted/dropped data. + @param collection The target collection. + @param action Indicate whether this is a copy, a move or link. + @returns The job performing the paste, 0 if there is nothing to paste. +*/ +AKONADICORE_EXPORT KJob *paste(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action, Session *session = nullptr); + +/** + URI list paste/drop. + @param mimeData The pasted/dropped data. + @param collection The target collection. + @param action The drop action (copy/move/link). + @returns The job performing the paste, 0 if there is nothing to paste. +*/ +AKONADICORE_EXPORT KJob *pasteUriList(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action, Session *session = nullptr); +} + +} + diff --git a/src/core/pluginloader.cpp b/src/core/pluginloader.cpp new file mode 100644 index 0000000..d4a4b40 --- /dev/null +++ b/src/core/pluginloader.cpp @@ -0,0 +1,174 @@ +/* -*- c++ -*- + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "pluginloader_p.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +PluginMetaData::PluginMetaData() + : loaded(false) +{ +} + +PluginMetaData::PluginMetaData(const QString &lib, const QString &name, const QString &comment, const QString &cname) + : library(lib) + , nameLabel(name) + , descriptionLabel(comment) + , className(cname) + , loaded(false) +{ +} + +PluginLoader *PluginLoader::mSelf = nullptr; + +PluginLoader::PluginLoader() +{ + scan(); +} + +PluginLoader::~PluginLoader() +{ + qDeleteAll(mPluginLoaders); + mPluginLoaders.clear(); +} + +PluginLoader *PluginLoader::self() +{ + if (!mSelf) { + mSelf = new PluginLoader(); + } + + return mSelf; +} + +QStringList PluginLoader::names() const +{ + return mPluginInfos.keys(); +} + +QObject *PluginLoader::createForName(const QString &name) +{ + if (!mPluginInfos.contains(name)) { + qCWarning(AKONADICORE_LOG) << "plugin name \"" << name << "\" is unknown to the plugin loader."; + return nullptr; + } + + PluginMetaData &info = mPluginInfos[name]; + + // First try to load it staticly + const auto instances = QPluginLoader::staticInstances(); + for (auto plugin : instances) { + if (QLatin1String(plugin->metaObject()->className()) == info.className) { + info.loaded = true; + return plugin; + } + } + + if (!info.loaded) { + auto loader = new QPluginLoader(info.library); + if (loader->fileName().isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Error loading" << info.library << ":" << loader->errorString(); + delete loader; + return nullptr; + } + + mPluginLoaders.insert(name, loader); + info.loaded = true; + } + + QPluginLoader *loader = mPluginLoaders.value(name); + Q_ASSERT(loader); + + QObject *object = loader->instance(); + if (!object) { + qCWarning(AKONADICORE_LOG) << "unable to load plugin" << info.library << "for plugin name" << name << "."; + qCWarning(AKONADICORE_LOG) << "Error was:\"" << loader->errorString() << "\"."; + return nullptr; + } + + return object; +} + +PluginMetaData PluginLoader::infoForName(const QString &name) const +{ + return mPluginInfos.value(name, PluginMetaData()); +} + +void PluginLoader::scan() +{ + const auto dirs = StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/plugins/serializer/")); + for (const QString &dir : dirs) { + const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.desktop")); + for (const QString &file : fileNames) { + const QString entry = dir + QLatin1Char('/') + file; + KConfig config(entry, KConfig::SimpleConfig); + if (config.hasGroup("Misc") && config.hasGroup("Plugin")) { + KConfigGroup group(&config, "Plugin"); + + const QString type = group.readEntry("Type").toLower(); + if (type.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Plugin]Type value in" << entry << "- skipping"; + continue; + } + + // read Class entry as a list so that types like QPair are + // properly escaped and don't end up being split into QPair. + const QStringList classes = group.readXdgListEntry("X-Akonadi-Class"); + if (classes.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Plugin]X-Akonadi-Class value in" << entry << "- skipping"; + continue; + } + + const QString library = group.readEntry("X-KDE-Library"); + if (library.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Plugin]X-KDE-Library value in" << entry << "- skipping"; + continue; + } + + KConfigGroup group2(&config, "Misc"); + + QString name = group2.readEntry("Name"); + if (name.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Misc]Name value in \"" << entry << "\" - inserting default name"; + name = i18n("Unnamed plugin"); + } + + QString comment = group2.readEntry("Comment"); + if (comment.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty [Misc]Comment value in \"" << entry << "\" - inserting default name"; + comment = i18n("No description available"); + } + + QString cname = group.readEntry("X-KDE-ClassName"); + if (cname.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "missing or empty X-KDE-ClassName value in \"" << entry << "\""; + } + + const QStringList mimeTypes = type.split(QLatin1Char(','), Qt::SkipEmptyParts); + + qCDebug(AKONADICORE_LOG) << "registering Desktop file" << entry << "for" << mimeTypes << '@' << classes; + for (const QString &mimeType : mimeTypes) { + for (const QString &classType : classes) { + mPluginInfos.insert(mimeType + QLatin1Char('@') + classType, PluginMetaData(library, name, comment, cname)); + } + } + + } else { + qCWarning(AKONADICORE_LOG) << "Desktop file \"" << entry << "\" doesn't seem to describe a plugin " + << "(misses Misc and/or Plugin group)"; + } + } + } +} diff --git a/src/core/pluginloader_p.h b/src/core/pluginloader_p.h new file mode 100644 index 0000000..7a98069 --- /dev/null +++ b/src/core/pluginloader_p.h @@ -0,0 +1,57 @@ +/* -*- c++ -*- + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonaditests_export.h" + +#include +#include +#include + +class QPluginLoader; + +namespace Akonadi +{ +class AKONADI_TESTS_EXPORT PluginMetaData +{ +public: + PluginMetaData(); + PluginMetaData(const QString &lib, const QString &name, const QString &comment, const QString &cname); + + QString library; + QString nameLabel; + QString descriptionLabel; + QString className; + bool loaded; +}; + +class AKONADI_TESTS_EXPORT PluginLoader +{ +public: + ~PluginLoader(); + + static PluginLoader *self(); + + QStringList names() const; + + QObject *createForName(const QString &name); + + PluginMetaData infoForName(const QString &name) const; + + void scan(); + +private: + Q_DISABLE_COPY(PluginLoader) + PluginLoader(); + + static PluginLoader *mSelf; + QHash mPluginLoaders; + QHash mPluginInfos; +}; + +} + diff --git a/src/core/protocolhelper.cpp b/src/core/protocolhelper.cpp new file mode 100644 index 0000000..f24a319 --- /dev/null +++ b/src/core/protocolhelper.cpp @@ -0,0 +1,764 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "attributefactory.h" +#include "collection_p.h" +#include "collectionstatistics.h" +#include "exceptionbase.h" +#include "item_p.h" +#include "itemfetchscope.h" +#include "itemserializer_p.h" +#include "itemserializerplugin.h" +#include "persistentsearchattribute.h" +#include "protocolhelper_p.h" +#include "servermanager.h" +#include "tag_p.h" +#include "tagfetchscope.h" + +#include "private/externalpartstorage_p.h" +#include "private/protocol_p.h" + +#include + +#include +#include + +using namespace Akonadi; +using namespace AkRanges; + +CachePolicy ProtocolHelper::parseCachePolicy(const Protocol::CachePolicy &policy) +{ + CachePolicy cp; + cp.setCacheTimeout(policy.cacheTimeout()); + cp.setIntervalCheckTime(policy.checkInterval()); + cp.setInheritFromParent(policy.inherit()); + cp.setSyncOnDemand(policy.syncOnDemand()); + cp.setLocalParts(policy.localParts()); + return cp; +} + +Protocol::CachePolicy ProtocolHelper::cachePolicyToProtocol(const CachePolicy &policy) +{ + Protocol::CachePolicy proto; + proto.setCacheTimeout(policy.cacheTimeout()); + proto.setCheckInterval(policy.intervalCheckTime()); + proto.setInherit(policy.inheritFromParent()); + proto.setSyncOnDemand(policy.syncOnDemand()); + proto.setLocalParts(policy.localParts()); + return proto; +} + +template inline static void parseAttributesImpl(const Protocol::Attributes &attributes, T *entity) +{ + for (auto iter = attributes.cbegin(), end = attributes.cend(); iter != end; ++iter) { + Attribute *attribute = AttributeFactory::createAttribute(iter.key()); + if (!attribute) { + qCWarning(AKONADICORE_LOG) << "Warning: unknown attribute" << iter.key(); + continue; + } + attribute->deserialize(iter.value()); + entity->addAttribute(attribute); + } +} + +template +inline static void +parseAncestorsCachedImpl(const QVector &ancestors, T *entity, Collection::Id parentCollection, ProtocolHelperValuePool *pool) +{ + if (!pool || parentCollection == -1) { + // if no pool or parent collection id is provided we can't cache anything, so continue as usual + ProtocolHelper::parseAncestors(ancestors, entity); + return; + } + + if (pool->ancestorCollections.contains(parentCollection)) { + // ancestor chain is cached already, so use the cached value + entity->setParentCollection(pool->ancestorCollections.value(parentCollection)); + } else { + // not cached yet, parse the chain + ProtocolHelper::parseAncestors(ancestors, entity); + pool->ancestorCollections.insert(parentCollection, entity->parentCollection()); + } +} + +template inline static Protocol::Attributes attributesToProtocolImpl(const T &entity, bool ns) +{ + Protocol::Attributes attributes; + const auto attrs = entity.attributes(); + for (const auto attr : attrs) { + attributes.insert(ProtocolHelper::encodePartIdentifier(ns ? ProtocolHelper::PartAttribute : ProtocolHelper::PartGlobal, attr->type()), + attr->serialized()); + } + return attributes; +} + +void ProtocolHelper::parseAncestorsCached(const QVector &ancestors, + Item *item, + Collection::Id parentCollection, + ProtocolHelperValuePool *pool) +{ + parseAncestorsCachedImpl(ancestors, item, parentCollection, pool); +} + +void ProtocolHelper::parseAncestorsCached(const QVector &ancestors, + Collection *collection, + Collection::Id parentCollection, + ProtocolHelperValuePool *pool) +{ + parseAncestorsCachedImpl(ancestors, collection, parentCollection, pool); +} + +void ProtocolHelper::parseAncestors(const QVector &ancestors, Item *item) +{ + Collection fakeCollection; + parseAncestors(ancestors, &fakeCollection); + + item->setParentCollection(fakeCollection.parentCollection()); +} + +void ProtocolHelper::parseAncestors(const QVector &ancestors, Collection *collection) +{ + static const Collection::Id rootCollectionId = Collection::root().id(); + + Collection *current = collection; + for (const Protocol::Ancestor &ancestor : ancestors) { + if (ancestor.id() == rootCollectionId) { + current->setParentCollection(Collection::root()); + break; + } + + Akonadi::Collection parentCollection(ancestor.id()); + parentCollection.setName(ancestor.name()); + parentCollection.setRemoteId(ancestor.remoteId()); + parseAttributesImpl(ancestor.attributes(), &parentCollection); + current->setParentCollection(parentCollection); + current = ¤t->parentCollection(); + } +} + +static Collection::ListPreference parsePreference(Tristate value) +{ + switch (value) { + case Tristate::True: + return Collection::ListEnabled; + case Tristate::False: + return Collection::ListDisabled; + case Tristate::Undefined: + return Collection::ListDefault; + } + + Q_ASSERT(false); + return Collection::ListDefault; +} + +CollectionStatistics ProtocolHelper::parseCollectionStatistics(const Protocol::FetchCollectionStatsResponse &stats) +{ + CollectionStatistics cs; + cs.setCount(stats.count()); + cs.setSize(stats.size()); + cs.setUnreadCount(stats.unseen()); + return cs; +} + +void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Item *item) +{ + parseAttributesImpl(attributes, item); +} + +void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Collection *collection) +{ + parseAttributesImpl(attributes, collection); +} + +void ProtocolHelper::parseAttributes(const Protocol::Attributes &attributes, Tag *tag) +{ + parseAttributesImpl(attributes, tag); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const Item &item, bool ns) +{ + return attributesToProtocolImpl(item, ns); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const Collection &collection, bool ns) +{ + return attributesToProtocolImpl(collection, ns); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const Tag &tag, bool ns) +{ + return attributesToProtocolImpl(tag, ns); +} + +Protocol::Attributes ProtocolHelper::attributesToProtocol(const std::vector &modifiedAttributes, bool ns) +{ + Protocol::Attributes attributes; + for (const Attribute *attr : modifiedAttributes) { + attributes.insert(ProtocolHelper::encodePartIdentifier(ns ? ProtocolHelper::PartAttribute : ProtocolHelper::PartGlobal, attr->type()), + attr->serialized()); + } + return attributes; +} + +Collection ProtocolHelper::parseCollection(const Protocol::FetchCollectionsResponse &data, bool requireParent) +{ + Collection collection(data.id()); + + if (requireParent) { + collection.setParentCollection(Collection(data.parentId())); + } + + collection.setName(data.name()); + collection.setRemoteId(data.remoteId()); + collection.setRemoteRevision(data.remoteRevision()); + collection.setResource(data.resource()); + collection.setContentMimeTypes(data.mimeTypes()); + collection.setVirtual(data.isVirtual()); + collection.setStatistics(parseCollectionStatistics(data.statistics())); + collection.setCachePolicy(parseCachePolicy(data.cachePolicy())); + parseAncestors(data.ancestors(), &collection); + collection.setEnabled(data.enabled()); + collection.setLocalListPreference(Collection::ListDisplay, parsePreference(data.displayPref())); + collection.setLocalListPreference(Collection::ListIndex, parsePreference(data.indexPref())); + collection.setLocalListPreference(Collection::ListSync, parsePreference(data.syncPref())); + + if (!data.searchQuery().isEmpty()) { + auto attr = collection.attribute(Collection::AddIfMissing); + attr->setQueryString(data.searchQuery()); + const auto cols = data.searchCollections() | Views::transform([](const auto id) { + return Collection{id}; + }) + | Actions::toQVector; + attr->setQueryCollections(cols); + } + + parseAttributes(data.attributes(), &collection); + + collection.d_ptr->resetChangeLog(); + return collection; +} + +Tag ProtocolHelper::parseTag(const Protocol::FetchTagsResponse &data) +{ + Tag tag(data.id()); + tag.setRemoteId(data.remoteId()); + tag.setGid(data.gid()); + tag.setType(data.type()); + tag.setParent(Tag(data.parentId())); + parseAttributes(data.attributes(), &tag); + tag.d_ptr->resetChangeLog(); + + return tag; +} + +QByteArray ProtocolHelper::encodePartIdentifier(PartNamespace ns, const QByteArray &label) +{ + switch (ns) { + case PartGlobal: + return label; + case PartPayload: + return "PLD:" + label; + case PartAttribute: + return "ATR:" + label; + default: + Q_ASSERT(false); + } + return QByteArray(); +} + +QByteArray ProtocolHelper::decodePartIdentifier(const QByteArray &data, PartNamespace &ns) +{ + if (data.startsWith("PLD:")) { // krazy:exclude=strings + ns = PartPayload; + return data.mid(4); + } else if (data.startsWith("ATR:")) { // krazy:exclude=strings + ns = PartAttribute; + return data.mid(4); + } else { + ns = PartGlobal; + return data; + } +} + +Protocol::ScopeContext +ProtocolHelper::commandContextToProtocol(const Akonadi::Collection &collection, const Akonadi::Tag &tag, const Item::List &requestedItems) +{ + Protocol::ScopeContext ctx; + if (tag.isValid()) { + ctx.setContext(Protocol::ScopeContext::Tag, tag.id()); + } + + if (collection == Collection::root()) { + if (requestedItems.isEmpty() && !tag.isValid()) { // collection content listing + throw Exception("Cannot perform item operations on root collection."); + } + } else { + if (collection.isValid()) { + ctx.setContext(Protocol::ScopeContext::Collection, collection.id()); + } else if (!collection.remoteId().isEmpty()) { + ctx.setContext(Protocol::ScopeContext::Collection, collection.remoteId()); + } + } + + return ctx; +} + +Scope ProtocolHelper::hierarchicalRidToScope(const Collection &col) +{ + if (col == Collection::root()) { + return Scope({Scope::HRID(0)}); + } + if (col.remoteId().isEmpty()) { + return Scope(); + } + + QVector chain; + Collection c = col; + while (!c.remoteId().isEmpty()) { + chain.append(Scope::HRID(c.id(), c.remoteId())); + c = c.parentCollection(); + } + return Scope(chain + QVector{Scope::HRID(0)}); +} + +Scope ProtocolHelper::hierarchicalRidToScope(const Item &item) +{ + return Scope(QVector({Scope::HRID(item.id(), item.remoteId())}) + hierarchicalRidToScope(item.parentCollection()).hridChain()); +} + +Protocol::ItemFetchScope ProtocolHelper::itemFetchScopeToProtocol(const ItemFetchScope &fetchScope) +{ + Protocol::ItemFetchScope fs; + QVector parts; + parts.reserve(fetchScope.payloadParts().size() + fetchScope.attributes().size()); + parts += fetchScope.payloadParts() | Views::transform(std::bind(encodePartIdentifier, PartPayload, std::placeholders::_1)) | Actions::toQVector; + parts += fetchScope.attributes() | Views::transform(std::bind(encodePartIdentifier, PartAttribute, std::placeholders::_1)) | Actions::toQVector; + fs.setRequestedParts(parts); + + // The default scope + fs.setFetch(Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::RemoteID | Protocol::ItemFetchScope::RemoteRevision + | Protocol::ItemFetchScope::MTime); + + fs.setFetch(Protocol::ItemFetchScope::FullPayload, fetchScope.fullPayload()); + fs.setFetch(Protocol::ItemFetchScope::AllAttributes, fetchScope.allAttributes()); + fs.setFetch(Protocol::ItemFetchScope::CacheOnly, fetchScope.cacheOnly()); + fs.setFetch(Protocol::ItemFetchScope::CheckCachedPayloadPartsOnly, fetchScope.checkForCachedPayloadPartsOnly()); + fs.setFetch(Protocol::ItemFetchScope::IgnoreErrors, fetchScope.ignoreRetrievalErrors()); + switch (fetchScope.ancestorRetrieval()) { + case ItemFetchScope::Parent: + fs.setAncestorDepth(Protocol::ItemFetchScope::ParentAncestor); + break; + case ItemFetchScope::All: + fs.setAncestorDepth(Protocol::ItemFetchScope::AllAncestors); + break; + case ItemFetchScope::None: + fs.setAncestorDepth(Protocol::ItemFetchScope::NoAncestor); + break; + default: + Q_ASSERT(false); + break; + } + + if (fetchScope.fetchChangedSince().isValid()) { + fs.setChangedSince(fetchScope.fetchChangedSince()); + } + + fs.setFetch(Protocol::ItemFetchScope::RemoteID, fetchScope.fetchRemoteIdentification()); + fs.setFetch(Protocol::ItemFetchScope::RemoteRevision, fetchScope.fetchRemoteIdentification()); + fs.setFetch(Protocol::ItemFetchScope::GID, fetchScope.fetchGid()); + fs.setFetch(Protocol::ItemFetchScope::Tags, fetchScope.fetchTags()); + fs.setFetch(Protocol::ItemFetchScope::VirtReferences, fetchScope.fetchVirtualReferences()); + fs.setFetch(Protocol::ItemFetchScope::MTime, fetchScope.fetchModificationTime()); + fs.setFetch(Protocol::ItemFetchScope::Relations, fetchScope.fetchRelations()); + + return fs; +} + +ItemFetchScope ProtocolHelper::parseItemFetchScope(const Protocol::ItemFetchScope &fetchScope) +{ + ItemFetchScope ifs; + const auto parts = fetchScope.requestedParts(); + for (const auto &part : parts) { + if (part.startsWith("PLD:")) { + ifs.fetchPayloadPart(part.mid(4), true); + } else if (part.startsWith("ATR:")) { + ifs.fetchAttribute(part.mid(4), true); + } + } + + if (fetchScope.fetch(Protocol::ItemFetchScope::FullPayload)) { + ifs.fetchFullPayload(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::AllAttributes)) { + ifs.fetchAllAttributes(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::CacheOnly)) { + ifs.setCacheOnly(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::CheckCachedPayloadPartsOnly)) { + ifs.setCheckForCachedPayloadPartsOnly(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::IgnoreErrors)) { + ifs.setIgnoreRetrievalErrors(true); + } + switch (fetchScope.ancestorDepth()) { + case Protocol::ItemFetchScope::ParentAncestor: + ifs.setAncestorRetrieval(ItemFetchScope::Parent); + break; + case Protocol::ItemFetchScope::AllAncestors: + ifs.setAncestorRetrieval(ItemFetchScope::All); + break; + default: + ifs.setAncestorRetrieval(ItemFetchScope::None); + break; + } + if (fetchScope.changedSince().isValid()) { + ifs.setFetchChangedSince(fetchScope.changedSince()); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::RemoteID) || fetchScope.fetch(Protocol::ItemFetchScope::RemoteRevision)) { + ifs.setFetchRemoteIdentification(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::GID)) { + ifs.setFetchGid(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::Tags)) { + ifs.setFetchTags(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::VirtReferences)) { + ifs.setFetchVirtualReferences(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::MTime)) { + ifs.setFetchModificationTime(true); + } + if (fetchScope.fetch(Protocol::ItemFetchScope::Relations)) { + ifs.setFetchRelations(true); + } + + return ifs; +} + +Protocol::CollectionFetchScope ProtocolHelper::collectionFetchScopeToProtocol(const CollectionFetchScope &fetchScope) +{ + Protocol::CollectionFetchScope cfs; + switch (fetchScope.listFilter()) { + case CollectionFetchScope::NoFilter: + cfs.setListFilter(Protocol::CollectionFetchScope::NoFilter); + break; + case CollectionFetchScope::Display: + cfs.setListFilter(Protocol::CollectionFetchScope::Display); + break; + case CollectionFetchScope::Sync: + cfs.setListFilter(Protocol::CollectionFetchScope::Sync); + break; + case CollectionFetchScope::Index: + cfs.setListFilter(Protocol::CollectionFetchScope::Index); + break; + case CollectionFetchScope::Enabled: + cfs.setListFilter(Protocol::CollectionFetchScope::Enabled); + break; + } + cfs.setIncludeStatistics(fetchScope.includeStatistics()); + cfs.setResource(fetchScope.resource()); + cfs.setContentMimeTypes(fetchScope.contentMimeTypes()); + cfs.setAttributes(fetchScope.attributes()); + cfs.setFetchIdOnly(fetchScope.fetchIdOnly()); + switch (fetchScope.ancestorRetrieval()) { + case CollectionFetchScope::None: + cfs.setAncestorRetrieval(Protocol::CollectionFetchScope::None); + break; + case CollectionFetchScope::Parent: + cfs.setAncestorRetrieval(Protocol::CollectionFetchScope::Parent); + break; + case CollectionFetchScope::All: + cfs.setAncestorRetrieval(Protocol::CollectionFetchScope::All); + break; + } + if (cfs.ancestorRetrieval() != Protocol::CollectionFetchScope::None) { + cfs.setAncestorAttributes(fetchScope.ancestorFetchScope().attributes()); + cfs.setAncestorFetchIdOnly(fetchScope.ancestorFetchScope().fetchIdOnly()); + } + cfs.setIgnoreRetrievalErrors(fetchScope.ignoreRetrievalErrors()); + + return cfs; +} + +CollectionFetchScope ProtocolHelper::parseCollectionFetchScope(const Protocol::CollectionFetchScope &fetchScope) +{ + CollectionFetchScope cfs; + switch (fetchScope.listFilter()) { + case Protocol::CollectionFetchScope::NoFilter: + cfs.setListFilter(CollectionFetchScope::NoFilter); + break; + case Protocol::CollectionFetchScope::Display: + cfs.setListFilter(CollectionFetchScope::Display); + break; + case Protocol::CollectionFetchScope::Sync: + cfs.setListFilter(CollectionFetchScope::Sync); + break; + case Protocol::CollectionFetchScope::Index: + cfs.setListFilter(CollectionFetchScope::Index); + break; + case Protocol::CollectionFetchScope::Enabled: + cfs.setListFilter(CollectionFetchScope::Enabled); + break; + } + cfs.setIncludeStatistics(fetchScope.includeStatistics()); + cfs.setResource(fetchScope.resource()); + cfs.setContentMimeTypes(fetchScope.contentMimeTypes()); + switch (fetchScope.ancestorRetrieval()) { + case Protocol::CollectionFetchScope::None: + cfs.setAncestorRetrieval(CollectionFetchScope::None); + break; + case Protocol::CollectionFetchScope::Parent: + cfs.setAncestorRetrieval(CollectionFetchScope::Parent); + break; + case Protocol::CollectionFetchScope::All: + cfs.setAncestorRetrieval(CollectionFetchScope::All); + break; + } + if (cfs.ancestorRetrieval() != CollectionFetchScope::None) { + cfs.ancestorFetchScope().setFetchIdOnly(fetchScope.ancestorFetchIdOnly()); + const auto attrs = fetchScope.ancestorAttributes(); + for (const auto &attr : attrs) { + cfs.ancestorFetchScope().fetchAttribute(attr, true); + } + } + const auto attrs = fetchScope.attributes(); + for (const auto &attr : attrs) { + cfs.fetchAttribute(attr, true); + } + cfs.setFetchIdOnly(fetchScope.fetchIdOnly()); + cfs.setIgnoreRetrievalErrors(fetchScope.ignoreRetrievalErrors()); + + return cfs; +} + +Protocol::TagFetchScope ProtocolHelper::tagFetchScopeToProtocol(const TagFetchScope &fetchScope) +{ + Protocol::TagFetchScope tfs; + tfs.setFetchIdOnly(fetchScope.fetchIdOnly()); + tfs.setAttributes(fetchScope.attributes()); + tfs.setFetchRemoteID(fetchScope.fetchRemoteId()); + tfs.setFetchAllAttributes(fetchScope.fetchAllAttributes()); + return tfs; +} + +TagFetchScope ProtocolHelper::parseTagFetchScope(const Protocol::TagFetchScope &fetchScope) +{ + TagFetchScope tfs; + tfs.setFetchIdOnly(fetchScope.fetchIdOnly()); + tfs.setFetchRemoteId(fetchScope.fetchRemoteID()); + tfs.setFetchAllAttributes(fetchScope.fetchAllAttributes()); + const auto attrs = fetchScope.attributes(); + for (const auto &attr : attrs) { + tfs.fetchAttribute(attr, true); + } + return tfs; +} + +static Item::Flags convertFlags(const QVector &flags, ProtocolHelperValuePool *valuePool) +{ +#if __cplusplus >= 201103L || defined(__GNUC__) || defined(__clang__) + // When the compiler supports thread-safe static initialization (mandated by the C++11 memory model) + // then use it to share the common case of a single-item set only containing the \SEEN flag. + // NOTE: GCC and clang has threadsafe static initialization for some time now, even without C++11. + if (flags.size() == 1 && flags.first() == "\\SEEN") { + static const Item::Flags sharedSeen = Item::Flags() << QByteArray("\\SEEN"); + return sharedSeen; + } +#endif + + Item::Flags convertedFlags; + convertedFlags.reserve(flags.size()); + for (const QByteArray &flag : flags) { + if (valuePool) { + convertedFlags.insert(valuePool->flagPool.sharedValue(flag)); + } else { + convertedFlags.insert(flag); + } + } + return convertedFlags; +} + +Item ProtocolHelper::parseItemFetchResult(const Protocol::FetchItemsResponse &data, + const Akonadi::ItemFetchScope *fetchScope, + ProtocolHelperValuePool *valuePool) +{ + Item item; + item.setId(data.id()); + item.setRevision(data.revision()); + if (!fetchScope || fetchScope->fetchRemoteIdentification()) { + item.setRemoteId(data.remoteId()); + item.setRemoteRevision(data.remoteRevision()); + } + item.setGid(data.gid()); + item.setStorageCollectionId(data.parentId()); + + if (valuePool) { + item.setMimeType(valuePool->mimeTypePool.sharedValue(data.mimeType())); + } else { + item.setMimeType(data.mimeType()); + } + + if (!item.isValid()) { + return Item(); + } + + item.setFlags(convertFlags(data.flags(), valuePool)); + + if ((!fetchScope || fetchScope->fetchTags()) && !data.tags().isEmpty()) { + Tag::List tags; + tags.reserve(data.tags().size()); + Q_FOREACH (const Protocol::FetchTagsResponse &tag, data.tags()) { + tags.append(parseTagFetchResult(tag)); + } + item.setTags(tags); + } + + if ((!fetchScope || fetchScope->fetchRelations()) && !data.relations().isEmpty()) { + Relation::List relations; + relations.reserve(data.relations().size()); + Q_FOREACH (const Protocol::FetchRelationsResponse &rel, data.relations()) { + relations.append(parseRelationFetchResult(rel)); + } + item.d_ptr->mRelations = relations; + } + + if ((!fetchScope || fetchScope->fetchVirtualReferences()) && !data.virtualReferences().isEmpty()) { + Collection::List virtRefs; + virtRefs.reserve(data.virtualReferences().size()); + Q_FOREACH (qint64 colId, data.virtualReferences()) { + virtRefs.append(Collection(colId)); + } + item.setVirtualReferences(virtRefs); + } + + if (!data.cachedParts().isEmpty()) { + QSet cp; + cp.reserve(data.cachedParts().size()); + Q_FOREACH (const QByteArray &ba, data.cachedParts()) { + cp.insert(ba); + } + item.setCachedPayloadParts(cp); + } + + item.setSize(data.size()); + item.setModificationTime(data.mTime()); + parseAncestorsCached(data.ancestors(), &item, data.parentId(), valuePool); + Q_FOREACH (const Protocol::StreamPayloadResponse &part, data.parts()) { + ProtocolHelper::PartNamespace ns; + const QByteArray plainKey = decodePartIdentifier(part.payloadName(), ns); + const auto metaData = part.metaData(); + switch (ns) { + case ProtocolHelper::PartPayload: + if (fetchScope && !fetchScope->fullPayload() && !fetchScope->payloadParts().contains(plainKey)) { + continue; + } + ItemSerializer::deserialize(item, plainKey, part.data(), metaData.version(), static_cast(metaData.storageType())); + if (metaData.storageType() == Protocol::PartMetaData::Foreign) { + item.d_ptr->mPayloadPath = QString::fromUtf8(part.data()); + } + break; + case ProtocolHelper::PartAttribute: { + if (fetchScope && !fetchScope->allAttributes() && !fetchScope->attributes().contains(plainKey)) { + continue; + } + Attribute *attr = AttributeFactory::createAttribute(plainKey); + Q_ASSERT(attr); + if (metaData.storageType() == Protocol::PartMetaData::External) { + const QString filename = ExternalPartStorage::resolveAbsolutePath(part.data()); + QFile file(filename); + if (file.open(QFile::ReadOnly)) { + attr->deserialize(file.readAll()); + } else { + qCWarning(AKONADICORE_LOG) << "Failed to open attribute file: " << filename; + delete attr; + attr = nullptr; + } + } else { + attr->deserialize(part.data()); + } + if (attr) { + item.addAttribute(attr); + } + break; + } + case ProtocolHelper::PartGlobal: + default: + qCWarning(AKONADICORE_LOG) << "Unknown item part type:" << part.payloadName(); + } + } + + item.d_ptr->resetChangeLog(); + return item; +} + +Tag ProtocolHelper::parseTagFetchResult(const Protocol::FetchTagsResponse &data) +{ + Tag tag; + tag.setId(data.id()); + tag.setGid(data.gid()); + tag.setRemoteId(data.remoteId()); + tag.setType(data.type()); + tag.setParent(data.parentId() > 0 ? Tag(data.parentId()) : Tag()); + + parseAttributes(data.attributes(), &tag); + tag.d_ptr->resetChangeLog(); + return tag; +} + +Relation ProtocolHelper::parseRelationFetchResult(const Protocol::FetchRelationsResponse &data) +{ + Relation relation; + relation.setLeft(Item(data.left())); + relation.setRight(Item(data.right())); + relation.setRemoteId(data.remoteId()); + relation.setType(data.type()); + return relation; +} + +bool ProtocolHelper::streamPayloadToFile(const QString &fileName, const QByteArray &data, QByteArray &error) +{ + const QString filePath = ExternalPartStorage::resolveAbsolutePath(fileName); + // qCDebug(AKONADICORE_LOG) << filePath << fileName; + if (!filePath.startsWith(ExternalPartStorage::akonadiStoragePath())) { + qCWarning(AKONADICORE_LOG) << "Invalid file path" << fileName; + error = "Invalid file path"; + return false; + } + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCWarning(AKONADICORE_LOG) << "Failed to open destination payload file" << file.errorString(); + error = "Failed to store payload into file"; + return false; + } + if (file.write(data) != data.size()) { + qCWarning(AKONADICORE_LOG) << "Failed to write all payload data to file"; + error = "Failed to store payload into file"; + return false; + } + // qCDebug(AKONADICORE_LOG) << "Wrote" << data.size() << "bytes to " << file.fileName(); + + // Make sure stuff is written to disk + file.close(); + return true; +} + +Akonadi::Tristate ProtocolHelper::listPreference(Collection::ListPreference pref) +{ + switch (pref) { + case Collection::ListEnabled: + return Tristate::True; + case Collection::ListDisabled: + return Tristate::False; + case Collection::ListDefault: + return Tristate::Undefined; + } + + Q_ASSERT(false); + return Tristate::Undefined; +} diff --git a/src/core/protocolhelper_p.h b/src/core/protocolhelper_p.h new file mode 100644 index 0000000..51b4af2 --- /dev/null +++ b/src/core/protocolhelper_p.h @@ -0,0 +1,315 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "cachepolicy.h" +#include "collection.h" +#include "collectionfetchscope.h" +#include "collectionutils.h" +#include "item.h" +#include "itemfetchscope.h" +#include "sharedvaluepool_p.h" +#include "tag.h" + +#include "private/imapparser_p.h" +#include "private/protocol_p.h" +#include "private/scope_p.h" +#include "private/tristate_p.h" + +#include + +#include +#include +#include +#include +#include + +namespace Akonadi +{ +struct ProtocolHelperValuePool { + using FlagPool = Internal::SharedValuePool; + using MimeTypePool = Internal::SharedValuePool; + + FlagPool flagPool; + MimeTypePool mimeTypePool; + QHash ancestorCollections; +}; + +/** + @internal + Helper methods for converting between libakonadi objects and their protocol + representation. + + @todo Add unit tests for this. + @todo Use exceptions for a useful error handling +*/ +class ProtocolHelper +{ +public: + /** Part namespaces. */ + enum PartNamespace { + PartGlobal, + PartPayload, + PartAttribute, + }; + + /** + Parse a cache policy definition. + @param policy The parsed cache policy. + @returns Akonadi::CachePolicy + */ + static CachePolicy parseCachePolicy(const Protocol::CachePolicy &policy); + + /** + Convert a cache policy object into its protocol representation. + */ + static Protocol::CachePolicy cachePolicyToProtocol(const CachePolicy &policy); + + /** + Convert a ancestor chain from its protocol representation into an Item object. + */ + static void parseAncestors(const QVector &ancestors, Item *item); + + /** + Convert a ancestor chain from its protocol representation into a Collection object. + */ + static void parseAncestors(const QVector &ancestors, Collection *collection); + + /** + Convert a ancestor chain from its protocol representation into an Item object. + + This method allows to pass a @p valuePool which acts as cache, so ancestor paths for the + same @p parentCollection don't have to be parsed twice. + */ + static void parseAncestorsCached(const QVector &ancestors, + Item *item, + Collection::Id parentCollection, + ProtocolHelperValuePool *valuePool = nullptr); + + /** + Convert a ancestor chain from its protocol representation into an Collection object. + + This method allows to pass a @p valuePool which acts as cache, so ancestor paths for the + same @p parentCollection don't have to be parsed twice. + */ + static void parseAncestorsCached(const QVector &ancestors, + Collection *collection, + Collection::Id parentCollection, + ProtocolHelperValuePool *valuePool = nullptr); + /** + Parse a collection description. + @param data The input data. + @param requireParent Whether or not we require a parent as part of the data. + @returns The parsed collection + */ + static Collection parseCollection(const Protocol::FetchCollectionsResponse &data, bool requireParent = true); + + static Tag parseTag(const Protocol::FetchTagsResponse &data); + + static void parseAttributes(const Protocol::Attributes &attributes, Item *item); + static void parseAttributes(const Protocol::Attributes &attributes, Collection *collection); + static void parseAttributes(const Protocol::Attributes &attributes, Tag *entity); + + static CollectionStatistics parseCollectionStatistics(const Protocol::FetchCollectionStatsResponse &stats); + + /** + Convert attributes to their protocol representation. + */ + static Protocol::Attributes attributesToProtocol(const Item &item, bool ns = false); + static Protocol::Attributes attributesToProtocol(const Collection &collection, bool ns = false); + static Protocol::Attributes attributesToProtocol(const Tag &entity, bool ns = false); + static Protocol::Attributes attributesToProtocol(const std::vector &modifiedAttributes, bool ns = false); + + /** + Encodes part label and namespace. + */ + static QByteArray encodePartIdentifier(PartNamespace ns, const QByteArray &label); + + /** + Decode part label and namespace. + */ + static QByteArray decodePartIdentifier(const QByteArray &data, PartNamespace &ns); + + /** + Converts the given set of items into a protocol representation. + @throws A Akonadi::Exception if the item set contains items with missing/invalid identifiers. + */ + template class Container> static Scope entitySetToScope(const Container &_objects) + { + if (_objects.isEmpty()) { + throw Exception("No objects specified"); + } + + Container objects(_objects); + using namespace std::placeholders; + std::sort(objects.begin(), objects.end(), [](const T &a, const T &b) -> bool { + return a.id() < b.id(); + }); + if (objects.at(0).isValid()) { + QVector uids; + uids.reserve(objects.size()); + for (const T &object : objects) { + uids << object.id(); + } + ImapSet set; + set.add(uids); + return Scope(set); + } + + if (entitySetHasGID(_objects)) { + return entitySetToGID(_objects); + } + + if (!entitySetHasRemoteIdentifier(_objects, std::mem_fn(&T::remoteId))) { + throw Exception("No remote identifier specified"); + } + + // check if we have RIDs or HRIDs + if (entitySetHasHRID(_objects)) { + return hierarchicalRidToScope(objects.first()); + } + + return entitySetToRemoteIdentifier(Scope::Rid, _objects, std::mem_fn(&T::remoteId)); + } + + static Protocol::ScopeContext commandContextToProtocol(const Akonadi::Collection &collection, const Akonadi::Tag &tag, const Item::List &requestedItems); + + /** + Converts the given object identifier into a protocol representation. + @throws A Akonadi::Exception if the item set contains items with missing/invalid identifiers. + */ + template static Scope entityToScope(const T &object) + { + return entitySetToScope(QVector() << object); + } + + /** + Converts the given collection's hierarchical RID into a protocol representation. + Assumes @p col has a valid hierarchical RID, so check that before! + */ + static Scope hierarchicalRidToScope(const Collection &col); + + /** + Converts the HRID of the given item into an ASAP protocol representation. + Assumes @p item has a valid HRID. + */ + static Scope hierarchicalRidToScope(const Item &item); + + static Scope hierarchicalRidToScope(const Tag & /*tag*/) + { + assert(false); + return Scope(); + } + + /** + Converts a given ItemFetchScope object into a protocol representation. + */ + static Protocol::ItemFetchScope itemFetchScopeToProtocol(const ItemFetchScope &fetchScope); + static ItemFetchScope parseItemFetchScope(const Protocol::ItemFetchScope &fetchScope); + + static Protocol::CollectionFetchScope collectionFetchScopeToProtocol(const CollectionFetchScope &fetchScope); + static CollectionFetchScope parseCollectionFetchScope(const Protocol::CollectionFetchScope &fetchScope); + + static Protocol::TagFetchScope tagFetchScopeToProtocol(const TagFetchScope &fetchScope); + static TagFetchScope parseTagFetchScope(const Protocol::TagFetchScope &fetchScope); + + /** + * Parses a single line from an item fetch job result into an Item object. + * FIXME: std::optional + */ + static Item + parseItemFetchResult(const Protocol::FetchItemsResponse &data, const ItemFetchScope *fetchScope = nullptr, ProtocolHelperValuePool *valuePool = nullptr); + static Tag parseTagFetchResult(const Protocol::FetchTagsResponse &data); + static Relation parseRelationFetchResult(const Protocol::FetchRelationsResponse &data); + + static bool streamPayloadToFile(const QString &file, const QByteArray &data, QByteArray &error); + + static Akonadi::Tristate listPreference(const Collection::ListPreference pref); + +private: + template class Container> + inline static typename std::enable_if::value, bool>::type entitySetHasGID(const Container &objects) + { + return entitySetHasRemoteIdentifier(objects, std::mem_fn(&T::gid)); + } + + template class Container> + inline static typename std::enable_if::value, bool>::type entitySetHasGID(const Container & /*objects*/, + int * /*dummy*/ = nullptr) + { + return false; + } + + template class Container> + inline static typename std::enable_if::value, Scope>::type entitySetToGID(const Container &objects) + { + return entitySetToRemoteIdentifier(Scope::Gid, objects, std::mem_fn(&T::gid)); + } + + template class Container> + inline static typename std::enable_if::value, Scope>::type entitySetToGID(const Container & /*objects*/, + int * /*dummy*/ = nullptr) + { + return Scope(); + } + + template class Container, typename RIDFunc> + inline static bool entitySetHasRemoteIdentifier(const Container &objects, const RIDFunc &ridFunc) + { + return std::find_if(objects.constBegin(), + objects.constEnd(), + [=](const T &obj) { + return ridFunc(obj).isEmpty(); + }) + == objects.constEnd(); + } + + template class Container, typename RIDFunc> + inline static typename std::enable_if::value, Scope>::type + entitySetToRemoteIdentifier(Scope::SelectionScope scope, const Container &objects, const RIDFunc &ridFunc) + { + QStringList rids; + rids.reserve(objects.size()); + std::transform(objects.cbegin(), objects.cend(), std::back_inserter(rids), [=](const T &obj) -> QString { + return ridFunc(obj); + }); + return Scope(scope, rids); + } + + template class Container, typename RIDFunc> + inline static typename std::enable_if::value, Scope>::type + entitySetToRemoteIdentifier(Scope::SelectionScope scope, const Container &objects, const RIDFunc &ridFunc, int * /*dummy*/ = nullptr) + { + QStringList rids; + rids.reserve(objects.size()); + std::transform(objects.cbegin(), objects.cend(), std::back_inserter(rids), [=](const T &obj) -> QString { + return QString::fromLatin1(ridFunc(obj)); + }); + return Scope(scope, rids); + } + + template class Container> + inline static typename std::enable_if::value, bool>::type entitySetHasHRID(const Container &objects) + { + return objects.size() == 1 + && std::find_if(objects.constBegin(), + objects.constEnd(), + [](const T &obj) -> bool { + return !CollectionUtils::hasValidHierarchicalRID(obj); + }) + == objects.constEnd(); // ### HRID sets are not yet specified + } + + template class Container> + inline static typename std::enable_if::value, bool>::type entitySetHasHRID(const Container & /*objects*/, int * /*dummy*/ = nullptr) + { + return false; + } +}; + +} + diff --git a/src/core/qtest_akonadi.h b/src/core/qtest_akonadi.h new file mode 100644 index 0000000..0b817cf --- /dev/null +++ b/src/core/qtest_akonadi.h @@ -0,0 +1,193 @@ +/* This file is based on qtest_kde.h from kdelibs + SPDX-FileCopyrightText: 2006 David Faure + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectionfetchscope.h" +#include "collectionpathresolver.h" +#include "itemfetchscope.h" +#include "monitor.h" +#include "servermanager.h" + +#include +#include +#include +#include +#include +#include + +/** + * \short Akonadi Replacement for QTEST_MAIN from QTestLib + * + * This macro should be used for classes that run inside the Akonadi Testrunner. + * So instead of writing QTEST_MAIN( TestClass ) you write + * QTEST_AKONADIMAIN( TestClass ). + * + * Unlike QTEST_MAIN, this macro actually does call QApplication::exec() so + * that the application is running during test execution. This is needed + * for proper clean up of Sessions. + * + * \param TestObject The class you use for testing. + * + * \see QTestLib + * \see QTEST_KDEMAIN + */ +#define QTEST_AKONADIMAIN(TestObject) \ + int main(int argc, char *argv[]) \ + { \ + qputenv("LC_ALL", "C"); \ + qunsetenv("KDE_COLOR_DEBUG"); \ + QApplication app(argc, argv); \ + app.setApplicationName(QStringLiteral("qttest")); \ + app.setOrganizationDomain(QStringLiteral("kde.org")); \ + app.setOrganizationName(QStringLiteral("KDE")); \ + QGuiApplication::setQuitOnLastWindowClosed(false); \ + qRegisterMetaType>(); \ + int result = 0; \ + QTimer::singleShot(0, &app, [argc, argv, &result]() { \ + TestObject tc; \ + result = QTest::qExec(&tc, argc, argv); \ + qApp->quit(); \ + }); \ + app.exec(); \ + return result; \ + } + +namespace AkonadiTest +{ +/** + * Checks that the test is running in the proper test environment + */ +void checkTestIsIsolated() +{ + if (qEnvironmentVariableIsEmpty("TESTRUNNER_DB_ENVIRONMENT")) + qFatal("This test must be run using ctest, in order to use the testrunner environment. Aborting, to avoid messing up your real akonadi"); + if (!qgetenv("XDG_DATA_HOME").contains("testrunner")) + qFatal("Did you forget to run the test using QTEST_AKONADIMAIN?"); +} + +/** + * Switch all resources offline to reduce interference from them + */ +void setAllResourcesOffline() +{ + // switch all resources offline to reduce interference from them + const auto lst = Akonadi::AgentManager::self()->instances(); + for (Akonadi::AgentInstance agent : lst) { + agent.setIsOnline(false); + } +} + +template bool akWaitForSignal(Object sender, Func member, int timeout = 1000) +{ + QSignalSpy spy(sender, member); + bool ok = false; + [&]() { + QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, timeout); + ok = true; + }(); + return ok; +} + +bool akWaitForSignal(const QObject *sender, const char *member, int timeout = 1000) +{ + QSignalSpy spy(sender, member); + bool ok = false; + [&]() { + QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, timeout); + ok = true; + }(); + return ok; +} + +qint64 collectionIdFromPath(const QString &path) +{ + auto resolver = new Akonadi::CollectionPathResolver(path); + bool success = resolver->exec(); + if (!success) { + qDebug() << "path resolution for " << path << " failed: " << resolver->errorText(); + return -1; + } + qint64 id = resolver->collection(); + return id; +} + +QString testrunnerServiceName() +{ + const QString pid = QString::fromLocal8Bit(qgetenv("AKONADI_TESTRUNNER_PID")); + Q_ASSERT(!pid.isEmpty()); + return QStringLiteral("org.kde.Akonadi.Testrunner-") + pid; +} + +bool restartAkonadiServer() +{ + QDBusInterface testrunnerIface(testrunnerServiceName(), QStringLiteral("/"), QStringLiteral("org.kde.Akonadi.Testrunner"), QDBusConnection::sessionBus()); + if (!testrunnerIface.isValid()) { + qWarning() << "Unable to get a dbus interface to the testrunner!"; + } + + QDBusReply reply = testrunnerIface.call(QStringLiteral("restartAkonadiServer")); + if (!reply.isValid()) { + qWarning() << reply.error(); + return false; + } else if (Akonadi::ServerManager::isRunning()) { + return true; + } else { + bool ok = false; + [&]() { + QSignalSpy spy(Akonadi::ServerManager::self(), &Akonadi::ServerManager::started); + QTRY_VERIFY_WITH_TIMEOUT(spy.count() > 0, 10000); + ok = true; + }(); + return ok; + } +} + +bool trackAkonadiProcess(bool track) +{ + QDBusInterface testrunnerIface(testrunnerServiceName(), QStringLiteral("/"), QStringLiteral("org.kde.Akonadi.Testrunner"), QDBusConnection::sessionBus()); + if (!testrunnerIface.isValid()) { + qWarning() << "Unable to get a dbus interface to the testrunner!"; + } + + QDBusReply reply = testrunnerIface.call(QStringLiteral("trackAkonadiProcess"), track); + if (!reply.isValid()) { + qWarning() << reply.error(); + return false; + } else { + return true; + } +} + +std::unique_ptr getTestMonitor() +{ + auto m = new Akonadi::Monitor(); + m->fetchCollection(true); + m->setCollectionMonitored(Akonadi::Collection::root(), true); + m->setAllMonitored(true); + auto &itemFS = m->itemFetchScope(); + itemFS.setAncestorRetrieval(Akonadi::ItemFetchScope::All); + auto &colFS = m->collectionFetchScope(); + colFS.setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + + QSignalSpy readySpy(m, &Akonadi::Monitor::monitorReady); + readySpy.wait(); + + return std::unique_ptr(m); +} + +} // namespace + +/** + * Runs an Akonadi::Job synchronously and aborts if the job failed. + * Similar to QVERIFY( job->exec() ) but includes the job error message + * in the output in case of a failure. + */ +#define AKVERIFYEXEC(job) QVERIFY2(job->exec(), job->errorString().toUtf8().constData()) + diff --git a/src/core/relation.cpp b/src/core/relation.cpp new file mode 100644 index 0000000..4c70a98 --- /dev/null +++ b/src/core/relation.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "relation.h" + +#include "item.h" + +using namespace Akonadi; + +const char *Akonadi::Relation::GENERIC = "GENERIC"; + +struct Q_DECL_HIDDEN Relation::Private : public QSharedData { + Item left; + Item right; + QByteArray type; + QByteArray remoteId; +}; + +Relation::Relation() + : d(new Private) +{ +} + +Relation::Relation(const QByteArray &type, const Item &left, const Item &right) + : d(new Private) +{ + d->left = left; + d->right = right; + d->type = type; +} + +Relation::Relation(const Relation &other) = default; + +Relation::Relation(Relation &&) noexcept = default; + +Relation::~Relation() = default; + +Relation &Relation::operator=(const Relation &) = default; + +Relation &Relation::operator=(Relation &&) noexcept = default; + +bool Relation::operator==(const Relation &other) const +{ + if (isValid() && other.isValid()) { + return d->left == other.d->left && d->right == other.d->right && d->type == other.d->type && d->remoteId == other.d->remoteId; + } + return false; +} + +bool Relation::operator!=(const Relation &other) const +{ + return !operator==(other); +} + +void Relation::setLeft(const Item &left) +{ + d->left = left; +} + +Item Relation::left() const +{ + return d->left; +} + +void Relation::setRight(const Item &right) +{ + d->right = right; +} + +Item Relation::right() const +{ + return d->right; +} + +void Relation::setType(const QByteArray &type) +{ + d->type = type; +} + +QByteArray Relation::type() const +{ + return d->type; +} + +void Relation::setRemoteId(const QByteArray &remoteId) +{ + d->remoteId = remoteId; +} + +QByteArray Relation::remoteId() const +{ + return d->remoteId; +} + +bool Relation::isValid() const +{ + return (d->left.isValid() || !d->left.remoteId().isEmpty()) && (d->right.isValid() || !d->right.remoteId().isEmpty()) && !d->type.isEmpty(); +} + +uint Akonadi::qHash(const Relation &relation) +{ + return (3 * qHash(relation.left()) + qHash(relation.right()) + qHash(relation.type()) + qHash(relation.remoteId())); +} + +QDebug &operator<<(QDebug &debug, const Relation &relation) +{ + debug << "Akonadi::Relation( TYPE " << relation.type() << ", LEFT " << relation.left().id() << ", RIGHT " << relation.right().id() << ", REMOTEID " + << relation.remoteId() << ")"; + return debug; +} diff --git a/src/core/relation.h b/src/core/relation.h new file mode 100644 index 0000000..c27cd25 --- /dev/null +++ b/src/core/relation.h @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +namespace Akonadi +{ +class Relation; + +AKONADICORE_EXPORT unsigned int qHash(const Akonadi::Relation &); +} + +#include +#include +#include + +namespace Akonadi +{ +class Item; + +/** + * An Akonadi Relation. + * + * A Relation object represents an relation between two Akonadi items. + * + * An example usecase could be a association of a note with an email. The note (that for instance contains personal notes for the email), + * can be stored independently but is easily retrieved by asking for relations the email. + * + * The relation type allows to distinguish various types of relations that could for instance be bidirectional or not. + * + * @since 4.15 + */ +class AKONADICORE_EXPORT Relation +{ +public: + using List = QVector; + + /** + * The GENERIC type represents a generic relation between two items. + */ + static const char *GENERIC; + + /** + * Creates an invalid relation. + */ + Relation(); + + /** + * Creates a relation + */ + explicit Relation(const QByteArray &type, const Item &left, const Item &right); + + Relation(const Relation &); + Relation(Relation &&) noexcept; + ~Relation(); + + Relation &operator=(const Relation &); + Relation &operator=(Relation &&) noexcept; + + bool operator==(const Relation &) const; + bool operator!=(const Relation &) const; + + /** + * Sets the @p item of the left side of the relation. + */ + void setLeft(const Item &item); + + /** + * Returns the identifier of the left side of the relation. + */ + Q_REQUIRED_RESULT Item left() const; + + /** + * Sets the @p item of the right side of the relation. + */ + void setRight(const Akonadi::Item &item); + + /** + * Returns the identifier of the right side of the relation. + */ + Q_REQUIRED_RESULT Item right() const; + + /** + * Sets the type of the relation. + */ + void setType(const QByteArray &type); + + /** + * Returns the type of the relation. + */ + Q_REQUIRED_RESULT QByteArray type() const; + + /** + * Sets the remote id of the relation. + */ + void setRemoteId(const QByteArray &type); + + /** + * Returns the remote id of the relation. + */ + Q_REQUIRED_RESULT QByteArray remoteId() const; + + Q_REQUIRED_RESULT bool isValid() const; + +private: + struct Private; + QSharedDataPointer d; +}; + +} + +AKONADICORE_EXPORT QDebug &operator<<(QDebug &debug, const Akonadi::Relation &tag); + +Q_DECLARE_METATYPE(Akonadi::Relation) +Q_DECLARE_METATYPE(Akonadi::Relation::List) +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_TYPEINFO(Akonadi::Relation, Q_MOVABLE_TYPE); diff --git a/src/core/relationsync.cpp b/src/core/relationsync.cpp new file mode 100644 index 0000000..329cdc8 --- /dev/null +++ b/src/core/relationsync.cpp @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +namespace Akonadi +{ +class Item; +} + +#include "relationsync.h" +#include "akonadicore_debug.h" +#include "itemfetchscope.h" + +#include "jobs/itemfetchjob.h" +#include "jobs/relationcreatejob.h" +#include "jobs/relationdeletejob.h" +#include "jobs/relationfetchjob.h" + +using namespace Akonadi; + +RelationSync::RelationSync(QObject *parent) + : Job(parent) +{ +} + +RelationSync::~RelationSync() +{ +} + +void RelationSync::setRemoteRelations(const Akonadi::Relation::List &relations) +{ + mRemoteRelations = relations; + mRemoteRelationsSet = true; + diffRelations(); +} + +void RelationSync::doStart() +{ + auto fetch = new Akonadi::RelationFetchJob({Akonadi::Relation::GENERIC}, this); + connect(fetch, &KJob::result, this, &RelationSync::onLocalFetchDone); +} + +void RelationSync::onLocalFetchDone(KJob *job) +{ + auto fetch = static_cast(job); + mLocalRelations = fetch->relations(); + mLocalRelationsFetched = true; + diffRelations(); +} + +void RelationSync::diffRelations() +{ + if (!mRemoteRelationsSet || !mLocalRelationsFetched) { + qCDebug(AKONADICORE_LOG) << "waiting for delivery: " << mRemoteRelationsSet << mLocalRelationsFetched; + return; + } + + QHash relationByRid; + for (const Akonadi::Relation &localRelation : std::as_const(mLocalRelations)) { + if (!localRelation.remoteId().isEmpty()) { + relationByRid.insert(localRelation.remoteId(), localRelation); + } + } + + for (const Akonadi::Relation &remoteRelation : std::as_const(mRemoteRelations)) { + if (relationByRid.contains(remoteRelation.remoteId())) { + relationByRid.remove(remoteRelation.remoteId()); + } else { + // New relation or had its GID updated, so create one now + auto createJob = new RelationCreateJob(remoteRelation, this); + connect(createJob, &KJob::result, this, &RelationSync::checkDone); + } + } + + for (const Akonadi::Relation &removedRelation : std::as_const(relationByRid)) { + // Removed remotely, remove locally + auto removeJob = new RelationDeleteJob(removedRelation, this); + connect(removeJob, &KJob::result, this, &RelationSync::checkDone); + } + checkDone(); +} + +void RelationSync::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during CollectionSync: " << job->errorString() << job->metaObject()->className(); + // pretend there were no errors + Akonadi::Job::removeSubjob(job); + } else { + Akonadi::Job::slotResult(job); + } +} + +void RelationSync::checkDone() +{ + if (hasSubjobs()) { + qCDebug(AKONADICORE_LOG) << "Still going"; + return; + } + qCDebug(AKONADICORE_LOG) << "done"; + emitResult(); +} diff --git a/src/core/relationsync.h b/src/core/relationsync.h new file mode 100644 index 0000000..ba39121 --- /dev/null +++ b/src/core/relationsync.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include "akonadicore_export.h" + +#include "jobs/job.h" +#include "relation.h" + +namespace Akonadi +{ +class AKONADICORE_EXPORT RelationSync : public Akonadi::Job +{ + Q_OBJECT +public: + explicit RelationSync(QObject *parent = nullptr); + ~RelationSync() override; + + void setRemoteRelations(const Akonadi::Relation::List &relations); + +protected: + void doStart() override; + +private Q_SLOTS: + void onLocalFetchDone(KJob *job); + void slotResult(KJob *job) override; + +private: + void diffRelations(); + void checkDone(); + +private: + Akonadi::Relation::List mRemoteRelations; + Akonadi::Relation::List mLocalRelations; + bool mRemoteRelationsSet = false; + bool mLocalRelationsFetched = false; +}; + +} + diff --git a/src/core/remotelog.cpp b/src/core/remotelog.cpp new file mode 100644 index 0000000..4e5112a --- /dev/null +++ b/src/core/remotelog.cpp @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include +#include +#include + +namespace +{ +const auto initRemoteLogger = []() { + qAddPreRoutine([]() { + // Initialize remote logging from event loop, this way applications like + // Akonadi Console or TestRunner have a chance to change AKONADI_INSTANCE + // before the RemoteLog class initialize + QTimer::singleShot(0, qApp, []() { + akInitRemoteLog(); + }); + }); + return true; +}(); + +} // namespace diff --git a/src/core/searchquery.cpp b/src/core/searchquery.cpp new file mode 100644 index 0000000..42c59f9 --- /dev/null +++ b/src/core/searchquery.cpp @@ -0,0 +1,362 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "searchquery.h" +#include "akonadicore_debug.h" + +#include +#include +#include + +using namespace Akonadi; + +class SearchTerm::Private : public QSharedData +{ +public: + bool operator==(const Private &other) const + { + return relation == other.relation && isNegated == other.isNegated && terms == other.terms && key == other.key && value == other.value + && condition == other.condition; + } + + QString key; + QVariant value; + Condition condition = SearchTerm::CondEqual; + Relation relation = SearchTerm::RelAnd; + QList terms; + bool isNegated = false; +}; + +class SearchQuery::Private : public QSharedData +{ +public: + bool operator==(const Private &other) const + { + return rootTerm == other.rootTerm && limit == other.limit; + } + + static QVariantMap termToJSON(const SearchTerm &term) + { + const QList &subTerms = term.subTerms(); + QVariantMap termJSON; + termJSON.insert(QStringLiteral("negated"), term.isNegated()); + if (subTerms.isEmpty()) { + if (!term.isNull()) { + termJSON.insert(QStringLiteral("key"), term.key()); + termJSON.insert(QStringLiteral("value"), term.value()); + termJSON.insert(QStringLiteral("cond"), static_cast(term.condition())); + } + } else { + termJSON.insert(QStringLiteral("rel"), static_cast(term.relation())); + QVariantList subTermsJSON; + subTermsJSON.reserve(subTerms.count()); + for (const SearchTerm &term : std::as_const(subTerms)) { + subTermsJSON.append(termToJSON(term)); + } + termJSON.insert(QStringLiteral("subTerms"), subTermsJSON); + } + + return termJSON; + } + + static SearchTerm JSONToTerm(const QVariantMap &map) + { + if (map.isEmpty()) { + return SearchTerm(); + } else if (map.contains(QStringLiteral("key"))) { + SearchTerm term(map[QStringLiteral("key")].toString(), + map[QStringLiteral("value")], + static_cast(map[QStringLiteral("cond")].toInt())); + term.setIsNegated(map[QStringLiteral("negated")].toBool()); + return term; + } else if (map.contains(QStringLiteral("rel"))) { + SearchTerm term(static_cast(map[QStringLiteral("rel")].toInt())); + term.setIsNegated(map[QStringLiteral("negated")].toBool()); + const QList list = map[QStringLiteral("subTerms")].toList(); + for (const QVariant &var : list) { + term.addSubTerm(JSONToTerm(var.toMap())); + } + return term; + } else { + qCWarning(AKONADICORE_LOG) << "Invalid JSON for term: " << map; + return SearchTerm(); + } + } + + SearchTerm rootTerm; + int limit = -1; +}; + +SearchTerm::SearchTerm(SearchTerm::Relation relation) + : d(new Private) +{ + d->relation = relation; +} + +SearchTerm::SearchTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition) + : d(new Private) +{ + d->relation = RelAnd; + d->key = key; + d->value = value; + d->condition = condition; +} + +SearchTerm::SearchTerm(const SearchTerm &other) + : d(other.d) +{ +} + +SearchTerm::~SearchTerm() = default; + +SearchTerm &SearchTerm::operator=(const SearchTerm &other) +{ + d = other.d; + return *this; +} + +bool SearchTerm::operator==(const SearchTerm &other) const +{ + return *d == *other.d; +} + +bool SearchTerm::isNull() const +{ + return d->key.isEmpty() && d->value.isNull() && d->terms.isEmpty(); +} + +QString SearchTerm::key() const +{ + return d->key; +} + +QVariant SearchTerm::value() const +{ + return d->value; +} + +SearchTerm::Condition SearchTerm::condition() const +{ + return d->condition; +} + +void SearchTerm::setIsNegated(bool negated) +{ + d->isNegated = negated; +} + +bool SearchTerm::isNegated() const +{ + return d->isNegated; +} + +void SearchTerm::addSubTerm(const SearchTerm &term) +{ + d->terms << term; +} + +QList SearchTerm::subTerms() const +{ + return d->terms; +} + +SearchTerm::Relation SearchTerm::relation() const +{ + return d->relation; +} + +SearchQuery::SearchQuery(SearchTerm::Relation rel) + : d(new Private) +{ + d->rootTerm = SearchTerm(rel); +} + +SearchQuery::SearchQuery(const SearchQuery &other) + : d(other.d) +{ +} + +SearchQuery::~SearchQuery() +{ +} + +SearchQuery &SearchQuery::operator=(const SearchQuery &other) +{ + d = other.d; + return *this; +} + +bool SearchQuery::operator==(const SearchQuery &other) const +{ + return *d == *other.d; +} + +bool SearchQuery::isNull() const +{ + return d->rootTerm.isNull(); +} + +SearchTerm SearchQuery::term() const +{ + return d->rootTerm; +} + +void SearchQuery::addTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition) +{ + addTerm(SearchTerm(key, value, condition)); +} + +void SearchQuery::addTerm(const SearchTerm &term) +{ + d->rootTerm.addSubTerm(term); +} + +void SearchQuery::setTerm(const SearchTerm &term) +{ + d->rootTerm = term; +} + +void SearchQuery::setLimit(int limit) +{ + d->limit = limit; +} + +int SearchQuery::limit() const +{ + return d->limit; +} + +QByteArray SearchQuery::toJSON() const +{ + QVariantMap root; + if (!d->rootTerm.isNull()) { + root = Private::termToJSON(d->rootTerm); + root.insert(QStringLiteral("limit"), d->limit); + } + + QJsonObject jo = QJsonObject::fromVariantMap(root); + QJsonDocument jdoc; + jdoc.setObject(jo); + return jdoc.toJson(); +} + +SearchQuery SearchQuery::fromJSON(const QByteArray &jsonData) +{ + QJsonParseError error; + const QJsonDocument json = QJsonDocument::fromJson(jsonData, &error); + if (error.error != QJsonParseError::NoError || json.isNull()) { + return SearchQuery(); + } + + SearchQuery query; + const QJsonObject obj = json.object(); + query.d->rootTerm = Private::JSONToTerm(obj.toVariantMap()); + if (obj.contains(QLatin1String("limit"))) { + query.d->limit = obj.value(QStringLiteral("limit")).toInt(); + } + return query; +} + +static QMap emailSearchFieldMapping() +{ + static QMap mapping; + if (mapping.isEmpty()) { + mapping.insert(EmailSearchTerm::Body, QStringLiteral("body")); + mapping.insert(EmailSearchTerm::Headers, QStringLiteral("headers")); + mapping.insert(EmailSearchTerm::Subject, QStringLiteral("subject")); + mapping.insert(EmailSearchTerm::Message, QStringLiteral("message")); + mapping.insert(EmailSearchTerm::HeaderFrom, QStringLiteral("from")); + mapping.insert(EmailSearchTerm::HeaderTo, QStringLiteral("to")); + mapping.insert(EmailSearchTerm::HeaderCC, QStringLiteral("cc")); + mapping.insert(EmailSearchTerm::HeaderBCC, QStringLiteral("bcc")); + mapping.insert(EmailSearchTerm::HeaderReplyTo, QStringLiteral("replyto")); + mapping.insert(EmailSearchTerm::HeaderOrganization, QStringLiteral("organization")); + mapping.insert(EmailSearchTerm::HeaderListId, QStringLiteral("listid")); + mapping.insert(EmailSearchTerm::HeaderResentFrom, QStringLiteral("resentfrom")); + mapping.insert(EmailSearchTerm::HeaderXLoop, QStringLiteral("xloop")); + mapping.insert(EmailSearchTerm::HeaderXMailingList, QStringLiteral("xmailinglist")); + mapping.insert(EmailSearchTerm::HeaderXSpamFlag, QStringLiteral("xspamflag")); + mapping.insert(EmailSearchTerm::HeaderDate, QStringLiteral("date")); + mapping.insert(EmailSearchTerm::HeaderOnlyDate, QStringLiteral("onlydate")); + mapping.insert(EmailSearchTerm::MessageStatus, QStringLiteral("messagestatus")); + mapping.insert(EmailSearchTerm::MessageTag, QStringLiteral("messagetag")); + mapping.insert(EmailSearchTerm::ByteSize, QStringLiteral("size")); + mapping.insert(EmailSearchTerm::Attachment, QStringLiteral("attachment")); + } + + return mapping; +} + +EmailSearchTerm::EmailSearchTerm(EmailSearchTerm::EmailSearchField field, const QVariant &value, SearchTerm::Condition condition) + : SearchTerm(toKey(field), value, condition) +{ +} + +QString EmailSearchTerm::toKey(EmailSearchTerm::EmailSearchField field) +{ + return emailSearchFieldMapping().value(field); +} + +EmailSearchTerm::EmailSearchField EmailSearchTerm::fromKey(const QString &key) +{ + return emailSearchFieldMapping().key(key); +} + +static QMap contactSearchFieldMapping() +{ + static QMap mapping; + if (mapping.isEmpty()) { + mapping.insert(ContactSearchTerm::Name, QStringLiteral("name")); + mapping.insert(ContactSearchTerm::Nickname, QStringLiteral("nickname")); + mapping.insert(ContactSearchTerm::Email, QStringLiteral("email")); + mapping.insert(ContactSearchTerm::Uid, QStringLiteral("uid")); + mapping.insert(ContactSearchTerm::All, QStringLiteral("all")); + } + return mapping; +} + +ContactSearchTerm::ContactSearchTerm(ContactSearchTerm::ContactSearchField field, const QVariant &value, SearchTerm::Condition condition) + : SearchTerm(toKey(field), value, condition) +{ +} + +QString ContactSearchTerm::toKey(ContactSearchTerm::ContactSearchField field) +{ + return contactSearchFieldMapping().value(field); +} + +ContactSearchTerm::ContactSearchField ContactSearchTerm::fromKey(const QString &key) +{ + return contactSearchFieldMapping().key(key); +} + +QMap incidenceSearchFieldMapping() +{ + QMap mapping; + if (mapping.isEmpty()) { + mapping.insert(IncidenceSearchTerm::All, QStringLiteral("all")); + mapping.insert(IncidenceSearchTerm::PartStatus, QStringLiteral("partstatus")); + mapping.insert(IncidenceSearchTerm::Organizer, QStringLiteral("organizer")); + mapping.insert(IncidenceSearchTerm::Summary, QStringLiteral("summary")); + mapping.insert(IncidenceSearchTerm::Location, QStringLiteral("location")); + } + return mapping; +} + +IncidenceSearchTerm::IncidenceSearchTerm(IncidenceSearchTerm::IncidenceSearchField field, const QVariant &value, SearchTerm::Condition condition) + : SearchTerm(toKey(field), value, condition) +{ +} + +QString IncidenceSearchTerm::toKey(IncidenceSearchTerm::IncidenceSearchField field) +{ + return incidenceSearchFieldMapping().value(field); +} + +IncidenceSearchTerm::IncidenceSearchField IncidenceSearchTerm::fromKey(const QString &key) +{ + return incidenceSearchFieldMapping().key(key); +} diff --git a/src/core/searchquery.h b/src/core/searchquery.h new file mode 100644 index 0000000..6af7772 --- /dev/null +++ b/src/core/searchquery.h @@ -0,0 +1,290 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadicore_export.h" + +namespace Akonadi +{ +/** + * Search term represents the actual condition within query. + * + * SearchTerm can either have multiple subterms, or can be so-called endterm, when + * there are no more subterms, but instead the actual condition is specified, that + * is have key, value and relation between them. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT SearchTerm +{ +public: + enum Relation { + RelAnd, + RelOr, + }; + + enum Condition { + CondEqual, + CondGreaterThan, + CondGreaterOrEqual, + CondLessThan, + CondLessOrEqual, + CondContains, + }; + + /** + * Constructs a term where all subterms will be in given relation + */ + explicit SearchTerm(SearchTerm::Relation relation = SearchTerm::RelAnd); + + /** + * Constructs an end term + */ + SearchTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + SearchTerm(const SearchTerm &other); + ~SearchTerm(); + + SearchTerm &operator=(const SearchTerm &other); + Q_REQUIRED_RESULT bool operator==(const SearchTerm &other) const; + + Q_REQUIRED_RESULT bool isNull() const; + + /** + * Returns key of this end term. + */ + Q_REQUIRED_RESULT QString key() const; + + /** + * Returns value of this end term. + */ + Q_REQUIRED_RESULT QVariant value() const; + + /** + * Returns relation between key and value. + */ + Q_REQUIRED_RESULT SearchTerm::Condition condition() const; + + /** + * Adds a new subterm to this term. + * + * Subterms will be in relation as specified in SearchTerm constructor. + * + * If there are subterms in a term, key, value and condition are ignored. + */ + void addSubTerm(const SearchTerm &term); + + /** + * Returns all subterms, or an empty list if this is an end term. + */ + Q_REQUIRED_RESULT QList subTerms() const; + + /** + * Returns relation in which all subterms are. + */ + Q_REQUIRED_RESULT SearchTerm::Relation relation() const; + + /** + * Sets whether the entire term is negated. + */ + void setIsNegated(bool negated); + + /** + * Returns whether the entire term is negated. + */ + Q_REQUIRED_RESULT bool isNegated() const; + +private: + class Private; + QSharedDataPointer d; +}; + +/** + * @brief A query that can be passed to ItemSearchJob or others. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT SearchQuery +{ +public: + /** + * Constructs query where all added terms will be in given relation + */ + explicit SearchQuery(SearchTerm::Relation rel = SearchTerm::RelAnd); + + ~SearchQuery(); + SearchQuery(const SearchQuery &other); + SearchQuery &operator=(const SearchQuery &other); + bool operator==(const SearchQuery &other) const; + + bool isNull() const; + + /** + * Adds a new term. + */ + void addTerm(const QString &key, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Adds a new term with subterms + */ + void addTerm(const SearchTerm &term); + + /** + * Sets the root term + */ + void setTerm(const SearchTerm &term); + + /** + * Returns the root term. + */ + SearchTerm term() const; + + /** + * Sets the maximum number of results. + * + * Note that this limit is only evaluated per search backend, + * so the total number of results retrieved may be larger. + */ + void setLimit(int limit); + + /** + * Returns the maximum number of results. + * + * The default value is -1, indicating no limit. + */ + int limit() const; + + QByteArray toJSON() const; + static SearchQuery fromJSON(const QByteArray &json); + +private: + class Private; + QSharedDataPointer d; +}; + +/** + * A search term for an email field. + * + * This class can be used to create queries that akonadi email search backends understand. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT EmailSearchTerm : public SearchTerm +{ +public: + /** + * All fields expect a search string unless noted otherwise. + */ + enum EmailSearchField { + Unknown, + Subject, + Body, + Message, // Complete message including headers, body and attachment + Headers, // All headers + HeaderFrom, + HeaderTo, + HeaderCC, + HeaderBCC, + HeaderReplyTo, + HeaderOrganization, + HeaderListId, + HeaderResentFrom, + HeaderXLoop, + HeaderXMailingList, + HeaderXSpamFlag, + HeaderDate, // Expects QDateTime + HeaderOnlyDate, // Expectes QDate + MessageStatus, // Expects message flag from Akonadi::MessageFlags. Boolean filter. + ByteSize, // Expects int + Attachment, // Textsearch on attachment + MessageTag + }; + + /** + * Constructs an email end term + */ + EmailSearchTerm(EmailSearchField field, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Translates field to key + */ + static QString toKey(EmailSearchField); + + /** + * Translates key to field + */ + static EmailSearchField fromKey(const QString &key); +}; + +/** + * A search term for a contact field. + * + * This class can be used to create queries that akonadi contact search backends understand. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT ContactSearchTerm : public SearchTerm +{ +public: + enum ContactSearchField { + Unknown, + Name, + Email, + Nickname, + Uid, + All // Special field: matches all contacts. + }; + + ContactSearchTerm(ContactSearchField field, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Translates field to key + */ + static QString toKey(ContactSearchField); + + /** + * Translates key to field + */ + static ContactSearchField fromKey(const QString &key); +}; + +/** + * A search term for a incidence field. + * + * This class can be used to create queries that akonadi incidence search backends understand. + * + * @since 5.0 + */ +class AKONADICORE_EXPORT IncidenceSearchTerm : public SearchTerm +{ +public: + enum IncidenceSearchField { + Unknown, + All, + PartStatus, // Own PartStatus + Organizer, + Summary, + Location + }; + + IncidenceSearchTerm(IncidenceSearchField field, const QVariant &value, SearchTerm::Condition condition = SearchTerm::CondEqual); + + /** + * Translates field to key + */ + static QString toKey(IncidenceSearchField); + + /** + * Translates key to field + */ + static IncidenceSearchField fromKey(const QString &key); +}; + +} + diff --git a/src/core/servermanager.cpp b/src/core/servermanager.cpp new file mode 100644 index 0000000..70d7060 --- /dev/null +++ b/src/core/servermanager.cpp @@ -0,0 +1,418 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "servermanager.h" +#include "servermanager_p.h" + +#include "agentmanager.h" +#include "agenttype.h" +#include "firstrun_p.h" +#include "session_p.h" + +#include "akonadicore_debug.h" + +#include +#include +#if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#endif + +#include "private/dbus_p.h" +#include "private/instance_p.h" +#include "private/protocol_p.h" +#include "private/standarddirs_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::ServerManagerPrivate +{ +public: + ServerManagerPrivate() + : instance(new ServerManager(this)) + , mState(ServerManager::NotRunning) + , mSafetyTimer(new QTimer) + { + mState = instance->state(); + mSafetyTimer->setSingleShot(true); + mSafetyTimer->setInterval(30000); + QObject::connect(mSafetyTimer.data(), &QTimer::timeout, instance, [this]() { + timeout(); + }); + if (mState == ServerManager::Running && Internal::clientType() == Internal::User && !ServerManager::hasInstanceIdentifier()) { + mFirstRunner = new Firstrun(instance); + } + } + + ~ServerManagerPrivate() + { + delete instance; + } + + void checkStatusChanged() + { + setState(instance->state()); + } + + void setState(ServerManager::State state) + { + if (mState != state) { + mState = state; + Q_EMIT instance->stateChanged(state); + if (state == ServerManager::Running) { + Q_EMIT instance->started(); + if (!mFirstRunner && Internal::clientType() == Internal::User && !ServerManager::hasInstanceIdentifier()) { + mFirstRunner = new Firstrun(instance); + } + } else if (state == ServerManager::NotRunning || state == ServerManager::Broken) { + Q_EMIT instance->stopped(); + } + if (state == ServerManager::Starting || state == ServerManager::Stopping) { + QMetaObject::invokeMethod(mSafetyTimer.data(), QOverload<>::of(&QTimer::start), Qt::QueuedConnection); // in case we are in a different thread + } else { + QMetaObject::invokeMethod(mSafetyTimer.data(), &QTimer::stop, Qt::QueuedConnection); // in case we are in a different thread + } + } + } + + void timeout() + { + if (mState == ServerManager::Starting || mState == ServerManager::Stopping) { + setState(ServerManager::Broken); + } + } + + ServerManager *instance = nullptr; + static int serverProtocolVersion; + static uint generation; + ServerManager::State mState; + QScopedPointer mSafetyTimer; + Firstrun *mFirstRunner = nullptr; + static Internal::ClientType clientType; + QString mBrokenReason; + std::unique_ptr serviceWatcher; +}; + +int ServerManagerPrivate::serverProtocolVersion = -1; +uint ServerManagerPrivate::generation = 0; +Internal::ClientType ServerManagerPrivate::clientType = Internal::User; + +Q_GLOBAL_STATIC(ServerManagerPrivate, sInstance) // NOLINT(readability-redundant-member-init) + +ServerManager::ServerManager(ServerManagerPrivate *dd) + : d(dd) +{ +#if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0) + Kdelibs4ConfigMigrator migrate(QStringLiteral("servermanager")); + migrate.setConfigFiles(QStringList() << QStringLiteral("akonadi-firstrunrc")); + migrate.migrate(); +#endif + qRegisterMetaType(); + + d->serviceWatcher = std::make_unique(ServerManager::serviceName(ServerManager::Server), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration); + d->serviceWatcher->addWatchedService(ServerManager::serviceName(ServerManager::Control)); + d->serviceWatcher->addWatchedService(ServerManager::serviceName(ServerManager::ControlLock)); + d->serviceWatcher->addWatchedService(ServerManager::serviceName(ServerManager::UpgradeIndicator)); + + // this (and also the two connects below) are queued so that they trigger after AgentManager is done loading + // the current agent types and instances + // this ensures the invariant of AgentManager reporting a consistent state if ServerManager::state() == Running + // that's the case with direct connections as well, but only after you enter the event loop once + connect( + d->serviceWatcher.get(), + &QDBusServiceWatcher::serviceRegistered, + this, + [this]() { + d->serverProtocolVersion = -1; + d->checkStatusChanged(); + }, + Qt::QueuedConnection); + connect( + d->serviceWatcher.get(), + &QDBusServiceWatcher::serviceUnregistered, + this, + [this](const QString &name) { + if (name == ServerManager::serviceName(ServerManager::ControlLock) && d->mState == ServerManager::Starting) { + // Control.Lock has disappeared during startup, which means that akonadi_control + // has terminated, most probably because it was not able to start akonadiserver + // process. Don't wait 30 seconds for sefetyTimeout, but go into Broken state + // immediately. + d->setState(ServerManager::Broken); + return; + } + + d->serverProtocolVersion = -1; + d->checkStatusChanged(); + }, + Qt::QueuedConnection); + + // AgentManager is dangerous to use for agents themselves + if (Internal::clientType() != Internal::User) { + return; + } + + connect( + AgentManager::self(), + &AgentManager::typeAdded, + this, + [this]() { + d->checkStatusChanged(); + }, + Qt::QueuedConnection); + connect( + AgentManager::self(), + &AgentManager::typeRemoved, + this, + [this]() { + d->checkStatusChanged(); + }, + Qt::QueuedConnection); +} + +ServerManager *Akonadi::ServerManager::self() +{ + return sInstance->instance; +} + +bool ServerManager::start() +{ + const bool controlRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control)); + const bool serverRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server)); + if (controlRegistered && serverRegistered) { + return true; + } + + const bool controlLockRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::ControlLock)); + if (controlLockRegistered || controlRegistered) { + qCDebug(AKONADICORE_LOG) << "Akonadi server is already starting up"; + sInstance->setState(Starting); + return true; + } + + qCDebug(AKONADICORE_LOG) << "executing akonadi_control"; + QStringList args; + if (hasInstanceIdentifier()) { + args << QStringLiteral("--instance") << instanceIdentifier(); + } + const bool ok = QProcess::startDetached(QStringLiteral("akonadi_control"), args); + if (!ok) { + qCWarning(AKONADICORE_LOG) << "Unable to execute akonadi_control, falling back to D-Bus auto-launch"; + QDBusReply reply = QDBusConnection::sessionBus().interface()->startService(ServerManager::serviceName(ServerManager::Control)); + if (!reply.isValid()) { + qCDebug(AKONADICORE_LOG) << "Akonadi server could not be started via D-Bus either: " << reply.error().message(); + return false; + } + } + sInstance->setState(Starting); + return true; +} + +bool ServerManager::stop() +{ + QDBusInterface iface(ServerManager::serviceName(ServerManager::Control), + QStringLiteral("/ControlManager"), + QStringLiteral("org.freedesktop.Akonadi.ControlManager")); + if (!iface.isValid()) { + return false; + } + iface.call(QDBus::NoBlock, QStringLiteral("shutdown")); + sInstance->setState(Stopping); + return true; +} + +void ServerManager::showSelfTestDialog(QWidget *parent) +{ + Q_UNUSED(parent) + QProcess::startDetached(QStringLiteral("akonadiselftest"), QStringList()); +} + +bool ServerManager::isRunning() +{ + return state() == Running; +} + +ServerManager::State ServerManager::state() +{ + ServerManager::State previousState = NotRunning; + if (sInstance.exists()) { // be careful, this is called from the ServerManager::Private ctor, so using sInstance unprotected can cause infinite recursion + previousState = sInstance->mState; + sInstance->mBrokenReason.clear(); + } + + const bool serverUpgrading = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::UpgradeIndicator)); + if (serverUpgrading) { + return Upgrading; + } + + const bool controlRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control)); + const bool serverRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server)); + if (controlRegistered && serverRegistered) { + // check if the server protocol is recent enough + if (sInstance.exists()) { + if (Internal::serverProtocolVersion() >= 0 && Internal::serverProtocolVersion() != Protocol::version()) { + sInstance->mBrokenReason = i18n( + "The Akonadi server protocol version differs from the protocol version used by this application.\n" + "If you recently updated your system please log out and back in to make sure all applications use the " + "correct protocol version."); + return Broken; + } + } + + // AgentManager is dangerous to use for agents themselves + if (Internal::clientType() == Internal::User) { + // besides the running server processes we also need at least one resource to be operational + const AgentType::List agentTypes = AgentManager::self()->types(); + for (const AgentType &type : agentTypes) { + if (type.capabilities().contains(QLatin1String("Resource"))) { + return Running; + } + } + if (sInstance.exists()) { + sInstance->mBrokenReason = i18n("There are no Akonadi Agents available. Please verify your KDE PIM installation."); + } + return Broken; + } else { + return Running; + } + } + + const bool controlLockRegistered = QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::ControlLock)); + if (controlLockRegistered || controlRegistered) { + qCDebug(AKONADICORE_LOG) << "Akonadi server is only partially running. Server:" << serverRegistered << "ControlLock:" << controlLockRegistered + << "Control:" << controlRegistered; + if (previousState == Running) { + return NotRunning; // we don't know if it's starting or stopping, probably triggered by someone else + } + return previousState; + } + + if (serverRegistered) { + qCWarning(AKONADICORE_LOG) << "Akonadi server running without control process!"; + return Broken; + } + + if (previousState == Starting) { // valid case where nothing is running (yet) + return previousState; + } + return NotRunning; +} + +QString ServerManager::brokenReason() +{ + if (sInstance.exists()) { + return sInstance->mBrokenReason; + } + return QString(); +} + +QString ServerManager::instanceIdentifier() +{ + return Instance::identifier(); +} + +bool ServerManager::hasInstanceIdentifier() +{ + return Instance::hasIdentifier(); +} + +QString ServerManager::serviceName(ServerManager::ServiceType serviceType) +{ + switch (serviceType) { + case Server: + return DBus::serviceName(DBus::Server); + case Control: + return DBus::serviceName(DBus::Control); + case ControlLock: + return DBus::serviceName(DBus::ControlLock); + case UpgradeIndicator: + return DBus::serviceName(DBus::UpgradeIndicator); + } + Q_ASSERT(!"WTF?"); + return QString(); +} + +QString ServerManager::agentServiceName(ServiceAgentType agentType, const QString &identifier) +{ + switch (agentType) { + case Agent: + return DBus::agentServiceName(identifier, DBus::Agent); + case Resource: + return DBus::agentServiceName(identifier, DBus::Resource); + case Preprocessor: + return DBus::agentServiceName(identifier, DBus::Preprocessor); + } + Q_ASSERT(!"WTF?"); + return QString(); +} + +QString ServerManager::serverConfigFilePath(OpenMode openMode) +{ + return StandardDirs::serverConfigFile(openMode == Akonadi::ServerManager::ReadOnly ? StandardDirs::ReadOnly : StandardDirs::ReadWrite); +} + +QString ServerManager::agentConfigFilePath(const QString &identifier) +{ + return StandardDirs::agentConfigFile(identifier); +} + +QString ServerManager::addNamespace(const QString &string) +{ + if (Instance::hasIdentifier()) { + return string % QLatin1Char('_') % Instance::identifier(); + } + return string; +} + +uint ServerManager::generation() +{ + return Internal::generation(); +} + +int Internal::serverProtocolVersion() +{ + return ServerManagerPrivate::serverProtocolVersion; +} + +void Internal::setServerProtocolVersion(int version) +{ + ServerManagerPrivate::serverProtocolVersion = version; + if (sInstance.exists()) { + sInstance->checkStatusChanged(); + } +} + +uint Internal::generation() +{ + return ServerManagerPrivate::generation; +} + +void Internal::setGeneration(uint generation) +{ + ServerManagerPrivate::generation = generation; +} + +Internal::ClientType Internal::clientType() +{ + return ServerManagerPrivate::clientType; +} + +void Internal::setClientType(ClientType type) +{ + ServerManagerPrivate::clientType = type; +} + +#include "moc_servermanager.cpp" diff --git a/src/core/servermanager.h b/src/core/servermanager.h new file mode 100644 index 0000000..1a344b7 --- /dev/null +++ b/src/core/servermanager.h @@ -0,0 +1,225 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include +#include + +namespace Akonadi +{ +class ServerManagerPrivate; + +/** + * @short Provides methods to control the Akonadi server process. + * + * Asynchronous, low-level control of the Akonadi server. + * Akonadi::Control provides a synchronous interface to some of the methods in here. + * + * @author Volker Krause + * @see Akonadi::Control + * @since 4.2 + */ +class AKONADICORE_EXPORT ServerManager : public QObject +{ + Q_OBJECT +public: + /** + * Enum for the various states the server can be in. + * @since 4.5 + */ + enum State { + NotRunning, ///< Server is not running, could be no one started it yet or it failed to start. + Starting, ///< Server was started but is not yet running. + Running, ///< Server is running and operational. + Stopping, ///< Server is shutting down. + Broken, ///< Server is not operational and an error has been detected. + Upgrading ///< Server is performing a database upgrade as part of a new startup. + }; + + /** + * Starts the server. This method returns immediately and does not wait + * until the server is actually up and running. + * @return @c true if the start was possible (which not necessarily means + * the server is really running though) and @c false if an immediate error occurred. + * @see Akonadi::Control::start() + */ + static bool start(); + + /** + * Stops the server. This methods returns immediately after the shutdown + * command has been send and does not wait until the server is actually + * shut down. + * @return @c true if the shutdown command was sent successfully, @c false + * otherwise + */ + static bool stop(); + + /** + * Shows the Akonadi self test dialog, which tests Akonadi for various problems + * and reports these to the user if. + * @param parent the parent widget for the dialog + */ + static void showSelfTestDialog(QWidget *parent); + + /** + * Checks if the server is available currently. For more detailed status information + * see state(). + * @see state() + */ + Q_REQUIRED_RESULT static bool isRunning(); + + /** + * Returns the state of the server. + * @since 4.5 + */ + Q_REQUIRED_RESULT static State state(); + + /** + * Returns the reason why the Server is broken, if known. + * + * If state() is @p Broken, then you can use this method to obtain a more + * detailed description of the problem and present it to users. Note that + * the message can be empty if the reason is not known. + * + * @since 5.6 + */ + Q_REQUIRED_RESULT static QString brokenReason(); + + /** + * Returns the identifier of the Akonadi instance we are connected to. This is usually + * an empty string (representing the default instance), unless you have explicitly set + * the AKONADI_INSTANCE environment variable to connect to a different one. + * @since 4.10 + */ + Q_REQUIRED_RESULT static QString instanceIdentifier(); + + /** + * Returns @c true if we are connected to a non-default Akonadi server instance. + * @since 4.10 + */ + Q_REQUIRED_RESULT static bool hasInstanceIdentifier(); + + /** + * Types of known D-Bus services. + * @since 4.10 + */ + enum ServiceType { + Server, + Control, + ControlLock, + UpgradeIndicator, + }; + + /** + * Returns the namespaced D-Bus service name for @p serviceType. + * Use this rather the raw service name strings in order to support usage of a non-default + * instance of the Akonadi server. + * @param serviceType the service type for which to return the D-Bus name + * @since 4.10 + */ + static QString serviceName(ServiceType serviceType); + + /** + * Known agent types. + * @since 4.10 + */ + enum ServiceAgentType { + Agent, + Resource, + Preprocessor, + }; + + /** + * Returns the namespaced D-Bus service name for an agent of type @p agentType with agent + * identifier @p identifier. + * @param agentType the agent type to use for D-Bus base name + * @param identifier the agent identifier to include in the D-Bus name + * @since 4.10 + */ + Q_REQUIRED_RESULT static QString agentServiceName(ServiceAgentType agentType, const QString &identifier); + + /** + * Adds the multi-instance namespace to @p string if required (with '_' as separator). + * Use whenever a multi-instance safe name is required (configfiles, identifiers, ...). + * @param string the string to adapt + * @since 4.10 + */ + Q_REQUIRED_RESULT static QString addNamespace(const QString &string); + + /** + * Returns the singleton instance of this class, for connecting to its + * signals + */ + static ServerManager *self(); + + enum OpenMode { + ReadOnly, + ReadWrite, + }; + /** + * Returns absolute path to akonadiserverrc file with Akonadi server + * configuration. + */ + Q_REQUIRED_RESULT static QString serverConfigFilePath(OpenMode openMode); + + /** + * Returns absolute path to configuration file of an agent identified by + * given @p identifier. + */ + Q_REQUIRED_RESULT static QString agentConfigFilePath(const QString &identifier); + + /** + * Returns current Akonadi database generation identifier + * + * Generation is guaranteed to never change unless as long as the database + * backend is not removed and re-created. In such case it is guaranteed that + * the new generation number will be higher than the previous one. + * + * Generation can be used by applications to detect when Akonadi database + * has been recreated and thus some of the configuration (for example + * collection IDs stored in a config file) must be invalidated. + * + * @note Note that the generation number is only available if the server + * is running. If this function is called before the server starts it will + * return 0. + * + * @since 5.4 + */ + Q_REQUIRED_RESULT static uint generation(); + +Q_SIGNALS: + /** + * Emitted whenever the server becomes fully operational. + */ + void started(); + + /** + * Emitted whenever the server becomes unavailable. + */ + void stopped(); + + /** + * Emitted whenever the server state changes. + * @param state the new server state + * @since 4.5 + */ + void stateChanged(Akonadi::ServerManager::State state); + +private: + /// @cond PRIVATE + friend class ServerManagerPrivate; + ServerManager(ServerManagerPrivate *dd); + ServerManagerPrivate *const d; + /// @endcond +}; + +} + +Q_DECLARE_METATYPE(Akonadi::ServerManager::State) + diff --git a/src/core/servermanager_p.h b/src/core/servermanager_p.h new file mode 100644 index 0000000..2df9db6 --- /dev/null +++ b/src/core/servermanager_p.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +namespace Akonadi +{ +namespace Internal +{ +AKONADICORE_EXPORT int serverProtocolVersion(); +AKONADICORE_EXPORT void setServerProtocolVersion(int version); +AKONADICORE_EXPORT uint generation(); +AKONADICORE_EXPORT void setGeneration(uint generation); + +enum ClientType { + User, + Agent, + Resource, +}; +AKONADICORE_EXPORT ClientType clientType(); +AKONADICORE_EXPORT void setClientType(ClientType type); + +} + +} diff --git a/src/core/session.cpp b/src/core/session.cpp new file mode 100644 index 0000000..6c0a62a --- /dev/null +++ b/src/core/session.cpp @@ -0,0 +1,459 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "session.h" +#include "session_p.h" + +#include "job.h" +#include "job_p.h" +#include "private/protocol_p.h" +#include "private/standarddirs_p.h" +#include "protocolhelper_p.h" +#include "servermanager.h" +#include "servermanager_p.h" +#include "sessionthread_p.h" + +#include "akonadicore_debug.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +// ### FIXME pipelining got broken by switching result emission in JobPrivate::handleResponse to delayed emission +// in order to work around exec() deadlocks. As a result of that Session knows to late about a finished job and still +// sends responses for the next one to the already finished one +#define PIPELINE_LENGTH 0 +//#define PIPELINE_LENGTH 2 + +using namespace Akonadi; + +/// @cond PRIVATE + +void SessionPrivate::startNext() +{ + QTimer::singleShot(0, mParent, [this]() { + doStartNext(); + }); +} + +void SessionPrivate::reconnect() +{ + if (!connection) { + connection = new Connection(Connection::CommandConnection, sessionId, &mCommandBuffer); + sessionThread()->addConnection(connection); + mParent->connect(connection, &Connection::reconnected, mParent, &Session::reconnected, Qt::QueuedConnection); + mParent->connect( + connection, + &Connection::socketDisconnected, + mParent, + [this]() { + socketDisconnected(); + }, + Qt::QueuedConnection); + mParent->connect( + connection, + &Connection::socketError, + mParent, + [this](const QString &error) { + socketError(error); + }, + Qt::QueuedConnection); + } + + connection->reconnect(); +} + +void SessionPrivate::socketError(const QString &error) +{ + qCWarning(AKONADICORE_LOG) << "Socket error occurred:" << error; + socketDisconnected(); +} + +void SessionPrivate::socketDisconnected() +{ + if (currentJob) { + currentJob->d_ptr->lostConnection(); + } + connected = false; +} + +bool SessionPrivate::handleCommands() +{ + CommandBufferLocker lock(&mCommandBuffer); + CommandBufferNotifyBlocker notify(&mCommandBuffer); + while (!mCommandBuffer.isEmpty()) { + const auto command = mCommandBuffer.dequeue(); + lock.unlock(); + const auto cmd = command.command; + const auto tag = command.tag; + + // Handle Hello response -> send Login + if (cmd->type() == Protocol::Command::Hello) { + const auto &hello = Protocol::cmdCast(cmd); + if (hello.isError()) { + qCWarning(AKONADICORE_LOG) << "Error when establishing connection with Akonadi server:" << hello.errorMessage(); + connection->closeConnection(); + QTimer::singleShot(1000, connection, &Connection::reconnect); + return false; + } + + qCDebug(AKONADICORE_LOG) << "Connected to" << hello.serverName() << ", using protocol version" << hello.protocolVersion(); + qCDebug(AKONADICORE_LOG) << "Server generation:" << hello.generation(); + qCDebug(AKONADICORE_LOG) << "Server says:" << hello.message(); + // Version mismatch is handled in SessionPrivate::startJob() so that + // we can report the error out via KJob API + protocolVersion = hello.protocolVersion(); + Internal::setServerProtocolVersion(protocolVersion); + Internal::setGeneration(hello.generation()); + + sendCommand(nextTag(), Protocol::LoginCommandPtr::create(sessionId)); + } else if (cmd->type() == Protocol::Command::Login) { + const auto &login = Protocol::cmdCast(cmd); + if (login.isError()) { + qCWarning(AKONADICORE_LOG) << "Unable to login to Akonadi server:" << login.errorMessage(); + connection->closeConnection(); + QTimer::singleShot(1000, mParent, [this]() { + reconnect(); + }); + return false; + } + + connected = true; + startNext(); + } else if (currentJob) { + currentJob->d_ptr->handleResponse(tag, cmd); + } + + lock.relock(); + } + + return true; +} + +bool SessionPrivate::canPipelineNext() +{ + if (queue.isEmpty() || pipeline.count() >= PIPELINE_LENGTH) { + return false; + } + if (pipeline.isEmpty() && currentJob) { + return currentJob->d_ptr->mWriteFinished; + } + if (!pipeline.isEmpty()) { + return pipeline.last()->d_ptr->mWriteFinished; + } + return false; +} + +void SessionPrivate::doStartNext() +{ + if (!connected || (queue.isEmpty() && pipeline.isEmpty())) { + return; + } + if (canPipelineNext()) { + Akonadi::Job *nextJob = queue.dequeue(); + pipeline.enqueue(nextJob); + startJob(nextJob); + } + if (jobRunning) { + return; + } + jobRunning = true; + if (!pipeline.isEmpty()) { + currentJob = pipeline.dequeue(); + } else { + currentJob = queue.dequeue(); + startJob(currentJob); + } +} + +void SessionPrivate::startJob(Job *job) +{ + if (protocolVersion != Protocol::version()) { + job->setError(Job::ProtocolVersionMismatch); + if (protocolVersion < Protocol::version()) { + job->setErrorText( + i18n("Protocol version mismatch. Server version is older (%1) than ours (%2). " + "If you updated your system recently please restart the Akonadi server.", + protocolVersion, + Protocol::version())); + qCWarning(AKONADICORE_LOG) << "Protocol version mismatch. Server version is older (" << protocolVersion << ") than ours (" << Protocol::version() + << "). " + "If you updated your system recently please restart the Akonadi server."; + } else { + job->setErrorText( + i18n("Protocol version mismatch. Server version is newer (%1) than ours (%2). " + "If you updated your system recently please restart all KDE PIM applications.", + protocolVersion, + Protocol::version())); + qCWarning(AKONADICORE_LOG) << "Protocol version mismatch. Server version is newer (" << protocolVersion << ") than ours (" << Protocol::version() + << "). " + "If you updated your system recently please restart all KDE PIM applications."; + } + job->emitResult(); + } else { + job->d_ptr->startQueued(); + } +} + +void SessionPrivate::endJob(Job *job) +{ + job->emitResult(); +} + +void SessionPrivate::jobDone(KJob *job) +{ + // ### careful, this method can be called from the QObject dtor of job (see jobDestroyed() below) + // so don't call any methods on job itself + if (job == currentJob) { + if (pipeline.isEmpty()) { + jobRunning = false; + currentJob = nullptr; + } else { + currentJob = pipeline.dequeue(); + } + startNext(); + } else { + // non-current job finished, likely canceled while still in the queue + queue.removeAll(static_cast(job)); + // ### likely not enough to really cancel already running jobs + pipeline.removeAll(static_cast(job)); + } +} + +void SessionPrivate::jobWriteFinished(Akonadi::Job *job) +{ + Q_ASSERT((job == currentJob && pipeline.isEmpty()) || (job = pipeline.last())); + Q_UNUSED(job) + + startNext(); +} + +void SessionPrivate::jobDestroyed(QObject *job) +{ + // careful, accessing non-QObject methods of job will fail here already + jobDone(static_cast(job)); +} + +void SessionPrivate::addJob(Job *job) +{ + queue.append(job); + QObject::connect(job, &KJob::result, mParent, [this](KJob *job) { + jobDone(job); + }); + QObject::connect(job, &Job::writeFinished, mParent, [this](Job *job) { + jobWriteFinished(job); + }); + QObject::connect(job, &QObject::destroyed, mParent, [this](QObject *o) { + jobDestroyed(o); + }); + startNext(); +} + +void SessionPrivate::publishOtherJobs(Job *thanThisJob) +{ + int count = 0; + for (const auto &job : std::as_const(queue)) { + if (job != thanThisJob) { + job->d_ptr->publishJob(); + ++count; + } + } + if (count > 0) { + qCDebug(AKONADICORE_LOG) << "published" << count << "pending jobs to the job tracker"; + } + if (currentJob && currentJob != thanThisJob) { + currentJob->d_ptr->signalStartedToJobTracker(); + } +} + +qint64 SessionPrivate::nextTag() +{ + return theNextTag++; +} + +void SessionPrivate::sendCommand(qint64 tag, const Protocol::CommandPtr &command) +{ + connection->sendCommand(tag, command); +} + +void SessionPrivate::serverStateChanged(ServerManager::State state) +{ + if (state == ServerManager::Running && !connected) { + reconnect(); + } else if (!connected && state == ServerManager::Broken) { + // If the server is broken, cancel all pending jobs, otherwise they will be + // blocked forever and applications waiting for them to finish would be stuck + for (Job *job : std::as_const(queue)) { + job->setError(Job::ConnectionFailed); + job->kill(KJob::EmitResult); + } + } else if (state == ServerManager::Stopping) { + sessionThread()->destroyConnection(connection); + connection = nullptr; + } +} + +void SessionPrivate::itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision) +{ + // only deal with the queue, for the guys in the pipeline it's too late already anyway + // and they shouldn't have gotten there if they depend on a preceding job anyway. + for (Job *job : std::as_const(queue)) { + job->d_ptr->updateItemRevision(itemId, oldRevision, newRevision); + } +} + +/// @endcond + +SessionPrivate::SessionPrivate(Session *parent) + : mParent(parent) + , mSessionThread(new SessionThread) + , connection(nullptr) + , protocolVersion(0) + , mCommandBuffer(parent, "handleCommands") + , currentJob(nullptr) +{ + // Shutdown the thread before QApplication event loop quits - the + // thread()->wait() mechanism in Connection dtor crashes sometimes + // when called from QApplication destructor + connThreadCleanUp = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this]() { + delete mSessionThread; + mSessionThread = nullptr; + }); +} + +SessionPrivate::~SessionPrivate() +{ + QObject::disconnect(connThreadCleanUp); + delete mSessionThread; +} + +void SessionPrivate::init(const QByteArray &id) +{ + if (!id.isEmpty()) { + sessionId = id; + } else { + sessionId = QCoreApplication::instance()->applicationName().toUtf8() + '-' + QByteArray::number(QRandomGenerator::global()->generate()); + } + + qCDebug(AKONADICORE_LOG) << "Initializing session with ID" << id; + + connected = false; + theNextTag = 2; + jobRunning = false; + + if (ServerManager::state() == ServerManager::NotRunning) { + ServerManager::start(); + } + QObject::connect(ServerManager::self(), &ServerManager::stateChanged, mParent, [this](ServerManager::State state) { + serverStateChanged(state); + }); + reconnect(); +} + +void SessionPrivate::forceReconnect() +{ + jobRunning = false; + connected = false; + if (connection) { + connection->forceReconnect(); + } + QMetaObject::invokeMethod( + mParent, + [this]() { + reconnect(); + }, + Qt::QueuedConnection); +} + +Session::Session(const QByteArray &sessionId, QObject *parent) + : QObject(parent) + , d(new SessionPrivate(this)) +{ + d->init(sessionId); +} + +Session::Session(SessionPrivate *dd, const QByteArray &sessionId, QObject *parent) + : QObject(parent) + , d(dd) +{ + d->mParent = this; + d->init(sessionId); +} + +Session::~Session() +{ + d->clear(false); + delete d; +} + +QByteArray Session::sessionId() const +{ + return d->sessionId; +} + +Q_GLOBAL_STATIC(QThreadStorage>, instances) // NOLINT(readability-redundant-member-init) + +void SessionPrivate::createDefaultSession(const QByteArray &sessionId) +{ + Q_ASSERT_X(!sessionId.isEmpty(), "SessionPrivate::createDefaultSession", "You tried to create a default session with empty session id!"); + Q_ASSERT_X(!instances()->hasLocalData(), "SessionPrivate::createDefaultSession", "You tried to create a default session twice!"); + + auto session = new Session(sessionId); + setDefaultSession(session); +} + +void SessionPrivate::setDefaultSession(Session *session) +{ + instances()->setLocalData({session}); + QObject::connect(qApp, &QCoreApplication::aboutToQuit, []() { + instances()->setLocalData({}); + }); +} + +Session *Session::defaultSession() +{ + if (!instances()->hasLocalData()) { + auto session = new Session(); + SessionPrivate::setDefaultSession(session); + } + return instances()->localData().data(); +} + +void Session::clear() +{ + d->clear(true); +} + +void SessionPrivate::clear(bool forceReconnect) +{ + for (Job *job : std::as_const(queue)) { + job->kill(KJob::EmitResult); // safe, not started yet + } + queue.clear(); + for (Job *job : std::as_const(pipeline)) { + job->d_ptr->mStarted = false; // avoid killing/reconnect loops + job->kill(KJob::EmitResult); + } + pipeline.clear(); + if (currentJob) { + currentJob->d_ptr->mStarted = false; // avoid killing/reconnect loops + currentJob->kill(KJob::EmitResult); + } + + if (forceReconnect) { + this->forceReconnect(); + } +} + +#include "moc_session.cpp" diff --git a/src/core/session.h b/src/core/session.h new file mode 100644 index 0000000..aab7bd8 --- /dev/null +++ b/src/core/session.h @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include + +class KJob; +class FakeSession; +class FakeNotificationConnection; + +namespace Akonadi +{ +namespace Protocol +{ +class Command; +using CommandPtr = QSharedPointer; +} + +class Job; +class SessionPrivate; +class ChangeNotificationDependenciesFactory; + +/** + * @short A communication session with the Akonadi storage. + * + * Every Job object has to be associated with a Session. + * The session is responsible of scheduling its jobs. + * For now only a simple serial execution is implemented (the IMAP-like + * protocol to communicate with the storage backend is capable of parallel + * execution on a single session though). + * + * @code + * + * using namespace Akonadi; + * + * Session *session = new Session( "mySession" ); + * + * CollectionFetchJob *job = new CollectionFetchJob( Collection::root(), + * CollectionFetchJob::Recursive, + * session ); + * + * connect( job, SIGNAL(result(KJob*)), this, SLOT(slotResult(KJob*)) ); + * + * @endcode + * + * @author Volker Krause + */ +class AKONADICORE_EXPORT Session : public QObject +{ + Q_OBJECT + + friend class Job; + friend class JobPrivate; + friend class SessionPrivate; + +public: + /** + * Creates a new session. + * + * @param sessionId The identifier for this session, will be a + * random value if empty. + * @param parent The parent object. + * + * @see defaultSession() + */ + explicit Session(const QByteArray &sessionId = QByteArray(), QObject *parent = nullptr); + + /** + * Destroys the session. + */ + ~Session(); + + /** + * Returns the session identifier. + */ + Q_REQUIRED_RESULT QByteArray sessionId() const; + + /** + * Returns the default session for this thread. + */ + static Session *defaultSession(); + + /** + * Stops all jobs queued for execution. + */ + void clear(); + +Q_SIGNALS: + /** + * This signal is emitted whenever the session has been reconnected + * to the server (e.g. after a server crash). + * + * @since 4.6 + */ + void reconnected(); + +protected: + /** + * Creates a new session with shared private object. + * + * @param d The private object. + * @param sessionId The identifier for this session, will be a + * random value if empty. + * @param parent The parent object. + * + * @note This constructor is needed for unit testing only. + */ + explicit Session(SessionPrivate *d, const QByteArray &sessionId = QByteArray(), QObject *parent = nullptr); + +private: + /// @cond PRIVATE + SessionPrivate *const d; + friend class ::FakeSession; + friend class ::FakeNotificationConnection; + friend class ChangeNotificationDependenciesFactory; + + Q_PRIVATE_SLOT(d, bool handleCommands()) + /// @endcond +}; + +} + diff --git a/src/core/session_p.h b/src/core/session_p.h new file mode 100644 index 0000000..8631c17 --- /dev/null +++ b/src/core/session_p.h @@ -0,0 +1,137 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "commandbuffer_p.h" +#include "item.h" +#include "servermanager.h" +#include "session.h" + +#include +#include +#include +#include + +namespace Akonadi +{ +class SessionThread; +class Connection; + +namespace Protocol +{ +class Command; +} + +/** + * @internal + */ +class AKONADICORE_EXPORT SessionPrivate +{ +public: + explicit SessionPrivate(Session *parent); + + virtual ~SessionPrivate(); + + virtual void init(const QByteArray &sessionId); + + SessionThread *sessionThread() const + { + return mSessionThread; + } + + void enqueueCommand(qint64 tag, const Protocol::CommandPtr &cmd); + + void startNext(); + /// Disconnects a previously existing connection and tries to reconnect + void forceReconnect(); + /// Attempts to establish a connections to the Akonadi server. + virtual void reconnect(); + void serverStateChanged(ServerManager::State); + void socketDisconnected(); + void socketError(const QString &error); + void dataReceived(); + virtual bool handleCommands(); + void doStartNext(); + void startJob(Job *job); + + /** + @internal For testing purposes only. See FakeSesson. + @param job the job to end + */ + void endJob(Job *job); + + void jobDone(KJob *job); + void jobWriteFinished(Akonadi::Job *job); + void jobDestroyed(QObject *job); + + bool canPipelineNext(); + + /** + * Creates a new default session for this thread with + * the given @p sessionId. The session can be accessed + * later by defaultSession(). + * + * You only need to call this method if you want that the + * default session has a special custom id, otherwise a random unique + * id is used automatically. + * @param sessionId the id of new default session + */ + static void createDefaultSession(const QByteArray &sessionId); + + /** + * Sets the default session. + * @internal Only for unit tests. + */ + static void setDefaultSession(Session *session); + + /** + Associates the given Job object with this session. + */ + virtual void addJob(Job *job); + + /** + Returns the next IMAP tag. + */ + qint64 nextTag(); + + /** + Sends the given command to server + */ + void sendCommand(qint64 tag, const Protocol::CommandPtr &command); + + /** + * Propagate item revision changes to following jobs. + */ + void itemRevisionChanged(Akonadi::Item::Id itemId, int oldRevision, int newRevision); + + void clear(bool forceReconnect); + + void publishOtherJobs(Job *thanThisJob); + + Session *mParent = nullptr; + SessionThread *mSessionThread = nullptr; + Connection *connection = nullptr; + QMetaObject::Connection connThreadCleanUp; + QByteArray sessionId; + bool connected; + qint64 theNextTag; + int protocolVersion; + + CommandBuffer mCommandBuffer; + + // job management + QQueue queue; + QQueue pipeline; + Job *currentJob = nullptr; + bool jobRunning; + + QFile *logFile = nullptr; +}; + +} + diff --git a/src/core/sessionthread.cpp b/src/core/sessionthread.cpp new file mode 100644 index 0000000..5b05b94 --- /dev/null +++ b/src/core/sessionthread.cpp @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadicore_debug.h" +#include "session_p.h" +#include "sessionthread_p.h" + +#include +#include + +Q_DECLARE_METATYPE(Akonadi::Connection::ConnectionType) +Q_DECLARE_METATYPE(Akonadi::Connection *) +Q_DECLARE_METATYPE(Akonadi::CommandBuffer *) + +using namespace Akonadi; + +SessionThread::SessionThread(QObject *parent) + : QObject(parent) +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + auto thread = new QThread(); + thread->setObjectName(QStringLiteral("SessionThread")); + moveToThread(thread); + thread->start(); +} + +SessionThread::~SessionThread() +{ + QMetaObject::invokeMethod(this, &SessionThread::doThreadQuit, Qt::QueuedConnection); + if (!thread()->wait(10 * 1000)) { + thread()->terminate(); + // Make sure to wait until it's done, otherwise it can crash when the pthread callback is called + thread()->wait(); + } + delete thread(); +} + +void SessionThread::addConnection(Connection *connection) +{ + connection->moveToThread(thread()); + const bool invoke = QMetaObject::invokeMethod(this, "doAddConnection", Qt::BlockingQueuedConnection, Q_ARG(Akonadi::Connection *, connection)); + Q_ASSERT(invoke); + Q_UNUSED(invoke) +} + +void SessionThread::doAddConnection(Connection *connection) +{ + Q_ASSERT(thread() == QThread::currentThread()); + Q_ASSERT(!mConnections.contains(connection)); + + connect(connection, &QObject::destroyed, this, [this](QObject *obj) { + mConnections.removeOne(static_cast(obj)); + }); + mConnections.push_back(connection); +} + +void SessionThread::destroyConnection(Connection *connection) +{ + if (QCoreApplication::closingDown()) { + return; + } + + const bool invoke = QMetaObject::invokeMethod(this, "doDestroyConnection", Qt::BlockingQueuedConnection, Q_ARG(Akonadi::Connection *, connection)); + Q_ASSERT(invoke); + Q_UNUSED(invoke) +} + +void SessionThread::doDestroyConnection(Connection *connection) +{ + Q_ASSERT(thread() == QThread::currentThread()); + Q_ASSERT(mConnections.contains(connection)); + + connection->disconnect(this); + connection->doCloseConnection(); + mConnections.removeAll(connection); + delete connection; +} + +void SessionThread::doThreadQuit() +{ + Q_ASSERT(thread() == QThread::currentThread()); + + for (Connection *conn : std::as_const(mConnections)) { + conn->disconnect(this); + conn->doCloseConnection(); // we can call directly because we are in the correct thread + delete conn; + } + + thread()->quit(); +} diff --git a/src/core/sessionthread_p.h b/src/core/sessionthread_p.h new file mode 100644 index 0000000..8e1b46d --- /dev/null +++ b/src/core/sessionthread_p.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "connection_p.h" + +namespace Akonadi +{ +class SessionThread : public QObject +{ + Q_OBJECT + +public: + explicit SessionThread(QObject *parent = nullptr); + ~SessionThread(); + + void addConnection(Connection *connection); + void destroyConnection(Connection *connection); + +private Q_SLOTS: + void doDestroyConnection(Akonadi::Connection *connection); + void doAddConnection(Akonadi::Connection *connection); + + void doThreadQuit(); + +private: + QVector mConnections; +}; + +} + diff --git a/src/core/sharedvaluepool_p.h b/src/core/sharedvaluepool_p.h new file mode 100644 index 0000000..fdeec89 --- /dev/null +++ b/src/core/sharedvaluepool_p.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Internal +{ +/** + * Pool of implicitly shared values, use for optimizing memory use + * when having a large amount of copies from a small set of different values. + */ +template class Container> class SharedValuePool +{ +public: + /** Returns the shared value equal to @p value .*/ + T sharedValue(const T &value) + { + // for small pool sizes this is actually faster than using lower_bound and a sorted vector + typename Container::const_iterator it = std::find(m_pool.constBegin(), m_pool.constEnd(), value); + if (it != m_pool.constEnd()) { + return *it; + } + m_pool.push_back(value); + return value; + } + +private: + Container m_pool; +}; + +} +} + diff --git a/src/core/specialcollections.cpp b/src/core/specialcollections.cpp new file mode 100644 index 0000000..49f8a71 --- /dev/null +++ b/src/core/specialcollections.cpp @@ -0,0 +1,269 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "specialcollections.h" +#include "akonadicore_debug.h" +#include "specialcollectionattribute.h" +#include "specialcollections_p.h" + +#include "agentinstance.h" +#include "agentmanager.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "collectionmodifyjob.h" +#include "monitor.h" + +#include + +#include + +using namespace Akonadi; + +SpecialCollectionsPrivate::SpecialCollectionsPrivate(KCoreConfigSkeleton *settings, SpecialCollections *qq) + : q(qq) + , mSettings(settings) + , mBatchMode(false) +{ + mMonitor = new Monitor(q); + mMonitor->setObjectName(QStringLiteral("SpecialCollectionsMonitor")); + mMonitor->fetchCollectionStatistics(true); + + /// In order to know if items are added or deleted + /// from one of our specialcollection folders, + /// we have to watch all mail item add/move/delete notifications + /// and check for the parent to see if it is one we care about + QObject::connect(mMonitor, &Monitor::collectionRemoved, q, [this](const Akonadi::Collection &col) { + collectionRemoved(col); + }); + QObject::connect(mMonitor, &Monitor::collectionStatisticsChanged, q, [this](Akonadi::Collection::Id id, const Akonadi::CollectionStatistics &statistics) { + collectionStatisticsChanged(id, statistics); + }); +} + +SpecialCollectionsPrivate::~SpecialCollectionsPrivate() +{ +} + +QString SpecialCollectionsPrivate::defaultResourceId() const +{ + if (mDefaultResourceId.isEmpty()) { + mSettings->load(); + const KConfigSkeletonItem *item = mSettings->findItem(QStringLiteral("DefaultResourceId")); + Q_ASSERT(item); + + mDefaultResourceId = item->property().toString(); + } + return mDefaultResourceId; +} + +void SpecialCollectionsPrivate::emitChanged(const QString &resourceId) +{ + if (mBatchMode) { + mToEmitChangedFor.insert(resourceId); + } else { + qCDebug(AKONADICORE_LOG) << "Emitting changed for" << resourceId; + const AgentInstance agentInstance = AgentManager::self()->instance(resourceId); + Q_EMIT q->collectionsChanged(agentInstance); + // first compare with local value then with config value (which also updates the local value) + if (resourceId == mDefaultResourceId || resourceId == defaultResourceId()) { + qCDebug(AKONADICORE_LOG) << "Emitting defaultFoldersChanged."; + Q_EMIT q->defaultCollectionsChanged(); + } + } +} + +void SpecialCollectionsPrivate::collectionRemoved(const Collection &collection) +{ + qCDebug(AKONADICORE_LOG) << "Collection" << collection.id() << "resource" << collection.resource(); + if (mFoldersForResource.contains(collection.resource())) { + // Retrieve the list of special folders for the resource the collection belongs to + QHash &folders = mFoldersForResource[collection.resource()]; + { + QMutableHashIterator it(folders); + while (it.hasNext()) { + it.next(); + if (it.value() == collection) { + // The collection to be removed is a special folder + it.remove(); + emitChanged(collection.resource()); + } + } + } + + if (folders.isEmpty()) { + // This resource has no more folders, so remove it completely. + mFoldersForResource.remove(collection.resource()); + } + } +} + +void SpecialCollectionsPrivate::collectionStatisticsChanged(Akonadi::Collection::Id collectionId, const Akonadi::CollectionStatistics &statistics) +{ + // need to get the name of the collection in order to be able to check if we are storing it, + // but we have the id from the monitor, so fetch the name. + auto fetchJob = new Akonadi::CollectionFetchJob(Collection(collectionId), Akonadi::CollectionFetchJob::Base); + fetchJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::None); + fetchJob->setProperty("statistics", QVariant::fromValue(statistics)); + + q->connect(fetchJob, &CollectionFetchJob::result, q, [this](KJob *job) { + collectionFetchJobFinished(job); + }); +} + +void SpecialCollectionsPrivate::collectionFetchJobFinished(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error fetching collection to get name from id for statistics updating in specialcollections!"; + return; + } + + const Akonadi::CollectionFetchJob *fetchJob = qobject_cast(job); + + Q_ASSERT(!fetchJob->collections().empty()); + const Akonadi::Collection collection = fetchJob->collections().at(0); + const auto statistics = fetchJob->property("statistics").value(); + + mFoldersForResource[collection.resource()][collection.name().toUtf8()].setStatistics(statistics); +} + +void SpecialCollectionsPrivate::beginBatchRegister() +{ + Q_ASSERT(!mBatchMode); + mBatchMode = true; + Q_ASSERT(mToEmitChangedFor.isEmpty()); +} + +void SpecialCollectionsPrivate::endBatchRegister() +{ + Q_ASSERT(mBatchMode); + mBatchMode = false; + + for (const QString &resourceId : std::as_const(mToEmitChangedFor)) { + emitChanged(resourceId); + } + + mToEmitChangedFor.clear(); +} + +void SpecialCollectionsPrivate::forgetFoldersForResource(const QString &resourceId) +{ + if (mFoldersForResource.contains(resourceId)) { + const auto folders = mFoldersForResource[resourceId]; + for (const auto &collection : folders) { + mMonitor->setCollectionMonitored(collection, false); + } + + mFoldersForResource.remove(resourceId); + emitChanged(resourceId); + } +} + +AgentInstance SpecialCollectionsPrivate::defaultResource() const +{ + const QString identifier = defaultResourceId(); + return AgentManager::self()->instance(identifier); +} + +SpecialCollections::SpecialCollections(KCoreConfigSkeleton *settings, QObject *parent) + : QObject(parent) + , d(new SpecialCollectionsPrivate(settings, this)) +{ +} + +SpecialCollections::~SpecialCollections() +{ + delete d; +} + +bool SpecialCollections::hasCollection(const QByteArray &type, const AgentInstance &instance) const +{ + return d->mFoldersForResource.value(instance.identifier()).contains(type); +} + +Akonadi::Collection SpecialCollections::collection(const QByteArray &type, const AgentInstance &instance) const +{ + return d->mFoldersForResource.value(instance.identifier()).value(type); +} + +void SpecialCollections::setSpecialCollectionType(const QByteArray &type, const Akonadi::Collection &collection) +{ + if (!collection.hasAttribute() || collection.attribute()->collectionType() != type) { + Collection attributeCollection(collection); + auto attribute = attributeCollection.attribute(Collection::AddIfMissing); + attribute->setCollectionType(type); + new CollectionModifyJob(attributeCollection); + } +} + +void SpecialCollections::unsetSpecialCollection(const Akonadi::Collection &collection) +{ + if (collection.hasAttribute()) { + Collection attributeCollection(collection); + attributeCollection.removeAttribute(); + new CollectionModifyJob(attributeCollection); + } +} + +bool SpecialCollections::unregisterCollection(const Collection &collection) +{ + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid collection."; + return false; + } + + const QString &resourceId = collection.resource(); + if (resourceId.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Collection has empty resourceId."; + return false; + } + + unsetSpecialCollection(collection); + + d->mMonitor->setCollectionMonitored(collection, false); + // Remove from list of collection + d->collectionRemoved(collection); + return true; +} + +bool SpecialCollections::registerCollection(const QByteArray &type, const Collection &collection) +{ + if (!collection.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid collection."; + return false; + } + + const QString &resourceId = collection.resource(); + if (resourceId.isEmpty()) { + qCWarning(AKONADICORE_LOG) << "Collection has empty resourceId."; + return false; + } + + setSpecialCollectionType(type, collection); + + const Collection oldCollection = d->mFoldersForResource.value(resourceId).value(type); + if (oldCollection != collection) { + if (oldCollection.isValid()) { + d->mMonitor->setCollectionMonitored(oldCollection, false); + } + d->mMonitor->setCollectionMonitored(collection, true); + d->mFoldersForResource[resourceId].insert(type, collection); + d->emitChanged(resourceId); + } + + return true; +} + +bool SpecialCollections::hasDefaultCollection(const QByteArray &type) const +{ + return hasCollection(type, d->defaultResource()); +} + +Akonadi::Collection SpecialCollections::defaultCollection(const QByteArray &type) const +{ + return collection(type, d->defaultResource()); +} + +#include "moc_specialcollections.cpp" diff --git a/src/core/specialcollections.h b/src/core/specialcollections.h new file mode 100644 index 0000000..96e1317 --- /dev/null +++ b/src/core/specialcollections.h @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" +#include "item.h" + +#include + +class KCoreConfigSkeleton; + +namespace Akonadi +{ +class AgentInstance; +class SpecialCollectionsPrivate; + +/** + @short An interface to special collections. + + This class is the central interface to special collections like inbox or + outbox in a mail resource or recent contacts in a contacts resource. + The class is not meant to be used directly, but to inherit the a type + specific special collections class from it (e.g. SpecialMailCollections). + + To check whether a special collection is available, simply use the hasCollection() and + hasDefaultCollection() methods. Available special collections are accessible through + the collection() and defaultCollection() methods. + + To create a special collection, use a SpecialCollectionsRequestJob. + This will create the special collections you request and automatically + register them with SpecialCollections, so that it now knows they are available. + + This class monitors all special collections known to it, and removes it + from the known list if they are deleted. Note that this class does not + automatically rebuild the collections that disappeared. + + The defaultCollectionsChanged() and collectionsChanged() signals are emitted when + the special collections for a resource change (i.e. some became available or some + become unavailable). + + @author Constantin Berzan + @since 4.4 +*/ +class AKONADICORE_EXPORT SpecialCollections : public QObject +{ + Q_OBJECT + +public: + /** + * Destroys the special collections object. + */ + ~SpecialCollections(); + + /** + * Returns whether the given agent @p instance has a special collection of + * the given @p type. + */ + Q_REQUIRED_RESULT bool hasCollection(const QByteArray &type, const AgentInstance &instance) const; + + /** + * Returns the special collection of the given @p type in the given agent + * @p instance, or an invalid collection if such a collection is unknown. + */ + Q_REQUIRED_RESULT Akonadi::Collection collection(const QByteArray &type, const AgentInstance &instance) const; + + /** + * Registers the given @p collection as a special collection + * with the given @p type. + * @param type the special type of @c collection + * @param collection the given collection to register + * The collection must be owned by a valid resource. + * Registering a new collection of a previously registered type forgets the + * old collection. + */ + bool registerCollection(const QByteArray &type, const Akonadi::Collection &collection); + + /** + * Unregisters the given @p collection as a special collection. + * @param type the special type of @c collection + * @since 4.12 + */ + bool unregisterCollection(const Collection &collection); + + /** + * unsets the special collection attribute which marks @p collection as being a special + * collection. + * @since 4.12 + */ + static void unsetSpecialCollection(const Akonadi::Collection &collection); + + /** + * Sets the special collection attribute which marks @p collection as being a special + * collection of type @p type. + * This is typically used by configuration dialog for resources, when the user can choose + * a specific special collection (ex: IMAP trash). + * + * @since 4.11 + */ + static void setSpecialCollectionType(const QByteArray &type, const Akonadi::Collection &collection); + + /** + * Returns whether the default resource has a special collection of + * the given @p type. + */ + Q_REQUIRED_RESULT bool hasDefaultCollection(const QByteArray &type) const; + + /** + * Returns the special collection of given @p type in the default + * resource, or an invalid collection if such a collection is unknown. + */ + Q_REQUIRED_RESULT Akonadi::Collection defaultCollection(const QByteArray &type) const; + +Q_SIGNALS: + /** + * Emitted when the special collections for a resource have been changed + * (for example, some become available, or some become unavailable). + * + * @param instance The instance of the resource the collection belongs to. + */ + void collectionsChanged(const Akonadi::AgentInstance &instance); + + /** + * Emitted when the special collections for the default resource have + * been changed (for example, some become available, or some become unavailable). + */ + void defaultCollectionsChanged(); + +protected: + /** + * Creates a new special collections object. + * + * @param config The configuration skeleton that provides the default resource id. + * @param parent The parent object. + */ + explicit SpecialCollections(KCoreConfigSkeleton *config, QObject *parent = nullptr); + +private: + /// @cond PRIVATE + friend class SpecialCollectionsRequestJob; + friend class SpecialCollectionsRequestJobPrivate; + friend class SpecialCollectionsPrivate; +#ifdef BUILD_TESTING + friend class SpecialMailCollectionsTesting; + friend class LocalFoldersTest; +#endif + + SpecialCollectionsPrivate *const d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/core/specialcollections_p.h b/src/core/specialcollections_p.h new file mode 100644 index 0000000..fd8710b --- /dev/null +++ b/src/core/specialcollections_p.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2009 Constantin Berzan + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "akonaditests_export.h" + +#include "collection.h" +#include "collectionstatistics.h" +#include "item.h" + +class KCoreConfigSkeleton; +class KJob; + +namespace Akonadi +{ +class AgentInstance; +class SpecialCollections; +class Monitor; + +/** + @internal +*/ +class AKONADI_TESTS_EXPORT SpecialCollectionsPrivate +{ +public: + SpecialCollectionsPrivate(KCoreConfigSkeleton *settings, SpecialCollections *qq); + ~SpecialCollectionsPrivate(); + + QString defaultResourceId() const; + void emitChanged(const QString &resourceId); + void collectionRemoved(const Collection &collection); // slot + void collectionFetchJobFinished(KJob *job); // slot + void collectionStatisticsChanged(Akonadi::Collection::Id collectionId, + const Akonadi::CollectionStatistics &statistics); // slot + + /** + Forgets all folders owned by the given resource. + This method is used by SpecialCollectionsRequestJob. + @param resourceId the identifier of the resource for which to forget folders + */ + void forgetFoldersForResource(const QString &resourceId); + + /** + Avoids emitting the foldersChanged() signal until endBatchRegister() + is called. This is used to avoid emitting repeated signals when multiple + folders are registered in a row. + This method is used by SpecialCollectionsRequestJob. + */ + void beginBatchRegister(); + + /** + @see beginBatchRegister() + This method is used by SpecialCollectionsRequestJob. + */ + void endBatchRegister(); + + AgentInstance defaultResource() const; + + SpecialCollections *const q; + KCoreConfigSkeleton *mSettings = nullptr; + QHash> mFoldersForResource; + bool mBatchMode; + QSet mToEmitChangedFor; + Monitor *mMonitor = nullptr; + + mutable QString mDefaultResourceId; +}; + +} // namespace Akonadi + diff --git a/src/core/supertrait.h b/src/core/supertrait.h new file mode 100644 index 0000000..1d4b699 --- /dev/null +++ b/src/core/supertrait.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +namespace Akonadi +{ +namespace Internal +{ +template struct check_type { + using type = void; +}; +} + +/** + @internal + @see SuperClass +*/ +template struct SuperClassTrait { + using Type = Super; +}; + +template struct SuperClassTrait::type> { + using Type = typename Class::SuperClass; +}; + +/** + Type trait to provide information about a base class for a given class. + Used eg. for the Akonadi payload mechanism. + + To provide base class introspection for own types, extend this trait as follows: + @code + namespace Akonadi + { + template <> struct SuperClass : public SuperClassTrait{}; + } + @endcode + + Alternatively, define a typedef "SuperClass" in your type, pointing to the base class. + This avoids having to include this header file if that's inconvenient from a dependency + point of view. +*/ +template struct SuperClass : public SuperClassTrait { +}; +} + diff --git a/src/core/tag.cpp b/src/core/tag.cpp new file mode 100644 index 0000000..ffa4f92 --- /dev/null +++ b/src/core/tag.cpp @@ -0,0 +1,240 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tag.h" +#include "akonadicore_debug.h" +#include "tag_p.h" + +#include "tagattribute.h" +#include +#include + +using namespace Akonadi; + +const char Akonadi::Tag::PLAIN[] = "PLAIN"; +const char Akonadi::Tag::GENERIC[] = "GENERIC"; + +uint Akonadi::qHash(const Tag &tag) +{ + return ::qHash(tag.id()); +} + +Tag::Tag() + : d_ptr(new TagPrivate) +{ +} + +Tag::Tag(Tag::Id id) + : d_ptr(new TagPrivate) +{ + d_ptr->id = id; +} + +Tag::Tag(const QString &name) + : d_ptr(new TagPrivate) +{ + d_ptr->gid = name.toUtf8(); + d_ptr->type = PLAIN; +} + +Tag::Tag(const Tag &) = default; +Tag::Tag(Tag &&) noexcept = default; +Tag::~Tag() = default; + +Tag &Tag::operator=(const Tag &) = default; +Tag &Tag::operator=(Tag &&) noexcept = default; + +bool Tag::operator==(const Tag &other) const +{ + // Valid tags are equal if their IDs are equal + if (isValid() && other.isValid()) { + return d_ptr->id == other.d_ptr->id; + } + + // Invalid tags are equal if their GIDs are non empty but equal + if (!d_ptr->gid.isEmpty() || !other.d_ptr->gid.isEmpty()) { + return d_ptr->gid == other.d_ptr->gid; + } + + // Invalid tags are equal if both are invalid + return !isValid() && !other.isValid(); +} + +bool Tag::operator!=(const Tag &other) const +{ + return !operator==(other); +} + +Tag Tag::fromUrl(const QUrl &url) +{ + if (url.scheme() != QLatin1String("akonadi")) { + return Tag(); + } + + const QString tagStr = QUrlQuery(url).queryItemValue(QStringLiteral("tag")); + bool ok = false; + Tag::Id itemId = tagStr.toLongLong(&ok); + if (!ok) { + return Tag(); + } + + return Tag(itemId); +} + +QUrl Tag::url() const +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("tag"), QString::number(id())); + + QUrl url; + url.setScheme(QStringLiteral("akonadi")); + url.setQuery(query); + return url; +} + +void Tag::addAttribute(Attribute *attr) +{ + d_ptr->mAttributeStorage.addAttribute(attr); +} + +void Tag::removeAttribute(const QByteArray &type) +{ + d_ptr->mAttributeStorage.removeAttribute(type); +} + +bool Tag::hasAttribute(const QByteArray &type) const +{ + return d_ptr->mAttributeStorage.hasAttribute(type); +} + +Attribute::List Tag::attributes() const +{ + return d_ptr->mAttributeStorage.attributes(); +} + +void Tag::clearAttributes() +{ + d_ptr->mAttributeStorage.clearAttributes(); +} + +const Attribute *Tag::attribute(const QByteArray &type) const +{ + return d_ptr->mAttributeStorage.attribute(type); +} + +Attribute *Tag::attribute(const QByteArray &type) +{ + markAttributeModified(type); + return d_ptr->mAttributeStorage.attribute(type); +} + +void Tag::setId(Tag::Id identifier) +{ + d_ptr->id = identifier; +} + +Tag::Id Tag::id() const +{ + return d_ptr->id; +} + +void Tag::setGid(const QByteArray &gid) +{ + d_ptr->gid = gid; +} + +QByteArray Tag::gid() const +{ + return d_ptr->gid; +} + +void Tag::setRemoteId(const QByteArray &remoteId) +{ + d_ptr->remoteId = remoteId; +} + +QByteArray Tag::remoteId() const +{ + return d_ptr->remoteId; +} + +void Tag::setName(const QString &name) +{ + if (!name.isEmpty()) { + auto *const attr = attribute(Tag::AddIfMissing); + attr->setDisplayName(name); + } +} + +QString Tag::name() const +{ + const auto *const attr = attribute(); + const QString displayName = attr ? attr->displayName() : QString(); + return !displayName.isEmpty() ? displayName : QString::fromUtf8(d_ptr->gid); +} + +void Tag::setParent(const Tag &parent) +{ + d_ptr->parent.reset(new Tag(parent)); +} + +Tag Tag::parent() const +{ + if (!d_ptr->parent) { + return Tag(); + } + return *d_ptr->parent; +} + +void Tag::setType(const QByteArray &type) +{ + d_ptr->type = type; +} + +QByteArray Tag::type() const +{ + return d_ptr->type; +} + +bool Tag::isValid() const +{ + return d_ptr->id >= 0; +} + +bool Tag::isImmutable() const +{ + return (d_ptr->type.isEmpty() || d_ptr->type == PLAIN); +} + +QDebug operator<<(QDebug debug, const Tag &tag) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "Akonadi::Tag(ID " << tag.id() << ", GID " << tag.gid() << ", parent tag ID " << tag.parent().id() << ")"; + return debug; +} + +Tag Tag::genericTag(const QString &name) +{ + Tag tag; + tag.d_ptr->type = GENERIC; + tag.d_ptr->gid = QUuid::createUuid().toByteArray().mid(1, 36); + tag.setName(name); + return tag; +} + +bool Tag::checkAttribute(const Attribute *attr, const QByteArray &type) const +{ + if (attr) { + return true; + } + qCWarning(AKONADICORE_LOG) << "Found attribute of unknown type" << type << ". Did you forget to call AttributeFactory::registerAttribute()?"; + return false; +} + +void Tag::markAttributeModified(const QByteArray &type) +{ + d_ptr->mAttributeStorage.markAttributeModified(type); +} diff --git a/src/core/tag.h b/src/core/tag.h new file mode 100644 index 0000000..c5bfce7 --- /dev/null +++ b/src/core/tag.h @@ -0,0 +1,252 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "attribute.h" + +#include +#include +#include +#include + +namespace Akonadi +{ +class TagModifyJob; +class TagPrivate; + +/** + * An Akonadi Tag. + */ +class AKONADICORE_EXPORT Tag +{ +public: + using List = QVector; + using Id = qint64; + + /** + * The PLAIN type has the following properties: + * * gid == displayName + * * immutable + * * no hierarchy (no parent) + * + * PLAIN tags are general purpose tags that are easy to map by backends. + */ + static const char PLAIN[]; + + /** + * The GENERIC type has the following properties: + * * mutable + * * gid is RFC 4122 compatible + * * no hierarchy (no parent) + * + * GENERIC tags are general purpose tags, that are used, if you can change tag name. + */ + static const char GENERIC[]; + + Tag(); + explicit Tag(Id id); + /** + * Creates a PLAIN tag + */ + explicit Tag(const QString &name); + + Tag(const Tag &); + + Tag(Tag &&) noexcept; + + ~Tag(); + + Tag &operator=(const Tag &); + Tag &operator=(Tag &&) noexcept; + // Avoid slicing + bool operator==(const Tag &) const; + bool operator!=(const Tag &) const; + + static Tag fromUrl(const QUrl &url); + + /** + * Adds an attribute to the entity. + * + * If an attribute of the same type name already exists, it is deleted and + * replaced with the new one. + * + * @param attribute The new attribute. + * + * @note The entity takes the ownership of the attribute. + */ + void addAttribute(Attribute *attribute); + + /** + * Removes and deletes the attribute of the given type @p name. + */ + void removeAttribute(const QByteArray &name); + + /** + * Returns @c true if the entity has an attribute of the given type @p name, + * false otherwise. + */ + bool hasAttribute(const QByteArray &name) const; + + /** + * Returns a list of all attributes of the entity. + */ + Attribute::List attributes() const; + + /** + * Removes and deletes all attributes of the entity. + */ + void clearAttributes(); + + /** + * Returns the attribute of the given type @p name if available, 0 otherwise. + */ + const Attribute *attribute(const QByteArray &name) const; + Attribute *attribute(const QByteArray &name); + + /** + * Describes the options that can be passed to access attributes. + */ + enum CreateOption { + AddIfMissing, ///< Creates the attribute if it is missing + DontCreate ///< Does not create an attribute if it is missing (default) + }; + + /** + * Returns the attribute of the requested type. + * If the entity has no attribute of that type yet, a new one + * is created and added to the entity. + * + * @param option The create options. + */ + template inline T *attribute(CreateOption option = DontCreate); + + /** + * Returns the attribute of the requested type or 0 if it is not available. + */ + template inline const T *attribute() const; + + /** + * Removes and deletes the attribute of the requested type. + */ + template inline void removeAttribute(); + + /** + * Returns whether the entity has an attribute of the requested type. + */ + template inline bool hasAttribute() const; + + /** + * Returns the url of the tag. + */ + QUrl url() const; + + /** + * Sets the unique @p identifier of the tag. + */ + void setId(Id identifier); + + /** + * Returns the unique identifier of the tag. + */ + Id id() const; + + void setGid(const QByteArray &gid); + QByteArray gid() const; + + void setRemoteId(const QByteArray &remoteId); + QByteArray remoteId() const; + + void setType(const QByteArray &type); + QByteArray type() const; + + void setName(const QString &name); + QString name() const; + + void setParent(const Tag &parent); + Tag parent() const; + + bool isValid() const; + + /** + * Returns true if the tag is immutable (cannot be modified after creation). + * Note that the immutability does not affect the attributes. + */ + bool isImmutable() const; + + /** + * Returns a GENERIC tag with the given name and a valid gid + */ + static Tag genericTag(const QString &name); + +private: + bool checkAttribute(const Attribute *attr, const QByteArray &type) const; + void markAttributeModified(const QByteArray &type); + + /// @cond PRIVATE + friend class TagModifyJob; + friend class TagFetchJob; + friend class ProtocolHelper; + + QSharedDataPointer d_ptr; + /// @endcond +}; + +AKONADICORE_EXPORT uint qHash(const Akonadi::Tag &); + +template inline T *Tag::attribute(CreateOption option) +{ + const QByteArray type = T().type(); + markAttributeModified(type); + if (hasAttribute(type)) { + T *attr = dynamic_cast(attribute(type)); + if (checkAttribute(attr, type)) { + return attr; + } + } else if (option == AddIfMissing) { + T *attr = new T(); + addAttribute(attr); + return attr; + } + + return nullptr; +} + +template inline const T *Tag::attribute() const +{ + const QByteArray type = T().type(); + if (hasAttribute(type)) { + const T *attr = dynamic_cast(attribute(type)); + if (checkAttribute(attr, type)) { + return attr; + } + } + + return nullptr; +} + +template inline void Tag::removeAttribute() +{ + const T dummy; + removeAttribute(dummy.type()); +} + +template inline bool Tag::hasAttribute() const +{ + const T dummy; + return hasAttribute(dummy.type()); +} + +} // namespace Akonadi + +AKONADICORE_EXPORT QDebug operator<<(QDebug debug, const Akonadi::Tag &tag); + +Q_DECLARE_METATYPE(Akonadi::Tag) +Q_DECLARE_METATYPE(Akonadi::Tag::List) +Q_DECLARE_METATYPE(QSet) +Q_DECLARE_TYPEINFO(Akonadi::Tag, Q_MOVABLE_TYPE); + diff --git a/src/core/tag_p.h b/src/core/tag_p.h new file mode 100644 index 0000000..34eb88d --- /dev/null +++ b/src/core/tag_p.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "attributestorage_p.h" +#include "tag.h" + +namespace Akonadi +{ +class TagPrivate : public QSharedData +{ +public: + explicit TagPrivate() = default; + TagPrivate(const TagPrivate &other) + : QSharedData(other) + { + id = other.id; + gid = other.gid; + remoteId = other.remoteId; + if (other.parent) { + parent.reset(new Tag(*other.parent)); + } + type = other.type; + mAttributeStorage = other.mAttributeStorage; + } + + ~TagPrivate() = default; + + void resetChangeLog() + { + mAttributeStorage.resetChangeLog(); + } + + // 4 bytes padding here (after QSharedData) + + Tag::Id id = -1; + QByteArray gid; + QByteArray remoteId; + QScopedPointer parent; + QByteArray type; + AttributeStorage mAttributeStorage; +}; + +} + diff --git a/src/core/tagfetchscope.cpp b/src/core/tagfetchscope.cpp new file mode 100644 index 0000000..6146a96 --- /dev/null +++ b/src/core/tagfetchscope.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagfetchscope.h" +#include + +using namespace Akonadi; + +struct Q_DECL_HIDDEN Akonadi::TagFetchScope::Private { + Private() + { + } + + QSet mAttributes; + bool mFetchIdOnly = false; + bool mFetchAllAttrs = true; + bool mFetchRemotId = false; +}; + +TagFetchScope::TagFetchScope() + : d(new Private) +{ +} + +TagFetchScope::~TagFetchScope() +{ +} + +TagFetchScope::TagFetchScope(const TagFetchScope &other) + : d(new Private) +{ + operator=(other); +} + +TagFetchScope &TagFetchScope::operator=(const TagFetchScope &other) +{ + d->mAttributes = other.d->mAttributes; + d->mFetchIdOnly = other.d->mFetchIdOnly; + d->mFetchRemotId = other.d->mFetchRemotId; + d->mFetchAllAttrs = other.d->mFetchAllAttrs; + return *this; +} + +QSet TagFetchScope::attributes() const +{ + return d->mAttributes; +} + +void TagFetchScope::fetchAttribute(const QByteArray &type, bool fetch) +{ + if (fetch) { + d->mAttributes.insert(type); + } else { + d->mAttributes.remove(type); + } +} + +void TagFetchScope::setFetchIdOnly(bool idOnly) +{ + d->mFetchIdOnly = idOnly; + d->mAttributes.clear(); +} + +bool TagFetchScope::fetchIdOnly() const +{ + return d->mFetchIdOnly; +} + +void TagFetchScope::setFetchRemoteId(bool fetchRemoteId) +{ + d->mFetchRemotId = fetchRemoteId; +} + +bool TagFetchScope::fetchRemoteId() const +{ + return d->mFetchRemotId; +} + +void TagFetchScope::setFetchAllAttributes(bool fetchAllAttrs) +{ + d->mFetchAllAttrs = fetchAllAttrs; +} + +bool TagFetchScope::fetchAllAttributes() const +{ + return d->mFetchAllAttrs; +} diff --git a/src/core/tagfetchscope.h b/src/core/tagfetchscope.h new file mode 100644 index 0000000..71263f4 --- /dev/null +++ b/src/core/tagfetchscope.h @@ -0,0 +1,123 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Specifies which parts of a tag should be fetched from the Akonadi storage. + * + * @since 4.13 + */ +class AKONADICORE_EXPORT TagFetchScope +{ +public: + /** + * Creates an empty tag fetch scope. + * + * Using an empty scope will only fetch the very basic meta data of tags, + * e.g. local id, remote id and mime type + */ + TagFetchScope(); + + /** + * Creates a new tag fetch scope from an @p other. + */ + TagFetchScope(const TagFetchScope &other); + + /** + * Destroys the tag fetch scope. + */ + ~TagFetchScope(); + + /** + * Assigns the @p other to this scope and returns a reference to this scope. + */ + TagFetchScope &operator=(const TagFetchScope &other); + + /** + * Returns all explicitly fetched attributes. + * + * Undefined if fetchAllAttributes() returns true. + * + * @see fetchAttribute() + */ + QSet attributes() const; + + /** + * Sets whether to fetch tag remote ID. + * + * This option only has effect for Resources. + */ + void setFetchRemoteId(bool fetchRemoteId); + + /** + * Returns whether tag remote ID should be fetched. + */ + bool fetchRemoteId() const; + + /** + * Sets whether to fetch all attributes. + */ + void setFetchAllAttributes(bool fetchAllAttributes); + + /** + * Returns whether to fetch all attributes + */ + bool fetchAllAttributes() const; + + /** + * Sets whether the attribute of the given @p type should be fetched. + * + * @param type The attribute type to fetch. + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + void fetchAttribute(const QByteArray &type, bool fetch = true); + + /** + * Sets whether the attribute of the requested type should be fetched. + * + * @param fetch @c true if the attribute should be fetched, @c false otherwise. + */ + template inline void fetchAttribute(bool fetch = true) + { + T dummy; + fetchAttribute(dummy.type(), fetch); + } + + /** + * Sets whether only the id or the complete tag should be fetched. + * + * The default is @c false. + * + * @since 4.15 + */ + void setFetchIdOnly(bool fetchIdOnly); + + /** + * Sets whether only the id of the tags should be retieved or the complete tag. + * + * @see tagFetchScope() + * @since 4.15 + */ + bool fetchIdOnly() const; + +private: + struct Private; + /// @cond PRIVATE + QSharedPointer d; + /// @endcond +}; + +} + +// Q_DECLARE_METATYPE(Akonadi::TagFetchScope) + diff --git a/src/core/tagsync.cpp b/src/core/tagsync.cpp new file mode 100644 index 0000000..fb4e93c --- /dev/null +++ b/src/core/tagsync.cpp @@ -0,0 +1,235 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +namespace Akonadi +{ +class Item; +} + +#include "tagsync.h" +#include "akonadicore_debug.h" +#include "itemfetchscope.h" +#include "jobs/itemfetchjob.h" +#include "jobs/itemmodifyjob.h" +#include "jobs/tagcreatejob.h" +#include "jobs/tagfetchjob.h" +#include "jobs/tagmodifyjob.h" +#include "tagfetchscope.h" + +using namespace Akonadi; + +bool operator==(const Item &left, const Item &right) +{ + if (left.isValid() && right.isValid() && (left.id() == right.id())) { + return true; + } + if (!left.remoteId().isEmpty() && !right.remoteId().isEmpty() && (left.remoteId() == right.remoteId())) { + return true; + } + if (!left.gid().isEmpty() && !right.gid().isEmpty() && (left.gid() == right.gid())) { + return true; + } + return false; +} + +TagSync::TagSync(QObject *parent) + : Job(parent) + , mDeliveryDone(false) + , mTagMembersDeliveryDone(false) + , mLocalTagsFetched(false) +{ +} + +TagSync::~TagSync() +{ +} + +void TagSync::setFullTagList(const Akonadi::Tag::List &tags) +{ + mRemoteTags = tags; + mDeliveryDone = true; + diffTags(); +} + +void TagSync::setTagMembers(const QHash &ridMemberMap) +{ + mRidMemberMap = ridMemberMap; + mTagMembersDeliveryDone = true; + diffTags(); +} + +void TagSync::doStart() +{ + // qCDebug(AKONADICORE_LOG); + // This should include all tags, including the ones that don't have a remote id + auto fetch = new Akonadi::TagFetchJob(this); + fetch->fetchScope().setFetchRemoteId(true); + connect(fetch, &KJob::result, this, &TagSync::onLocalTagFetchDone); +} + +void TagSync::onLocalTagFetchDone(KJob *job) +{ + // qCDebug(AKONADICORE_LOG); + auto fetch = static_cast(job); + mLocalTags = fetch->tags(); + mLocalTagsFetched = true; + diffTags(); +} + +void TagSync::diffTags() +{ + if (!mDeliveryDone || !mTagMembersDeliveryDone || !mLocalTagsFetched) { + qCDebug(AKONADICORE_LOG) << "waiting for delivery: " << mDeliveryDone << mLocalTagsFetched; + return; + } + // qCDebug(AKONADICORE_LOG) << "diffing"; + QHash tagByGid; + QHash tagByRid; + QHash tagById; + Q_FOREACH (const Akonadi::Tag &localTag, mLocalTags) { + tagByRid.insert(localTag.remoteId(), localTag); + tagByGid.insert(localTag.gid(), localTag); + if (!localTag.remoteId().isEmpty()) { + tagById.insert(localTag.id(), localTag); + } + } + Q_FOREACH (const Akonadi::Tag &remoteTag, mRemoteTags) { + if (tagByRid.contains(remoteTag.remoteId())) { + // Tag still exists, check members + Tag tag = tagByRid.value(remoteTag.remoteId()); + auto itemFetch = new ItemFetchJob(tag, this); + itemFetch->setProperty("tag", QVariant::fromValue(tag)); + itemFetch->setProperty("merge", false); + itemFetch->fetchScope().setFetchGid(true); + connect(itemFetch, &KJob::result, this, &TagSync::onTagItemsFetchDone); + connect(itemFetch, &KJob::result, this, &TagSync::onJobDone); + tagById.remove(tagByRid.value(remoteTag.remoteId()).id()); + } else if (tagByGid.contains(remoteTag.gid())) { + // Tag exists but has no rid + // Merge members and set rid + Tag tag = tagByGid.value(remoteTag.gid()); + tag.setRemoteId(remoteTag.remoteId()); + auto itemFetch = new ItemFetchJob(tag, this); + itemFetch->setProperty("tag", QVariant::fromValue(tag)); + itemFetch->setProperty("merge", true); + itemFetch->fetchScope().setFetchGid(true); + connect(itemFetch, &KJob::result, this, &TagSync::onTagItemsFetchDone); + connect(itemFetch, &KJob::result, this, &TagSync::onJobDone); + tagById.remove(tagByGid.value(remoteTag.gid()).id()); + } else { + // New tag, create + auto createJob = new TagCreateJob(remoteTag, this); + createJob->setMergeIfExisting(true); + connect(createJob, &KJob::result, this, &TagSync::onCreateTagDone); + connect(createJob, &KJob::result, this, &TagSync::onJobDone); + } + } + Q_FOREACH (const Tag &tag, tagById) { + // Removed remotely, unset rid + Tag copy(tag); + copy.setRemoteId(QByteArray("")); + auto modJob = new TagModifyJob(copy, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + } + checkDone(); +} + +void TagSync::onCreateTagDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "ItemFetch failed: " << job->errorString(); + return; + } + + Akonadi::Tag tag = static_cast(job)->tag(); + const Item::List remoteMembers = mRidMemberMap.value(QString::fromLatin1(tag.remoteId())); + for (Item item : remoteMembers) { + item.setTag(tag); + auto modJob = new ItemModifyJob(item, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + qCDebug(AKONADICORE_LOG) << "setting tag " << item.remoteId(); + } +} + +static bool containsByGidOrRid(const Item::List &items, const Item &key) +{ + return std::any_of(items.cbegin(), items.cend(), [&key](const Item &item) { + return ((!item.gid().isEmpty() && !key.gid().isEmpty()) && (item.gid() == key.gid())) || (item.remoteId() == key.remoteId()); + }); +} + +void TagSync::onTagItemsFetchDone(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "ItemFetch failed: " << job->errorString(); + return; + } + + const Akonadi::Item::List items = static_cast(job)->items(); + const auto tag = job->property("tag").value(); + const bool merge = job->property("merge").toBool(); + const Item::List remoteMembers = mRidMemberMap.value(QString::fromLatin1(tag.remoteId())); + + // add = remote - local + Item::List toAdd; + for (const Item &remote : remoteMembers) { + if (!containsByGidOrRid(items, remote)) { + toAdd << remote; + } + } + + // remove = local - remote + Item::List toRemove; + for (const Item &local : items) { + // Skip items that have no remote id yet + // Trying to them will only result in a conflict + if (local.remoteId().isEmpty()) { + continue; + } + if (!containsByGidOrRid(remoteMembers, local)) { + toRemove << local; + } + } + + if (!merge) { + for (Item item : std::as_const(toRemove)) { + item.clearTag(tag); + auto modJob = new ItemModifyJob(item, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + qCDebug(AKONADICORE_LOG) << "removing tag " << item.remoteId(); + } + } + for (Item item : std::as_const(toAdd)) { + item.setTag(tag); + auto modJob = new ItemModifyJob(item, this); + connect(modJob, &KJob::result, this, &TagSync::onJobDone); + qCDebug(AKONADICORE_LOG) << "setting tag " << item.remoteId(); + } +} + +void TagSync::onJobDone(KJob * /*unused*/) +{ + checkDone(); +} + +void TagSync::slotResult(KJob *job) +{ + if (job->error()) { + qCWarning(AKONADICORE_LOG) << "Error during TagSync: " << job->errorString() << job->metaObject()->className(); + // pretend there were no errors + Akonadi::Job::removeSubjob(job); + } else { + Akonadi::Job::slotResult(job); + } +} + +void TagSync::checkDone() +{ + if (hasSubjobs()) { + return; + } + qCDebug(AKONADICORE_LOG) << "done"; + emitResult(); +} diff --git a/src/core/tagsync.h b/src/core/tagsync.h new file mode 100644 index 0000000..10dbfb4 --- /dev/null +++ b/src/core/tagsync.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include "akonadicore_export.h" + +#include "item.h" +#include "jobs/job.h" +#include "tag.h" + +namespace Akonadi +{ +class AKONADICORE_EXPORT TagSync : public Akonadi::Job +{ + Q_OBJECT +public: + explicit TagSync(QObject *parent = nullptr); + ~TagSync() override; + + void setFullTagList(const Akonadi::Tag::List &tags); + void setTagMembers(const QHash &ridMemberMap); + +protected: + void doStart() override; + +private Q_SLOTS: + void onLocalTagFetchDone(KJob *job); + void onCreateTagDone(KJob *job); + void onTagItemsFetchDone(KJob *job); + void onJobDone(KJob *job); + void slotResult(KJob *job) override; + +private: + void diffTags(); + void checkDone(); + +private: + Akonadi::Tag::List mRemoteTags; + Akonadi::Tag::List mLocalTags; + bool mDeliveryDone; + bool mTagMembersDeliveryDone; + bool mLocalTagsFetched; + QHash mRidMemberMap; +}; + +} + diff --git a/src/core/trashsettings.cpp b/src/core/trashsettings.cpp new file mode 100644 index 0000000..666f7a7 --- /dev/null +++ b/src/core/trashsettings.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "trashsettings.h" +#include "akonadicore_debug.h" + +#include +#include + +#include +#include + +using namespace Akonadi; + +Akonadi::Collection TrashSettings::getTrashCollection(const QString &resource) +{ + KConfig config(QStringLiteral("akonaditrashrc")); + KConfigGroup group(&config, resource); + const auto colId = group.readEntry("TrashCollection", -1); + qCWarning(AKONADICORE_LOG) << resource << colId; + return Collection(colId); +} + +void TrashSettings::setTrashCollection(const QString &resource, const Akonadi::Collection &collection) +{ + KConfig config(QStringLiteral("akonaditrashrc")); + KConfigGroup group(&config, resource); + qCWarning(AKONADICORE_LOG) << resource << collection.id(); + group.writeEntry("TrashCollection", collection.id()); +} diff --git a/src/core/trashsettings.h b/src/core/trashsettings.h new file mode 100644 index 0000000..3002500 --- /dev/null +++ b/src/core/trashsettings.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2011 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include "collection.h" + +class QString; + +namespace Akonadi +{ +/** + * @short Global Trash-related Settings + * + * All settings concerning the trashhandling should go here. + * + * @author Christian Mollekopf + * @since 4.8 + */ +// TODO setting for time before items are deleted by janitor agent +namespace TrashSettings +{ +/** + * Set the trash collection for the given @p resource which is then used by the TrashJob + */ +AKONADICORE_EXPORT void setTrashCollection(const QString &resource, const Collection &collection); +/** + * Get the trash collection for the given @p resource + */ +Q_REQUIRED_RESULT AKONADICORE_EXPORT Collection getTrashCollection(const QString &resource); +} + +} + diff --git a/src/core/typepluginloader.cpp b/src/core/typepluginloader.cpp new file mode 100644 index 0000000..8ce56a1 --- /dev/null +++ b/src/core/typepluginloader.cpp @@ -0,0 +1,437 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "typepluginloader_p.h" + +#include "item.h" +#include "itemserializer_p.h" +#include "itemserializerplugin.h" + +#include "akonadicore_debug.h" + +// Qt +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// temporary +#include "pluginloader_p.h" + +#include +#include + +static const char LEGACY_NAME[] = "legacy"; +static const char DEFAULT_NAME[] = "default"; +static const char _APPLICATION_OCTETSTREAM[] = "application/octet-stream"; + +namespace Akonadi +{ +Q_GLOBAL_STATIC(DefaultItemSerializerPlugin, s_defaultItemSerializerPlugin) // NOLINT(readability-redundant-member-init) + +class PluginEntry +{ +public: + PluginEntry() + : mPlugin(nullptr) + { + } + + explicit PluginEntry(const QString &identifier, QObject *plugin = nullptr) + : mIdentifier(identifier) + , mPlugin(plugin) + { + qCDebug(AKONADICORE_LOG) << " PLUGIN : identifier" << identifier; + } + + QObject *plugin() const + { + if (mPlugin) { + return mPlugin; + } + + QObject *object = PluginLoader::self()->createForName(mIdentifier); + if (!object) { + qCWarning(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "plugin" << mIdentifier << "is not valid!"; + + // we try to use the default in that case + mPlugin = s_defaultItemSerializerPlugin; + } + + mPlugin = object; + if (!qobject_cast(mPlugin)) { + qCWarning(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "plugin" << mIdentifier << "doesn't provide interface ItemSerializerPlugin!"; + + // we try to use the default in that case + mPlugin = s_defaultItemSerializerPlugin; + } + + Q_ASSERT(mPlugin); + + return mPlugin; + } + + const char *pluginClassName() const + { + return plugin()->metaObject()->className(); + } + + QString identifier() const + { + return mIdentifier; + } + + bool operator<(const PluginEntry &other) const + { + return mIdentifier < other.mIdentifier; + } + + bool operator<(const QString &identifier) const + { + return mIdentifier < identifier; + } + +private: + QString mIdentifier; + mutable QObject *mPlugin; +}; + +class MimeTypeEntry +{ +public: + explicit MimeTypeEntry(const QString &mimeType) + : m_mimeType(mimeType) + { + } + + QString type() const + { + return m_mimeType; + } + + void add(const QByteArray &class_, const PluginEntry &entry) + { + m_pluginsByMetaTypeId.clear(); // iterators will be invalidated by next line + m_plugins.insert(class_, entry); + } + + const PluginEntry *plugin(const QByteArray &class_) const + { + const QHash::const_iterator it = m_plugins.find(class_); + return it == m_plugins.end() ? nullptr : it.operator->(); + } + + const PluginEntry *defaultPlugin() const + { + // 1. If there's an explicit default plugin, use that one: + if (const PluginEntry *pe = plugin(DEFAULT_NAME)) { + return pe; + } + + // 2. Otherwise, look through the already instantiated plugins, + // and return one of them (preferably not the legacy one): + bool sawZero = false; + for (QMap::const_iterator>::const_iterator it = m_pluginsByMetaTypeId.constBegin(), + end = m_pluginsByMetaTypeId.constEnd(); + it != end; + ++it) { + if (it.key() == 0) { + sawZero = true; + } else if (*it != m_plugins.end()) { + return it->operator->(); + } + } + + // 3. Otherwise, look through the whole list (again, preferably not the legacy one): + for (QHash::const_iterator it = m_plugins.constBegin(), end = m_plugins.constEnd(); it != end; ++it) { + if (it.key() == LEGACY_NAME) { + sawZero = true; + } else { + return it.operator->(); + } + } + + // 4. take the legacy one: + if (sawZero) { + return plugin(0); + } + return nullptr; + } + + const PluginEntry *plugin(int metaTypeId) const + { + const QMap::const_iterator> &c_pluginsByMetaTypeId = m_pluginsByMetaTypeId; + QMap::const_iterator>::const_iterator it = c_pluginsByMetaTypeId.find(metaTypeId); + if (it == c_pluginsByMetaTypeId.end()) { + it = QMap::const_iterator>::const_iterator( + m_pluginsByMetaTypeId.insert(metaTypeId, m_plugins.find(metaTypeId ? QMetaType::typeName(metaTypeId) : LEGACY_NAME))); + } + return *it == m_plugins.end() ? nullptr : it->operator->(); + } + + const PluginEntry *plugin(const QVector &metaTypeIds, int &chosen) const + { + bool sawZero = false; + for (QVector::const_iterator it = metaTypeIds.begin(), end = metaTypeIds.end(); it != end; ++it) { + if (*it == 0) { + sawZero = true; // skip the legacy type and see if we can find something else first + } else if (const PluginEntry *const entry = plugin(*it)) { + chosen = *it; + return entry; + } + } + if (sawZero) { + chosen = 0; + return plugin(0); + } + return nullptr; + } + +private: + QString m_mimeType; + QHash m_plugins; + mutable QMap::const_iterator> m_pluginsByMetaTypeId; +}; + +class PluginRegistry +{ +public: + PluginRegistry() + : mDefaultPlugin(PluginEntry(QStringLiteral("application/octet-stream@QByteArray"), s_defaultItemSerializerPlugin)) + , mOverridePlugin(nullptr) + { + const PluginLoader *pl = PluginLoader::self(); + if (!pl) { + qCWarning(AKONADICORE_LOG) << "Cannot instantiate plugin loader!"; + return; + } + const QStringList names = pl->names(); + qCDebug(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "found" << names.size() << "plugins."; + QMap map; + QRegularExpression rx(QRegularExpression::anchoredPattern(QStringLiteral("(.+)@(.+)"))); + QMimeDatabase mimeDb; + for (const QString &name : names) { + QRegularExpressionMatch match = rx.match(name); + if (match.hasMatch()) { + const QMimeType mime = mimeDb.mimeTypeForName(match.captured(1)); + if (mime.isValid()) { + const QString mimeType = mime.name(); + const QByteArray classType = match.captured(2).toLatin1(); + QMap::iterator it = map.find(mimeType); + if (it == map.end()) { + it = map.insert(mimeType, MimeTypeEntry(mimeType)); + } + it->add(classType, PluginEntry(name)); + } + } else { + qCDebug(AKONADICORE_LOG) << "ItemSerializerPluginLoader: " + << "name" << name << "doesn't look like mimetype@classtype"; + } + } + const QString APPLICATION_OCTETSTREAM = QLatin1String(_APPLICATION_OCTETSTREAM); + QMap::iterator it = map.find(APPLICATION_OCTETSTREAM); + if (it == map.end()) { + it = map.insert(APPLICATION_OCTETSTREAM, MimeTypeEntry(APPLICATION_OCTETSTREAM)); + } + it->add("QByteArray", mDefaultPlugin); + it->add(LEGACY_NAME, mDefaultPlugin); + const int size = map.size(); + allMimeTypes.reserve(size); + std::copy(map.begin(), map.end(), std::back_inserter(allMimeTypes)); + } + + QObject *findBestMatch(const QString &type, const QVector &metaTypeId, TypePluginLoader::Options opt) + { + if (QObject *const plugin = findBestMatch(type, metaTypeId)) { + { + if ((opt & TypePluginLoader::NoDefault) && plugin == mDefaultPlugin.plugin()) { + return nullptr; + } + return plugin; + } + } + return nullptr; + } + + QObject *findBestMatch(const QString &type, const QVector &metaTypeIds) + { + if (mOverridePlugin) { + return mOverridePlugin; + } + if (QObject *const plugin = cacheLookup(type, metaTypeIds)) { + // plugin cached, so let's take that one + return plugin; + } + int chosen = -1; + QObject *const plugin = findBestMatchImpl(type, metaTypeIds, chosen); + if (metaTypeIds.empty()) { + if (plugin) { + cachedDefaultPlugins[type] = plugin; + } + } + if (chosen >= 0) { + cachedPlugins[type][chosen] = plugin; + } + return plugin; + } + + void overrideDefaultPlugin(QObject *p) + { + mOverridePlugin = p; + } + +private: + QObject *findBestMatchImpl(const QString &type, const QVector &metaTypeIds, int &chosen) const + { + const QMimeDatabase mimeDb; + const QMimeType mimeType = mimeDb.mimeTypeForName(type); + if (!mimeType.isValid()) { + qCWarning(AKONADICORE_LOG) << "Invalid mimetype requested:" << type; + return mDefaultPlugin.plugin(); + } + + // step 1: find all plugins that match at all + QVector matchingIndexes; + for (int i = 0, end = allMimeTypes.size(); i < end; ++i) { + if (mimeType.inherits(allMimeTypes[i].type())) { + matchingIndexes.append(i); + } + } + + // step 2: if we have more than one match, find the most specific one using topological sort + QVector order; + if (matchingIndexes.size() <= 1) { + order.push_back(0); + } else { + boost::adjacency_list<> graph(matchingIndexes.size()); + for (int i = 0, end = matchingIndexes.size(); i != end; ++i) { + const QMimeType mimeType = mimeDb.mimeTypeForName(allMimeTypes[matchingIndexes[i]].type()); + if (!mimeType.isValid()) { + continue; + } + for (int j = 0; j != end; ++j) { + if (i != j && mimeType.inherits(allMimeTypes[matchingIndexes[j]].type())) { + boost::add_edge(j, i, graph); + } + } + } + + order.reserve(matchingIndexes.size()); + try { + boost::topological_sort(graph, std::back_inserter(order)); + } catch (const boost::not_a_dag &e) { + qCWarning(AKONADICORE_LOG) << "Mimetype tree is not a DAG!"; + return mDefaultPlugin.plugin(); + } + } + + // step 3: ask each one in turn if it can handle any of the metaTypeIds: + // qCDebug(AKONADICORE_LOG) << "Looking for " << format( type, metaTypeIds ); + for (QVector::const_iterator it = order.constBegin(), end = order.constEnd(); it != end; ++it) { + // qCDebug(AKONADICORE_LOG) << " Considering serializer plugin for type" << allMimeTypes[matchingIndexes[*it]].type() + // // << "as the closest match"; + const MimeTypeEntry &mt = allMimeTypes[matchingIndexes[*it]]; + if (metaTypeIds.empty()) { + if (const PluginEntry *const entry = mt.defaultPlugin()) { + // qCDebug(AKONADICORE_LOG) << " -> got " << entry->pluginClassName() << " and am happy with it."; + // FIXME ? in qt5 we show "application/octet-stream" first so if will use default plugin. Exclude it until we look at all mimetype and use + // default at the end if necessary + if (allMimeTypes[matchingIndexes[*it]].type() != QLatin1String("application/octet-stream")) { + return entry->plugin(); + } + } else { + // qCDebug(AKONADICORE_LOG) << " -> no default plugin for this mime type, trying next"; + } + } else if (const PluginEntry *const entry = mt.plugin(metaTypeIds, chosen)) { + // qCDebug(AKONADICORE_LOG) << " -> got " << entry->pluginClassName() << " and am happy with it."; + return entry->plugin(); + } else { + // qCDebug(AKONADICORE_LOG) << " -> can't handle any of the types, trying next"; + } + } + + // qCDebug(AKONADICORE_LOG) << " No further candidates, using default plugin"; + // no luck? Use the default plugin + return mDefaultPlugin.plugin(); + } + + std::vector allMimeTypes; + QHash> cachedPlugins; + QHash cachedDefaultPlugins; + + // ### cache NULLs, too + QObject *cacheLookup(const QString &mimeType, const QVector &metaTypeIds) const + { + if (metaTypeIds.empty()) { + const QHash::const_iterator hit = cachedDefaultPlugins.find(mimeType); + if (hit != cachedDefaultPlugins.end()) { + return *hit; + } + } + + const QHash>::const_iterator hit = cachedPlugins.find(mimeType); + if (hit == cachedPlugins.end()) { + return nullptr; + } + bool sawZero = false; + for (QVector::const_iterator it = metaTypeIds.begin(), end = metaTypeIds.end(); it != end; ++it) { + if (*it == 0) { + sawZero = true; // skip the legacy type and see if we can find something else first + } else if (QObject *const o = hit->value(*it)) { + return o; + } + } + if (sawZero) { + return hit->value(0); + } + return nullptr; + } + +private: + PluginEntry mDefaultPlugin; + QObject *mOverridePlugin; +}; + +Q_GLOBAL_STATIC(PluginRegistry, s_pluginRegistry) // NOLINT(readability-redundant-member-init) + +QObject *TypePluginLoader::objectForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options opt) +{ + return s_pluginRegistry->findBestMatch(mimetype, metaTypeIds, opt); +} + +QObject *TypePluginLoader::defaultObjectForMimeType(const QString &mimetype) +{ + return objectForMimeTypeAndClass(mimetype, QVector()); +} + +ItemSerializerPlugin *TypePluginLoader::pluginForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options opt) +{ + return qobject_cast(objectForMimeTypeAndClass(mimetype, metaTypeIds, opt)); +} + +ItemSerializerPlugin *TypePluginLoader::defaultPluginForMimeType(const QString &mimetype) +{ + ItemSerializerPlugin *plugin = qobject_cast(defaultObjectForMimeType(mimetype)); + Q_ASSERT(plugin); + return plugin; +} + +void TypePluginLoader::overridePluginLookup(QObject *p) +{ + s_pluginRegistry->overrideDefaultPlugin(p); +} + +} // namespace Akonadi diff --git a/src/core/typepluginloader_p.h b/src/core/typepluginloader_p.h new file mode 100644 index 0000000..2f8d765 --- /dev/null +++ b/src/core/typepluginloader_p.h @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2007 Till Adam + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadicore_export.h" +#include + +class QObject; +class QString; +template class QVector; + +namespace Akonadi +{ +class ItemSerializerPlugin; + +/** + * @internal + * + * With KDE 4.6 we are on the way to change the ItemSerializer plugins into general TypePlugins + * which provide several type specific actions, namely: + * @li Serializing/Deserializing of payload + * @li Comparing two payloads and reporting the differences + * + * To share the code of loading the plugins and finding the right plugin for a given mime type + * the old code from ItemSerializer has been extracted into the pluginForMimeType() method + * inside the TypePluginLoader namespace. + */ +namespace TypePluginLoader +{ +enum Option { + NoOptions, + NoDefault = 1, + + _LastOption, + OptionMask = 2 * _LastOption - 1 +}; +Q_DECLARE_FLAGS(Options, Option) + +/** + * Returns the default item serializer plugin that matches the given @p mimetype. + */ +AKONADICORE_EXPORT ItemSerializerPlugin *defaultPluginForMimeType(const QString &mimetype); + +/** + * Returns the item serializer plugin that matches the given + * @p mimetype, and any of the classes described by @p metaTypeIds. + */ +AKONADICORE_EXPORT ItemSerializerPlugin *pluginForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options options = NoOptions); + +/** + * Returns the default type plugin object that matches the given @p mimetype. + */ +AKONADICORE_EXPORT QObject *defaultObjectForMimeType(const QString &mimetype); + +/** + * Returns the type plugin object that matches the given @p mimetype, + * and any of the classes described by @p metaTypeIds. + */ +AKONADICORE_EXPORT QObject *objectForMimeTypeAndClass(const QString &mimetype, const QVector &metaTypeIds, Options options = NoOptions); + +/** + * Override the plugin-lookup with @p plugin. + * + * After calling this each lookup will always return @p plugin. + * This is useful to inject a special plugin for testing purposes. + * To reset the plugin, set to 0. + * + * @since 4.12 + */ +AKONADICORE_EXPORT void overridePluginLookup(QObject *plugin); + +} + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::TypePluginLoader::Options) + diff --git a/src/interfaces/CMakeLists.txt b/src/interfaces/CMakeLists.txt new file mode 100644 index 0000000..9ee7452 --- /dev/null +++ b/src/interfaces/CMakeLists.txt @@ -0,0 +1,21 @@ +SET(DBUS_INTERFACE_XMLS + org.freedesktop.Akonadi.AgentManager.xml + org.freedesktop.Akonadi.NotificationManager.xml + org.freedesktop.Akonadi.Preprocessor.xml + org.freedesktop.Akonadi.Tracer.xml + org.freedesktop.Akonadi.Agent.Control.xml + org.freedesktop.Akonadi.Agent.Search.xml + org.freedesktop.Akonadi.Agent.Status.xml + org.freedesktop.Akonadi.Resource.xml + org.freedesktop.Akonadi.ControlManager.xml + org.freedesktop.Akonadi.NotificationSource.xml + org.freedesktop.Akonadi.Server.xml + org.freedesktop.Akonadi.StorageDebugger.xml + org.freedesktop.Akonadi.TracerNotification.xml +) + +install(FILES ${DBUS_INTERFACE_XMLS} + DESTINATION ${AKONADI_DBUS_INTERFACES_INSTALL_DIR} +) + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml b/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml new file mode 100644 index 0000000..9aab3c3 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml b/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml new file mode 100644 index 0000000..35a18fb --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml b/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml new file mode 100644 index 0000000..fd1d843 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Agent.Status.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml b/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml new file mode 100644 index 0000000..f62520f --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml b/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml new file mode 100644 index 0000000..4110421 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.AgentManagerInternal.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml b/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml new file mode 100644 index 0000000..01540e5 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.AgentServer.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml b/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml new file mode 100644 index 0000000..07ad225 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.ControlManager.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Janitor.xml b/src/interfaces/org.freedesktop.Akonadi.Janitor.xml new file mode 100644 index 0000000..c1502df --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Janitor.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml b/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml new file mode 100644 index 0000000..18ef48f --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.NotificationManager.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml b/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml new file mode 100644 index 0000000..cdef9bf --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.NotificationSource.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml b/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml new file mode 100644 index 0000000..2535294 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml b/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml new file mode 100644 index 0000000..bf1ef66 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml b/src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml new file mode 100644 index 0000000..b372cba --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Resource.Transport.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Resource.xml b/src/interfaces/org.freedesktop.Akonadi.Resource.xml new file mode 100644 index 0000000..ce98cf6 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Resource.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Resource2.Task.xml b/src/interfaces/org.freedesktop.Akonadi.Resource2.Task.xml new file mode 100644 index 0000000..20cfd0d --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Resource2.Task.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Resource2.xml b/src/interfaces/org.freedesktop.Akonadi.Resource2.xml new file mode 100644 index 0000000..e38b149 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Resource2.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml b/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml new file mode 100644 index 0000000..3f5b62c --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.SearchManager.xml b/src/interfaces/org.freedesktop.Akonadi.SearchManager.xml new file mode 100644 index 0000000..b2ba6ba --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.SearchManager.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Server.xml b/src/interfaces/org.freedesktop.Akonadi.Server.xml new file mode 100644 index 0000000..9f2788d --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Server.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml b/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml new file mode 100644 index 0000000..ed37522 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.Tracer.xml b/src/interfaces/org.freedesktop.Akonadi.Tracer.xml new file mode 100644 index 0000000..09e88a7 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.Tracer.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml b/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml new file mode 100644 index 0000000..1e290b6 --- /dev/null +++ b/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/interfaces/org.kde.Akonadi.Accounts.xml b/src/interfaces/org.kde.Akonadi.Accounts.xml new file mode 100644 index 0000000..b8ae7a9 --- /dev/null +++ b/src/interfaces/org.kde.Akonadi.Accounts.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/private/CMakeLists.txt b/src/private/CMakeLists.txt new file mode 100644 index 0000000..522fcd2 --- /dev/null +++ b/src/private/CMakeLists.txt @@ -0,0 +1,128 @@ +add_subdirectory(protocolgen) + +# TODO: Use LibLZMA::LibLZMA when we'll require CMake >= 3.14 +include_directories(${LIBLZMA_INCLUDE_DIRS}) + +if(NOT XMLLINT_EXECUTABLE) + message(STATUS "xmllint not found, skipping protocol.xml validation") +else() + add_test(AkonadiPrivate-protocol-xmllint ${XMLLINT_EXECUTABLE} --noout ${CMAKE_CURRENT_SOURCE_DIR}/protocol.xml) +endif() + +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/protocol_gen.cpp + ${CMAKE_CURRENT_BINARY_DIR}/protocol_gen.h + COMMAND protocolgen ${CMAKE_CURRENT_SOURCE_DIR}/protocol.xml + DEPENDS protocolgen ${CMAKE_CURRENT_SOURCE_DIR}/protocol.xml + COMMENT "Generating Protocol implementation" +) + +add_custom_target(generate_protocol DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/protocol_gen.cpp) + +set(akonadiprivate_SRCS + imapparser.cpp + imapset.cpp + instance.cpp + compressionstream.cpp + datastream_p.cpp + externalpartstorage.cpp + protocol.cpp + scope.cpp + tristate.cpp + standarddirs.cpp + dbus.cpp +) + +set(akonadiprivate_LIBS +PUBLIC + Qt::Core + Qt::DBus +PRIVATE + ${LIBLZMA_LIBRARIES} +) +if (WIN32) + set(akonadiprivate_LIBS + ${akonadiprivate_LIBS} + PRIVATE + Qt::Network + ) +endif() + +ecm_qt_declare_logging_category(akonadiprivate_SRCS HEADER akonadiprivate_debug.h IDENTIFIER AKONADIPRIVATE_LOG CATEGORY_NAME org.kde.pim.akonadiprivate + DESCRIPTION "akonadi (Akonadi Private Library)" + OLD_CATEGORY_NAMES akonadiprivate_log + EXPORT AKONADI + ) + +macro(update_include_directories _target) + target_include_directories(${_target} PUBLIC "$") + target_include_directories(${_target} PRIVATE "${Akonadi_SOURCE_DIR}/src/shared") + target_include_directories(${_target} PUBLIC "$") +endmacro() + +if (WIN32) + # MSVC does not like when the same object files are reused for shared and + # static linking, so in this case we build all sources twice to make it happy + set(akonadiprivate_buildsources ${akonadiprivate_SRCS}) +else() + add_library(akonadiprivate_obj OBJECT ${akonadiprivate_SRCS}) + update_include_directories(akonadiprivate_obj) + set_target_properties(akonadiprivate_obj PROPERTIES POSITION_INDEPENDENT_CODE 1) + add_dependencies(akonadiprivate_obj generate_protocol) + set(akonadiprivate_buildsources $) +endif() + +add_library(KF5AkonadiPrivate SHARED ${akonadiprivate_buildsources}) +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(KF5AkonadiPrivate PROPERTIES UNITY_BUILD ON) +endif() + +add_library(KF5::AkonadiPrivate ALIAS KF5AkonadiPrivate) +if (WIN32) + add_dependencies(KF5AkonadiPrivate generate_protocol) + update_include_directories(KF5AkonadiPrivate) +endif() +target_link_libraries(KF5AkonadiPrivate ${akonadiprivate_LIBS}) +generate_export_header(KF5AkonadiPrivate BASE_NAME akonadiprivate) + +set_target_properties(KF5AkonadiPrivate PROPERTIES + VERSION ${AKONADI_VERSION} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiPrivate +) + +install(TARGETS + KF5AkonadiPrivate + EXPORT KF5AkonadiTargets + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadiprivate_export.h + standarddirs_p.h + dbus_p.h + imapparser_p.h + imapset_p.h + instance_p.h + externalpartstorage_p.h + protocol_p.h + ${CMAKE_CURRENT_BINARY_DIR}/protocol_gen.h + protocol_exception_p.h + capabilities_p.h + scope_p.h + tristate_p.h + compressionstream_p.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/akonadi/private +) + + +### Private static library used by unit-tests #### + +add_library(akonadiprivate_static STATIC ${akonadiprivate_buildsources}) +if (WIN32) + add_dependencies(akonadiprivate_static generate_protocol) + update_include_directories(akonadiprivate_static) +endif() +set_target_properties(akonadiprivate_static PROPERTIES + COMPILE_FLAGS -DAKONADIPRIVATE_STATIC_DEFINE +) +target_link_libraries(akonadiprivate_static ${akonadiprivate_LIBS}) diff --git a/src/private/capabilities_p.h b/src/private/capabilities_p.h new file mode 100644 index 0000000..3fedb8a --- /dev/null +++ b/src/private/capabilities_p.h @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +/** + @file capabilities_p.h Shared constants for agent capabilities. + + @todo Fill this file with the missing capabilities. +*/ + +#define AKONADI_AGENT_CAPABILITY_AUTOSTART "Autostart" +#define AKONADI_AGENT_CAPABILITY_NOCONFIG "NoConfig" +#define AKONADI_AGENT_CAPABILITY_PREPROCESSOR "Preprocessor" +#define AKONADI_AGENT_CAPABILITY_RESOURCE "Resource" +#define AKONADI_AGENT_CAPABILITY_SEARCH "Search" +#define AKONADI_AGENT_CAPABILITY_UNIQUE "Unique" +#define AKONADI_AGENT_CAPABILITY_VIRTUAL "Virtual" + diff --git a/src/private/compressionstream.cpp b/src/private/compressionstream.cpp new file mode 100644 index 0000000..cfee357 --- /dev/null +++ b/src/private/compressionstream.cpp @@ -0,0 +1,324 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadiprivate_debug.h" +#include "compressionstream_p.h" + +#include + +#include + +#include + +using namespace Akonadi; + +namespace +{ +class LZMAErrorCategory : public std::error_category +{ +public: + const char *name() const noexcept override + { + return "lzma"; + } + std::string message(int ev) const noexcept override + { + switch (static_cast(ev)) { + case LZMA_OK: + return "Operation completed succesfully"; + case LZMA_STREAM_END: + return "End of stream was reached"; + case LZMA_NO_CHECK: + return "Input stream has no integrity check"; + case LZMA_UNSUPPORTED_CHECK: + return "Cannot calculate the integrity check"; + case LZMA_GET_CHECK: + return "Integrity check type is now available"; + case LZMA_MEM_ERROR: + return "Cannot allocate memory"; + case LZMA_MEMLIMIT_ERROR: + return "Memory usage limit was reached"; + case LZMA_FORMAT_ERROR: + return "File format not recognized"; + case LZMA_OPTIONS_ERROR: + return "Invalid or unsupported options"; + case LZMA_DATA_ERROR: + return "Data is corrupt"; + case LZMA_BUF_ERROR: + return "No progress is possible"; + case LZMA_PROG_ERROR: + return "Programming error"; + } + + Q_UNREACHABLE(); + } +}; + +const LZMAErrorCategory &lzmaErrorCategory() +{ + static const LZMAErrorCategory lzmaErrorCategory{}; + return lzmaErrorCategory; +} + +} // namespace + +namespace std +{ +template<> struct is_error_code_enum : std::true_type { +}; + +std::error_condition make_error_condition(lzma_ret ret) +{ + return std::error_condition(static_cast(ret), lzmaErrorCategory()); +} + +QDebug operator<<(QDebug dbg, const std::string &str) +{ + dbg << QString::fromStdString(str); + return dbg; +} + +} // namespace std + +std::error_code make_error_code(lzma_ret e) +{ + return {static_cast(e), lzmaErrorCategory()}; +} + +class Akonadi::Compressor +{ +public: + std::error_code initialize(QIODevice::OpenMode openMode) + { + if (openMode == QIODevice::ReadOnly) { + return lzma_auto_decoder(&mStream, 100 * 1024 * 1024 /* 100 MiB */, 0); + } else { + return lzma_easy_encoder(&mStream, LZMA_PRESET_DEFAULT, LZMA_CHECK_CRC32); + } + } + + void setInputBuffer(const char *data, qint64 size) + { + mStream.next_in = reinterpret_cast(data); + mStream.avail_in = size; + } + + void setOutputBuffer(char *data, qint64 maxSize) + { + mStream.next_out = reinterpret_cast(data); + mStream.avail_out = maxSize; + } + + int inputBufferAvailable() const + { + return mStream.avail_in; + } + + int outputBufferAvailable() const + { + return mStream.avail_out; + } + + std::error_code finalize() + { + lzma_end(&mStream); + return LZMA_OK; + } + + std::error_code inflate() + { + return lzma_code(&mStream, LZMA_RUN); + } + + std::error_code deflate(bool finish) + { + return lzma_code(&mStream, finish ? LZMA_FINISH : LZMA_RUN); + } + +protected: + lzma_stream mStream = LZMA_STREAM_INIT; +}; + +CompressionStream::CompressionStream(QIODevice *stream, QObject *parent) + : QIODevice(parent) + , mStream(stream) + , mResult(LZMA_OK) +{ +} + +CompressionStream::~CompressionStream() +{ + CompressionStream::close(); +} + +bool CompressionStream::isSequential() const +{ + return true; +} + +bool CompressionStream::open(OpenMode mode) +{ + if ((mode & QIODevice::ReadOnly) && (mode & QIODevice::WriteOnly)) { + qCWarning(AKONADIPRIVATE_LOG) << "Invalid open mode for CompressionStream."; + return false; + } + + mCompressor = std::make_unique(); + if (const auto err = mCompressor->initialize(mode & QIODevice::ReadOnly ? QIODevice::ReadOnly : QIODevice::WriteOnly); err != LZMA_OK) { + qCWarning(AKONADIPRIVATE_LOG) << "Failed to initialize LZMA stream coder:" << err.message(); + return false; + } + + if (mode & QIODevice::WriteOnly) { + mBuffer.resize(BUFSIZ); + mCompressor->setOutputBuffer(mBuffer.data(), mBuffer.size()); + } + + return QIODevice::open(mode); +} + +void CompressionStream::close() +{ + if (!isOpen()) { + return; + } + + if (openMode() & QIODevice::WriteOnly && mResult == LZMA_OK) { + write(nullptr, 0); + } + + mResult = mCompressor->finalize(); + + setOpenMode(QIODevice::NotOpen); +} + +std::error_code CompressionStream::error() const +{ + return mResult == LZMA_STREAM_END ? LZMA_OK : mResult; +} + +bool CompressionStream::atEnd() const +{ + return mResult == LZMA_STREAM_END && QIODevice::atEnd() && mStream->atEnd(); +} + +qint64 CompressionStream::readData(char *data, qint64 dataSize) +{ + qint64 dataRead = 0; + + if (mResult == LZMA_STREAM_END) { + return 0; + } else if (mResult != LZMA_OK) { + return -1; + } + + mCompressor->setOutputBuffer(data, dataSize); + + while (dataSize > 0) { + if (mCompressor->inputBufferAvailable() == 0) { + mBuffer.resize(BUFSIZ); + const auto compressedDataRead = mStream->read(mBuffer.data(), mBuffer.size()); + + if (compressedDataRead > 0) { + mCompressor->setInputBuffer(mBuffer.data(), compressedDataRead); + } else { + break; + } + } + + mResult = mCompressor->inflate(); + + if (mResult != LZMA_OK && mResult != LZMA_STREAM_END) { + qCWarning(AKONADIPRIVATE_LOG) << "Error while decompressing LZMA stream:" << mResult.message(); + break; + } + + const auto decompressedDataRead = dataSize - mCompressor->outputBufferAvailable(); + dataRead += decompressedDataRead; + dataSize -= decompressedDataRead; + + if (mResult == LZMA_STREAM_END) { + if (mStream->atEnd()) { + break; + } + } + + mCompressor->setOutputBuffer(data + dataRead, dataSize); + } + + return dataRead; +} + +qint64 CompressionStream::writeData(const char *data, qint64 dataSize) +{ + if (mResult != LZMA_OK) { + return 0; + } + + bool finish = (data == nullptr); + if (!finish) { + mCompressor->setInputBuffer(data, dataSize); + } + + qint64 dataWritten = 0; + + while (dataSize > 0 || finish) { + mResult = mCompressor->deflate(finish); + + if (mResult != LZMA_OK && mResult != LZMA_STREAM_END) { + qCWarning(AKONADIPRIVATE_LOG) << "Error while compressing LZMA stream:" << mResult.message(); + break; + } + + if (mCompressor->inputBufferAvailable() == 0 || (mResult == LZMA_STREAM_END)) { + const auto wrote = dataSize - mCompressor->inputBufferAvailable(); + + dataWritten += wrote; + dataSize -= wrote; + + if (dataSize > 0) { + mCompressor->setInputBuffer(data + dataWritten, dataSize); + } + } + + if (mCompressor->outputBufferAvailable() == 0 || (mResult == LZMA_STREAM_END) || finish) { + const auto toWrite = mBuffer.size() - mCompressor->outputBufferAvailable(); + if (toWrite > 0) { + const auto writtenSize = mStream->write(mBuffer.constData(), toWrite); + if (writtenSize != toWrite) { + qCWarning(AKONADIPRIVATE_LOG) << "Failed to write compressed data to output device:" << mStream->errorString(); + setErrorString(QStringLiteral("Failed to write compressed data to output device.")); + return 0; + } + } + + if (mResult == LZMA_STREAM_END) { + Q_ASSERT(finish); + break; + } + mBuffer.resize(BUFSIZ); + mCompressor->setOutputBuffer(mBuffer.data(), mBuffer.size()); + } + } + + return dataWritten; +} + +bool CompressionStream::isCompressed(QIODevice *data) +{ + constexpr std::array magic = {0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}; + + if (!data->isOpen() && !data->isReadable()) { + return false; + } + + char buf[6] = {}; + if (data->peek(buf, sizeof(buf)) != sizeof(buf)) { + return false; + } + + return memcmp(magic.data(), buf, sizeof(buf)) == 0; +} diff --git a/src/private/compressionstream_p.h b/src/private/compressionstream_p.h new file mode 100644 index 0000000..e7dfff0 --- /dev/null +++ b/src/private/compressionstream_p.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" + +#include + +#include +#include + +namespace Akonadi +{ +class Compressor; +class AKONADIPRIVATE_EXPORT CompressionStream : public QIODevice +{ + Q_OBJECT +public: + explicit CompressionStream(QIODevice *stream, QObject *parent = nullptr); + ~CompressionStream() override; + + bool open(QIODevice::OpenMode mode) override; + void close() override; + bool atEnd() const override; + + bool isSequential() const override; + + std::error_code error() const; + + static bool isCompressed(QIODevice *data); + +protected: + qint64 readData(char *data, qint64 maxSize) override; + qint64 writeData(const char *data, qint64 maxSize) override; + +private: + QIODevice *mStream = nullptr; + QByteArray mBuffer; + std::error_code mResult; + std::unique_ptr mCompressor; +}; + +} // namespace Akonadi + diff --git a/src/private/datastream_p.cpp b/src/private/datastream_p.cpp new file mode 100644 index 0000000..ce74da5 --- /dev/null +++ b/src/private/datastream_p.cpp @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "datastream_p_p.h" + +#ifdef Q_OS_WIN +#include +#include +#include +#endif + +using namespace Akonadi; +using namespace Akonadi::Protocol; + +DataStream::DataStream() + : mDev(nullptr) +{ +} + +DataStream::DataStream(QIODevice *device) + : mDev(device) +{ +} + +DataStream::~DataStream() +{ + // No flush() here. Throwing an exception in a destructor would go badly. The caller MUST call flush after writing. +} + +void DataStream::flush() +{ + if (!mWriteBuffer.isEmpty()) { + const int len = mWriteBuffer.size(); + int ret = mDev->write(mWriteBuffer); + if (ret != len) { + // TODO: Try to write data again unless ret is -1? + throw ProtocolException("Failed to write all data"); + } + mWriteBuffer.clear(); + } +} + +void DataStream::waitForData(QIODevice *device, int timeoutMs) +{ +#ifdef Q_OS_WIN + // Apparently readyRead() gets emitted sometimes even if there are no data + // so we will re-enter the wait again immediately + while (device->bytesAvailable() == 0) { + auto ls = qobject_cast(device); + if (ls && ls->state() != QLocalSocket::ConnectedState) { + throw ProtocolException("Socket not connected to server"); + } + + QEventLoop loop; + QObject::connect(device, &QIODevice::readyRead, &loop, &QEventLoop::quit); + if (ls) { + QObject::connect(ls, &QLocalSocket::stateChanged, &loop, &QEventLoop::quit); + } + bool timeout = false; + if (timeoutMs > 0) { + QTimer::singleShot(timeoutMs, &loop, [&]() { + timeout = true; + loop.quit(); + }); + } + loop.exec(); + if (timeout) { + throw ProtocolException("Timeout while waiting for data"); + } + if (ls && ls->state() != QLocalSocket::ConnectedState) { + throw ProtocolException("Socket not connected to server"); + } + } +#else + if (!device->waitForReadyRead(timeoutMs)) { + throw ProtocolException("Timeout while waiting for data"); + } +#endif +} + +QIODevice *DataStream::device() const +{ + return mDev; +} + +void DataStream::setDevice(QIODevice *device) +{ + mDev = device; +} + +std::chrono::milliseconds DataStream::waitTimeout() const +{ + return mWaitTimeout; +} +void DataStream::setWaitTimeout(std::chrono::milliseconds timeout) +{ + mWaitTimeout = timeout; +} + +void DataStream::waitForData(quint32 size) +{ + checkDevice(); + + while (mDev->bytesAvailable() < size) { + waitForData(mDev, mWaitTimeout.count()); + } +} + +void DataStream::writeRawData(const char *data, int len) +{ + checkDevice(); + + mWriteBuffer += QByteArray::fromRawData(data, len); +} + +void DataStream::writeBytes(const char *bytes, int len) +{ + *this << static_cast(len); + if (len) { + writeRawData(bytes, len); + } +} + +int DataStream::readRawData(char *buffer, int len) +{ + checkDevice(); + + return mDev->read(buffer, len); +} diff --git a/src/private/datastream_p_p.h b/src/private/datastream_p_p.h new file mode 100644 index 0000000..8ec75f3 --- /dev/null +++ b/src/private/datastream_p_p.h @@ -0,0 +1,327 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include +#include + +#include "akonadiprivate_export.h" +#include "protocol_exception_p.h" + +#include +#include +#include + +namespace Akonadi +{ +namespace Protocol +{ +class AKONADIPRIVATE_EXPORT DataStream +{ +public: + explicit DataStream(); + explicit DataStream(QIODevice *device); + ~DataStream(); + + static void waitForData(QIODevice *device, int timeoutMs); + + QIODevice *device() const; + void setDevice(QIODevice *device); + + std::chrono::milliseconds waitTimeout() const; + void setWaitTimeout(std::chrono::milliseconds timeout); + + void flush(); + + template inline typename std::enable_if::value, DataStream>::type &operator<<(T val); + template inline typename std::enable_if::value, DataStream>::type &operator<<(T val); + + inline DataStream &operator<<(const QString &str); + inline DataStream &operator<<(const QByteArray &data); + inline DataStream &operator<<(const QDateTime &dt); + + template inline typename std::enable_if::value, DataStream>::type &operator>>(T &val); + template inline typename std::enable_if::value, DataStream>::type &operator>>(T &val); + inline DataStream &operator>>(QString &str); + inline DataStream &operator>>(QByteArray &data); + inline DataStream &operator>>(QDateTime &dt); + + void writeRawData(const char *data, int len); + void writeBytes(const char *bytes, int len); + + int readRawData(char *buffer, int len); + + void waitForData(quint32 size); + +private: + Q_DISABLE_COPY(DataStream) + + inline void checkDevice() const + { + if (Q_UNLIKELY(!mDev)) { + throw ProtocolException("Device does not exist"); + } + } + + QIODevice *mDev; + QByteArray mWriteBuffer; + std::chrono::milliseconds mWaitTimeout = std::chrono::seconds{30}; +}; + +template inline typename std::enable_if::value, DataStream>::type &DataStream::operator<<(T val) +{ + checkDevice(); + writeRawData((char *)&val, sizeof(T)); + return *this; +} + +template inline typename std::enable_if::value, DataStream>::type &DataStream::operator<<(T val) +{ + return *this << (typename std::underlying_type::type)val; +} + +inline DataStream &DataStream::operator<<(const QString &str) +{ + if (str.isNull()) { + *this << (quint32)0xffffffff; + } else { + writeBytes(reinterpret_cast(str.unicode()), sizeof(QChar) * str.length()); + } + return *this; +} + +inline DataStream &DataStream::operator<<(const QByteArray &data) +{ + if (data.isNull()) { + *this << (quint32)0xffffffff; + } else { + writeBytes(data.constData(), data.size()); + } + return *this; +} + +inline DataStream &DataStream::operator<<(const QDateTime &dt) +{ + *this << dt.date().toJulianDay() << dt.time().msecsSinceStartOfDay() << dt.timeSpec(); + if (dt.timeSpec() == Qt::OffsetFromUTC) { + *this << dt.offsetFromUtc(); + } else if (dt.timeSpec() == Qt::TimeZone) { + *this << dt.timeZone().id(); + } + return *this; +} + +template inline typename std::enable_if::value, DataStream>::type &DataStream::operator>>(T &val) +{ + checkDevice(); + + waitForData(sizeof(T)); + + if (mDev->read((char *)&val, sizeof(T)) != sizeof(T)) { + throw Akonadi::ProtocolException("Failed to read enough data from stream"); + } + return *this; +} + +template inline typename std::enable_if::value, DataStream>::type &DataStream::operator>>(T &val) +{ + return *this >> reinterpret_cast::type &>(val); +} + +inline DataStream &DataStream::operator>>(QString &str) +{ + str.clear(); + + quint32 bytes = 0; + *this >> bytes; + if (bytes == 0xffffffff) { + return *this; + } else if (bytes == 0) { + str = QString(QLatin1String("")); + return *this; + } + + if (bytes & 0x1) { + str.clear(); + throw Akonadi::ProtocolException("Read corrupt data"); + } + + const quint32 step = 1024 * 1024; + const quint32 len = bytes / 2; + quint32 allocated = 0; + + while (allocated < len) { + const int blockSize = qMin(step, len - allocated); + waitForData(blockSize * sizeof(QChar)); + str.resize(allocated + blockSize); + if (readRawData(reinterpret_cast(str.data()) + allocated * sizeof(QChar), blockSize * sizeof(QChar)) != int(blockSize * sizeof(QChar))) { + throw Akonadi::ProtocolException("Failed to read enough data from stream"); + } + allocated += blockSize; + } + + return *this; +} + +inline DataStream &DataStream::operator>>(QByteArray &data) +{ + data.clear(); + + quint32 len = 0; + *this >> len; + if (len == 0xffffffff) { + return *this; + } + + const quint32 step = 1024 * 1024; + quint32 allocated = 0; + + while (allocated < len) { + const int blockSize = qMin(step, len - allocated); + waitForData(blockSize); + data.resize(allocated + blockSize); + if (readRawData(data.data() + allocated, blockSize) != blockSize) { + throw Akonadi::ProtocolException("Failed to read enough data from stream"); + } + allocated += blockSize; + } + + return *this; +} + +inline DataStream &DataStream::operator>>(QDateTime &dt) +{ + QDate date; + QTime time; + qint64 jd; + int mds; + Qt::TimeSpec spec; + + *this >> jd >> mds >> spec; + date = QDate::fromJulianDay(jd); + time = QTime::fromMSecsSinceStartOfDay(mds); + if (spec == Qt::OffsetFromUTC) { + int offset = 0; + *this >> offset; + dt = QDateTime(date, time, spec, offset); + } else if (spec == Qt::TimeZone) { + QByteArray id; + *this >> id; + dt = QDateTime(date, time, QTimeZone(id)); + } else { + dt = QDateTime(date, time, spec); + } + + return *this; +} + +} // namespace Protocol +} // namespace Akonadi + +// Inline functions + +template inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, QFlags flags) +{ + return stream << static_cast::Int>(flags); +} + +template inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, QFlags &flags) +{ + stream >> reinterpret_cast::Int &>(flags); + return stream; +} + +// Generic streaming for all Qt value-based containers (as well as std containers that +// implement operator<< for appending) +template class Container> +// typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Container &list) +{ + stream << (quint32)list.size(); + for (auto iter = list.cbegin(), end = list.cend(); iter != end; ++iter) { + stream << *iter; + } + return stream; +} + +template class Container> +// //typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Container &list) +{ + list.clear(); + quint32 size = 0; + stream >> size; + list.reserve(size); + for (quint32 i = 0; i < size; ++i) { + T t; + stream >> t; + list << t; + } + return stream; +} + +inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const QStringList &list) +{ + return stream << static_cast>(list); +} + +inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, QStringList &list) +{ + return stream >> static_cast &>(list); +} + +namespace Akonadi +{ +namespace Protocol +{ +namespace Private +{ +template class Container> inline void container_reserve(Container &container, int size) +{ + container.reserve(size); +} + +template inline void container_reserve(QMap &, int) +{ + // noop +} +} // namespace Private +} // namespace Protocol +} // namespace Akonadi + +// Generic streaming for all Qt dictionary-based containers +template class Container> +// typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Container &map) +{ + stream << (quint32)map.size(); + auto iter = map.cend(), begin = map.cbegin(); + while (iter != begin) { + --iter; + stream << iter.key() << iter.value(); + } + return stream; +} + +template class Container> +// typename std::enable_if::value, Akonadi::Protocol::DataStream>::type +inline Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Container &map) +{ + map.clear(); + quint32 size = 0; + stream >> size; + Akonadi::Protocol::Private::container_reserve(map, size); + for (quint32 i = 0; i < size; ++i) { + Key key; + Value value; + stream >> key >> value; + map.insertMulti(key, value); + } + return stream; +} + diff --git a/src/private/dbus.cpp b/src/private/dbus.cpp new file mode 100644 index 0000000..c9203a2 --- /dev/null +++ b/src/private/dbus.cpp @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbus_p.h" +#include "instance_p.h" + +#include +#include +#include +#include + +#include + +using namespace Akonadi; + +#define AKONADI_DBUS_SERVER_SERVICE u"org.freedesktop.Akonadi" +#define AKONADI_DBUS_CONTROL_SERVICE u"org.freedesktop.Akonadi.Control" +#define AKONADI_DBUS_CONTROL_SERVICE_LOCK u"org.freedesktop.Akonadi.Control.lock" +#define AKONADI_DBUS_AGENTSERVER_SERVICE u"org.freedesktop.Akonadi.AgentServer" +#define AKONADI_DBUS_STORAGEJANITOR_SERVICE u"org.freedesktop.Akonadi.Janitor" +#define AKONADI_DBUS_SERVER_SERVICE_UPGRADING u"org.freedesktop.Akonadi.upgrading" + +static QString makeServiceName(QStringView base) +{ + if (!Instance::hasIdentifier()) { + return base.toString(); + } + return base + QLatin1Char('.') + Instance::identifier(); +} + +QString DBus::serviceName(DBus::ServiceType serviceType) +{ + switch (serviceType) { + case Server: + return makeServiceName(AKONADI_DBUS_SERVER_SERVICE); + case Control: + return makeServiceName(AKONADI_DBUS_CONTROL_SERVICE); + case ControlLock: + return makeServiceName(AKONADI_DBUS_CONTROL_SERVICE_LOCK); + case AgentServer: + return makeServiceName(AKONADI_DBUS_AGENTSERVER_SERVICE); + case StorageJanitor: + return makeServiceName(AKONADI_DBUS_STORAGEJANITOR_SERVICE); + case UpgradeIndicator: + return makeServiceName(AKONADI_DBUS_SERVER_SERVICE_UPGRADING); + } + Q_ASSERT(!"WTF?"); + return QString(); +} + +std::optional DBus::parseAgentServiceName(const QString &serviceName) +{ + if (!serviceName.startsWith(AKONADI_DBUS_SERVER_SERVICE ".")) { + return std::nullopt; + } + + const auto parts = serviceName.midRef(QStringView(AKONADI_DBUS_SERVER_SERVICE ".").length()).split(QLatin1Char('.')); + if ((parts.size() == 2 && !Akonadi::Instance::hasIdentifier()) + || (parts.size() == 3 && Akonadi::Instance::hasIdentifier() && Akonadi::Instance::identifier() == parts.at(2))) { + // switch on parts.at( 0 ) + if (parts.at(0) == QLatin1String("Agent")) { + return AgentService{parts.at(1).toString(), DBus::Agent}; + } else if (parts.at(0) == QLatin1String("Resource")) { + return AgentService{parts.at(1).toString(), DBus::Resource}; + } else if (parts.at(0) == QLatin1String("Preprocessor")) { + return AgentService{parts.at(1).toString(), DBus::Preprocessor}; + } else { + return std::nullopt; + } + } + + return std::nullopt; +} + +QString DBus::agentServiceName(const QString &agentIdentifier, DBus::AgentType agentType) +{ + Q_ASSERT(!agentIdentifier.isEmpty()); + Q_ASSERT(agentType != Unknown); + QString serviceName = QStringLiteral(AKONADI_DBUS_SERVER_SERVICE "."); + switch (agentType) { + case Agent: + serviceName += QLatin1String("Agent."); + break; + case Resource: + serviceName += QLatin1String("Resource."); + break; + case Preprocessor: + serviceName += QLatin1String("Preprocessor."); + break; + default: + Q_ASSERT(!"WTF?"); + } + serviceName += agentIdentifier; + if (Akonadi::Instance::hasIdentifier()) { + serviceName += QLatin1Char('.') % Akonadi::Instance::identifier(); + } + return serviceName; +} + +std::optional DBus::parseInstanceIdentifier(const QString &serviceName) +{ + constexpr std::array services = {QStringView{AKONADI_DBUS_STORAGEJANITOR_SERVICE}, + QStringView{AKONADI_DBUS_SERVER_SERVICE_UPGRADING}, + QStringView{AKONADI_DBUS_AGENTSERVER_SERVICE}, + QStringView{AKONADI_DBUS_CONTROL_SERVICE_LOCK}, + QStringView{AKONADI_DBUS_CONTROL_SERVICE}}; + for (const auto &service : services) { + if (serviceName.startsWith(service)) { + if (serviceName != service) { + return serviceName.mid(service.length() + 1); // +1 for the separator "." + } + return std::nullopt; + } + } + + if (serviceName.startsWith(QStringView{AKONADI_DBUS_SERVER_SERVICE})) { + const auto split = serviceName.splitRef(QLatin1Char('.')); + if (split.size() <= 3) { + return std::nullopt; + } + + // [0]org.[1]freedesktop.[2]Akonadi.[3]type.[4]identifier.[5]instance + if (split[3] == QStringView{u"Agent"} || split[3] == QStringView{u"Resource"} || split[3] == QStringView{u"Preprocessor"}) { + if (split.size() == 6) { + return split[5].toString(); + } else { + return std::nullopt; + } + } else { + return split[3].toString(); + } + } + + return std::nullopt; +} diff --git a/src/private/dbus_p.h b/src/private/dbus_p.h new file mode 100644 index 0000000..dc1e181 --- /dev/null +++ b/src/private/dbus_p.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" + +#include + +#include + +/** + * Helper methods for obtaining D-Bus identifiers. + * This should be used instead of hardcoded identifiers or constants to support multi-instance namespacing + * @since 1.7 + */ + +#define AKONADI_DBUS_AGENTMANAGER_PATH "/AgentManager" +#define AKONADI_DBUS_AGENTSERVER_PATH "/AgentServer" +#define AKONADI_DBUS_STORAGEJANITOR_PATH "/Janitor" + +namespace Akonadi +{ +namespace DBus +{ +/** D-Bus service types used by the Akonadi server processes. */ +enum ServiceType { + Server, + Control, + ControlLock, + AgentServer, + StorageJanitor, + UpgradeIndicator, +}; + +/** + * Returns the service name for the given @p serviceType. + */ +AKONADIPRIVATE_EXPORT QString serviceName(ServiceType serviceType); + +/** Known D-Bus service name types for agents. */ +enum AgentType { + Unknown, + Agent, + Resource, + Preprocessor, +}; + +struct AgentService { + QString identifier{}; + DBus::AgentType agentType{DBus::Unknown}; +}; + +/** + * Parses a D-Bus service name and checks if it belongs to an agent of this instance. + * @param serviceName The service name to parse. + * @return The identifier of the agent, empty string if that's not an agent (or an agent of a different Akonadi instance) + */ +AKONADIPRIVATE_EXPORT std::optional parseAgentServiceName(const QString &serviceName); + +/** + * Returns the D-Bus service name of the agent @p agentIdentifier for type @p agentType. + */ +AKONADIPRIVATE_EXPORT QString agentServiceName(const QString &agentIdentifier, DBus::AgentType agentType); + +/** + * Returns the Akonadi instance name encoded in the service name. + */ +AKONADIPRIVATE_EXPORT std::optional parseInstanceIdentifier(const QString &serviceName); + +} + +} + diff --git a/src/private/externalpartstorage.cpp b/src/private/externalpartstorage.cpp new file mode 100644 index 0000000..573d6da --- /dev/null +++ b/src/private/externalpartstorage.cpp @@ -0,0 +1,311 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "akonadiprivate_debug.h" +#include "externalpartstorage_p.h" +#include "standarddirs_p.h" + +#include +#include +#include +#include + +using namespace Akonadi; + +ExternalPartStorageTransaction::ExternalPartStorageTransaction() +{ + ExternalPartStorage::self()->beginTransaction(); +} + +ExternalPartStorageTransaction::~ExternalPartStorageTransaction() +{ + if (ExternalPartStorage::self()->inTransaction()) { + rollback(); + } +} + +bool ExternalPartStorageTransaction::commit() +{ + return ExternalPartStorage::self()->commitTransaction(); +} + +bool ExternalPartStorageTransaction::rollback() +{ + return ExternalPartStorage::self()->rollbackTransaction(); +} + +ExternalPartStorage::ExternalPartStorage() +{ +} + +ExternalPartStorage *ExternalPartStorage::self() +{ + static ExternalPartStorage sInstance; + return &sInstance; +} + +QString ExternalPartStorage::resolveAbsolutePath(const QByteArray &filename, bool *exists, bool legacyFallback) +{ + return resolveAbsolutePath(QString::fromLocal8Bit(filename), exists, legacyFallback); +} + +QString ExternalPartStorage::resolveAbsolutePath(const QString &filename, bool *exists, bool legacyFallback) +{ + if (exists) { + *exists = false; + } + + QFileInfo finfo(filename); + if (finfo.isAbsolute()) { + if (exists && finfo.exists()) { + *exists = true; + } + return filename; + } + + const QString basePath = StandardDirs::saveDir("data", QStringLiteral("file_db_data")); + Q_ASSERT(!basePath.isEmpty()); + + // Part files are stored in levelled cache. We use modulo 100 of the partID + // to ensure even distribution of the part files into the subfolders. + // PartID is encoded in filename as "PARTID_rX". + const int revPos = filename.indexOf(QLatin1Char('_')); + const QString path = basePath + QDir::separator() + (revPos > 1 ? filename[revPos - 2] : QLatin1Char('0')) + + (revPos > 0 ? filename[revPos - 1] : QLatin1Char('0')) + QDir::separator() + filename; + // If legacy fallback is disabled, return it in any case + if (!legacyFallback) { + QFileInfo finfo(path); + QDir().mkpath(finfo.path()); + return path; + } + + // ..otherwise return it only if it exists + if (QFile::exists(path)) { + if (exists) { + *exists = true; + } + return path; + } + + // .. and fallback to legacy if it does not, but only when legacy exists + const QString legacyPath = basePath + QDir::separator() + filename; + if (QFile::exists(legacyPath)) { + if (exists) { + *exists = true; + } + return legacyPath; + } else { + QFileInfo finfo(path); + QDir().mkpath(finfo.path()); + // If neither legacy or new path exists, return the new path, so that + // new items are created in the correct location + return path; + } +} + +bool ExternalPartStorage::createPartFile(const QByteArray &data, qint64 partId, QByteArray &partFileName) +{ + bool exists = false; + partFileName = updateFileNameRevision(QByteArray::number(partId)); + const QString path = resolveAbsolutePath(partFileName, &exists); + if (exists) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to create a part" << partFileName << ", which already exists!"; + return false; + } + + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for writing:" << f.errorString(); + return false; + } + if (f.write(data) != data.size()) { + // TODO: Maybe just try again? + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file"; + return false; + } + f.close(); + + if (inTransaction()) { + addToTransaction({{Operation::Create, path}}); + } + return true; +} + +bool ExternalPartStorage::updatePartFile(const QByteArray &newData, const QByteArray &partFile, QByteArray &newPartFile) +{ + bool exists = false; + const QString currentPartPath = resolveAbsolutePath(partFile, &exists); + if (!exists) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update a non-existent part, aborting update"; + return false; + } + + newPartFile = updateFileNameRevision(partFile); + exists = false; + const QString newPartPath = resolveAbsolutePath(newPartFile, &exists); + if (exists) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: asked to update part" << partFile << ", but" << newPartFile << "already exists, aborting update"; + return false; + } + + QFile f(newPartPath); + if (!f.open(QIODevice::WriteOnly)) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to open new part file for update:" << f.errorString(); + return false; + } + + if (f.write(newData) != newData.size()) { + // TODO: Maybe just try again? + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to write all data into the part file"; + return false; + } + f.close(); + + if (inTransaction()) { + addToTransaction({{Operation::Create, newPartPath}, {Operation::Delete, currentPartPath}}); + } else { + if (!QFile::remove(currentPartPath)) { + // Not a reason to fail the operation + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove old part payload file" << currentPartPath; + } + } + + return true; +} + +bool ExternalPartStorage::removePartFile(const QString &partFile) +{ + if (inTransaction()) { + addToTransaction({{Operation::Delete, partFile}}); + } else { + if (!QFile::remove(partFile)) { + // Not a reason to fail the operation + qCWarning(AKONADIPRIVATE_LOG) << "Error: failed to remove part file" << partFile; + } + } + + return true; +} + +QByteArray ExternalPartStorage::updateFileNameRevision(const QByteArray &filename) +{ + const int revIndex = filename.indexOf("_r"); + if (revIndex > -1) { + QByteArray rev = filename.mid(revIndex + 2); + int r = rev.toInt(); + r++; + rev = QByteArray::number(r); + return filename.left(revIndex + 2) + rev; + } + + return filename + "_r0"; +} + +QByteArray ExternalPartStorage::nameForPartId(qint64 partId) +{ + return QByteArray::number(partId) + "_r0"; +} + +bool ExternalPartStorage::beginTransaction() +{ + QMutexLocker locker(&mTransactionLock); + if (mTransactions.contains(QThread::currentThread())) { + qCWarning(AKONADIPRIVATE_LOG) << "Error: there is already a transaction in progress in this thread"; + return false; + } + + mTransactions.insert(QThread::currentThread(), QVector()); + return true; +} + +QString ExternalPartStorage::akonadiStoragePath() +{ + return StandardDirs::saveDir("data", QStringLiteral("file_db_data")); +} + +bool ExternalPartStorage::commitTransaction() +{ + QMutexLocker locker(&mTransactionLock); + auto iter = mTransactions.find(QThread::currentThread()); + if (iter == mTransactions.end()) { + qCWarning(AKONADIPRIVATE_LOG) << "Commit error: there is no transaction in progress in this thread"; + return false; + } + + const QVector trx = iter.value(); + mTransactions.erase(iter); + locker.unlock(); + + return replayTransaction(trx, true); +} + +bool ExternalPartStorage::rollbackTransaction() +{ + QMutexLocker locker(&mTransactionLock); + auto iter = mTransactions.find(QThread::currentThread()); + if (iter == mTransactions.end()) { + qCWarning(AKONADIPRIVATE_LOG) << "Rollback error: there is no transaction in progress in this thread"; + return false; + } + + const QVector trx = iter.value(); + mTransactions.erase(iter); + locker.unlock(); + + return replayTransaction(trx, false); +} + +bool ExternalPartStorage::inTransaction() const +{ + QMutexLocker locker(&mTransactionLock); + return mTransactions.contains(QThread::currentThread()); +} + +void ExternalPartStorage::addToTransaction(const QVector &ops) +{ + QMutexLocker locker(&mTransactionLock); + auto iter = mTransactions.find(QThread::currentThread()); + Q_ASSERT(iter != mTransactions.end()); + locker.unlock(); + + for (const Operation &op : ops) { + iter->append(op); + } +} + +bool ExternalPartStorage::replayTransaction(const QVector &trx, bool commit) +{ + for (auto iter = trx.constBegin(), end = trx.constEnd(); iter != end; ++iter) { + const Operation &op = *iter; + + if (op.type == Operation::Create) { + if (commit) { + // no-op: we actually created that already in createPart()/updatePart() + } else { + if (!QFile::remove(op.filename)) { + // We failed to remove the file, but don't abort the rollback. + // This is an error, but does not cause data loss. + qCWarning(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while rolling back a transaction"; + } + } + } else if (op.type == Operation::Delete) { + if (commit) { + if (!QFile::remove(op.filename)) { + // We failed to remove the file, but don't abort the commit. + // This is an error, but does not cause data loss. + qCWarning(AKONADIPRIVATE_LOG) << "Warning: failed to remove" << op.filename << "while committing a transaction"; + } + } else { + // no-op: we did not actually delete the file yet + } + } else { + Q_UNREACHABLE(); + } + } + + return true; +} diff --git a/src/private/externalpartstorage_p.h b/src/private/externalpartstorage_p.h new file mode 100644 index 0000000..dba64fb --- /dev/null +++ b/src/private/externalpartstorage_p.h @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +// krazy:excludeall=dpointer + +#pragma once + +#include "akonadiprivate_export.h" + +#include +#include +#include + +class QString; +class QByteArray; +class QThread; + +namespace Akonadi +{ +class AKONADIPRIVATE_EXPORT ExternalPartStorageTransaction +{ +public: + explicit ExternalPartStorageTransaction(); + ~ExternalPartStorageTransaction(); + + bool commit(); + bool rollback(); + +private: + Q_DISABLE_COPY(ExternalPartStorageTransaction) +}; + +/** + * Provides access to external payload part file storage + * + * Use ExternalPartStorageTransaction to delay deletion of part files until + * commit. Files created during the transaction will be deleted when transaction + * is rolled back to keep the storage clean. + */ +class AKONADIPRIVATE_EXPORT ExternalPartStorage +{ +public: + static ExternalPartStorage *self(); + + static QString resolveAbsolutePath(const QByteArray &filename, bool *exists = nullptr, bool legacyFallback = true); + static QString resolveAbsolutePath(const QString &filename, bool *exists = nullptr, bool legacyFallback = true); + static QByteArray updateFileNameRevision(const QByteArray &filename); + static QByteArray nameForPartId(qint64 partId); + static QString akonadiStoragePath(); + + bool updatePartFile(const QByteArray &newData, const QByteArray &partFile, QByteArray &newPartFile); + bool createPartFile(const QByteArray &newData, qint64 partId, QByteArray &partFileName); + bool removePartFile(const QString &partFile); + + bool inTransaction() const; + +private: + friend class ExternalPartStorageTransaction; + + struct Operation { + enum Type { + Create, + Delete + // We never update files, we always create a new one with increased + // revision number, hence no "Update" + }; + + Type type; + QString filename; + }; + + ExternalPartStorage(); + + bool beginTransaction(); + bool commitTransaction(); + bool rollbackTransaction(); + + bool replayTransaction(const QVector &trx, bool commit); + void addToTransaction(const QVector &ops); + + mutable QMutex mTransactionLock; + QHash> mTransactions; +}; + +} + diff --git a/src/private/imapparser.cpp b/src/private/imapparser.cpp new file mode 100644 index 0000000..acb3cb3 --- /dev/null +++ b/src/private/imapparser.cpp @@ -0,0 +1,686 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imapparser_p.h" + +#include + +#include + +using namespace Akonadi; + +class ImapParser::Private +{ +public: + QByteArray tagBuffer; + QByteArray dataBuffer; + int parenthesesCount; + qint64 literalSize; + bool continuation; + + // returns true if readBuffer contains a literal start and sets + // parser state accordingly + bool checkLiteralStart(const QByteArray &readBuffer, int pos = 0) + { + if (readBuffer.trimmed().endsWith('}')) { + const int begin = readBuffer.lastIndexOf('{'); + const int end = readBuffer.lastIndexOf('}'); + + // new literal in previous literal data block + if (begin < pos) { + return false; + } + + // TODO error handling + literalSize = readBuffer.mid(begin + 1, end - begin - 1).toLongLong(); + + // empty literal + if (literalSize == 0) { + return false; + } + + continuation = true; + dataBuffer.reserve(dataBuffer.size() + static_cast(literalSize) + 1); + return true; + } + return false; + } +}; + +namespace +{ +template int parseParenthesizedListHelper(const QByteArray &data, T &result, int start) +{ + result.clear(); + if (start >= data.length()) { + return data.length(); + } + + const int begin = data.indexOf('(', start); + if (begin < 0) { + return start; + } + + result.reserve(16); + + int count = 0; + int sublistBegin = start; + bool insideQuote = false; + for (int i = begin + 1; i < data.length(); ++i) { + const char currentChar = data[i]; + if (currentChar == '(' && !insideQuote) { + ++count; + if (count == 1) { + sublistBegin = i; + } + + continue; + } + + if (currentChar == ')' && !insideQuote) { + if (count <= 0) { + return i + 1; + } + + if (count == 1) { + result.append(data.mid(sublistBegin, i - sublistBegin + 1)); + } + + --count; + continue; + } + + if (currentChar == ' ' || currentChar == '\n' || currentChar == '\r') { + continue; + } + + if (count == 0) { + QByteArray ba; + const int consumed = ImapParser::parseString(data, ba, i); + i = consumed - 1; // compensate for the for loop increment + result.append(ba); + } else if (count > 0) { + if (currentChar == '"') { + insideQuote = !insideQuote; + } else if (currentChar == '\\' && insideQuote) { + ++i; + continue; + } + } + } + + return data.length(); +} + +} // namespace + +int ImapParser::parseParenthesizedList(const QByteArray &data, QVarLengthArray &result, int start) +{ + return parseParenthesizedListHelper(data, result, start); +} + +int ImapParser::parseParenthesizedList(const QByteArray &data, QList &result, int start) +{ + return parseParenthesizedListHelper(data, result, start); +} + +int ImapParser::parseString(const QByteArray &data, QByteArray &result, int start) +{ + int begin = stripLeadingSpaces(data, start); + result.clear(); + if (begin >= data.length()) { + return data.length(); + } + + // literal string + // TODO: error handling + if (data[begin] == '{') { + int end = data.indexOf('}', begin); + Q_ASSERT(end > begin); + int size = data.mid(begin + 1, end - begin - 1).toInt(); + + // strip CRLF + begin = end + 1; + if (begin < data.length() && data[begin] == '\r') { + ++begin; + } + if (begin < data.length() && data[begin] == '\n') { + ++begin; + } + + end = begin + size; + result = data.mid(begin, end - begin); + return end; + } + + // quoted string + return parseQuotedString(data, result, begin); +} + +int ImapParser::parseQuotedString(const QByteArray &data, QByteArray &result, int start) +{ + int begin = stripLeadingSpaces(data, start); + int end = begin; + result.clear(); + if (begin >= data.length()) { + return data.length(); + } + + bool foundSlash = false; + // quoted string + if (data[begin] == '"') { + ++begin; + result.reserve(qMin(32, data.size() - begin)); + for (int i = begin; i < data.length(); ++i) { + const char ch = data.at(i); + if (foundSlash) { + foundSlash = false; + if (ch == 'r') { + result += '\r'; + } else if (ch == 'n') { + result += '\n'; + } else if (ch == '\\') { + result += '\\'; + } else if (ch == '\"') { + result += '\"'; + } else { + // TODO: this is actually an error + result += ch; + } + continue; + } + if (ch == '\\') { + foundSlash = true; + continue; + } + if (ch == '"') { + end = i + 1; // skip the '"' + break; + } + result += ch; + } + } else { + // unquoted string + bool reachedInputEnd = true; + for (int i = begin; i < data.length(); ++i) { + const char ch = data.at(i); + if (ch == ' ' || ch == '(' || ch == ')' || ch == '\n' || ch == '\r') { + end = i; + reachedInputEnd = false; + break; + } + if (ch == '\\') { + foundSlash = true; + } + } + if (reachedInputEnd) { + end = data.length(); + } + result = data.mid(begin, end - begin); + + // transform unquoted NIL + if (result == "NIL") { + result.clear(); + } + + // strip quotes + if (foundSlash) { + while (result.contains("\\\"")) { + result.replace("\\\"", "\""); + } + while (result.contains("\\\\")) { + result.replace("\\\\", "\\"); + } + } + } + + return end; +} + +int ImapParser::stripLeadingSpaces(const QByteArray &data, int start) +{ + for (int i = start; i < data.length(); ++i) { + if (data[i] != ' ') { + return i; + } + } + + return data.length(); +} + +int ImapParser::parenthesesBalance(const QByteArray &data, int start) +{ + int count = 0; + bool insideQuote = false; + for (int i = start; i < data.length(); ++i) { + const char ch = data[i]; + if (ch == '"') { + insideQuote = !insideQuote; + continue; + } + if (ch == '\\' && insideQuote) { + ++i; + continue; + } + if (ch == '(' && !insideQuote) { + ++count; + continue; + } + if (ch == ')' && !insideQuote) { + --count; + continue; + } + } + return count; +} + +QByteArray ImapParser::join(const QList &list, const QByteArray &separator) +{ + // shortcuts for the easy cases + if (list.isEmpty()) { + return QByteArray(); + } + if (list.size() == 1) { + return list.first(); + } + + // avoid expensive realloc's by determining the size beforehand + QList::const_iterator it = list.constBegin(); + const QList::const_iterator endIt = list.constEnd(); + int resultSize = (list.size() - 1) * separator.size(); + for (; it != endIt; ++it) { + resultSize += (*it).size(); + } + + QByteArray result; + result.reserve(resultSize); + it = list.constBegin(); + result += (*it); + ++it; + for (; it != endIt; ++it) { + result += separator; + result += (*it); + } + + return result; +} + +QByteArray ImapParser::join(const QSet &set, const QByteArray &separator) +{ + const QList list(set.begin(), set.end()); + + return ImapParser::join(list, separator); +} + +int ImapParser::parseString(const QByteArray &data, QString &result, int start) +{ + QByteArray tmp; + const int end = parseString(data, tmp, start); + result = QString::fromUtf8(tmp); + return end; +} + +int ImapParser::parseNumber(const QByteArray &data, qint64 &result, bool *ok, int start) +{ + if (ok) { + *ok = false; + } + + int pos = stripLeadingSpaces(data, start); + if (pos >= data.length()) { + return data.length(); + } + + int begin = pos; + for (; pos < data.length(); ++pos) { + if (!isdigit(data.at(pos))) { + break; + } + } + + const QByteArray tmp = data.mid(begin, pos - begin); + result = tmp.toLongLong(ok); + + return pos; +} + +QByteArray ImapParser::quote(const QByteArray &data) +{ + if (data.isEmpty()) { + static const QByteArray empty("\"\""); + return empty; + } + + const int inputLength = data.length(); + int stuffToQuote = 0; + for (int i = 0; i < inputLength; ++i) { + const char ch = data.at(i); + if (ch == '"' || ch == '\\' || ch == '\n' || ch == '\r') { + ++stuffToQuote; + } + } + + QByteArray result; + result.reserve(inputLength + stuffToQuote + 2); + result += '"'; + + // shortcut for the case that we don't need to quote anything at all + if (stuffToQuote == 0) { + result += data; + } else { + for (int i = 0; i < inputLength; ++i) { + const char ch = data.at(i); + if (ch == '\n') { + result += "\\n"; + continue; + } + + if (ch == '\r') { + result += "\\r"; + continue; + } + + if (ch == '"' || ch == '\\') { + result += '\\'; + } + + result += ch; + } + } + + result += '"'; + return result; +} + +int ImapParser::parseSequenceSet(const QByteArray &data, ImapSet &result, int start) +{ + int begin = stripLeadingSpaces(data, start); + qint64 value = -1; + qint64 lower = -1; + qint64 upper = -1; + for (int i = begin; i < data.length(); ++i) { + if (data[i] == '*') { + value = 0; + } else if (data[i] == ':') { + lower = value; + } else if (isdigit(data[i])) { + bool ok = false; + i = parseNumber(data, value, &ok, i); + Q_ASSERT(ok); // TODO handle error + --i; + } else { + upper = value; + if (lower < 0) { + lower = value; + } + result.add(ImapInterval(lower, upper)); + lower = -1; + upper = -1; // NOLINT(clang-analyzer-deadcode.DeadStores) // false positive? + value = -1; + if (data[i] != ',') { + return i; + } + } + } + // take care of left-overs at input end + upper = value; + if (lower < 0) { + lower = value; + } + + if (lower >= 0 && upper >= 0) { + result.add(ImapInterval(lower, upper)); + } + + return data.length(); +} + +int ImapParser::parseDateTime(const QByteArray &data, QDateTime &dateTime, int start) +{ + // Syntax: + // date-time = DQUOTE date-day-fixed "-" date-month "-" date-year + // SP time SP zone DQUOTE + // date-day-fixed = (SP DIGIT) / 2DIGIT + // ; Fixed-format version of date-day + // date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / + // "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" + // date-year = 4DIGIT + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT + // ; Hours minutes seconds + // zone = ("+" / "-") 4DIGIT + // ; Signed four-digit value of hhmm representing + // ; hours and minutes east of Greenwich (that is, + // ; the amount that the given time differs from + // ; Universal Time). Subtracting the timezone + // ; from the given time will give the UT form. + // ; The Universal Time zone is "+0000". + // Example : "28-May-2006 01:03:35 +0200" + // Position: 0123456789012345678901234567 + // 1 2 + + int pos = stripLeadingSpaces(data, start); + if (data.length() <= pos) { + return pos; + } + + bool quoted = false; + if (data[pos] == '"') { + quoted = true; + ++pos; + + if (data.length() <= pos + 26) { + return start; + } + } else { + if (data.length() < pos + 26) { + return start; + } + } + + bool ok = true; + const int day = (data[pos] == ' ' ? data[pos + 1] - '0' // single digit day + : data.mid(pos, 2).toInt(&ok)); + if (!ok) { + return start; + } + + pos += 3; + static const QByteArray shortMonthNames("janfebmaraprmayjunjulaugsepoctnovdec"); + int month = shortMonthNames.indexOf(data.mid(pos, 3).toLower()); + if (month == -1) { + return start; + } + + month = month / 3 + 1; + pos += 4; + const int year = data.mid(pos, 4).toInt(&ok); + if (!ok) { + return start; + } + + pos += 5; + const int hours = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 3; + const int minutes = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 3; + const int seconds = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 4; + const int tzhh = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + pos += 2; + const int tzmm = data.mid(pos, 2).toInt(&ok); + if (!ok) { + return start; + } + + int tzsecs = tzhh * 60 * 60 + tzmm * 60; + if (data[pos - 3] == '-') { + tzsecs = -tzsecs; + } + + const QDate date(year, month, day); + const QTime time(hours, minutes, seconds); + dateTime = QDateTime(date, time, Qt::UTC); + if (!dateTime.isValid()) { + return start; + } + + dateTime = dateTime.addSecs(-tzsecs); + + pos += 2; + if (data.length() <= pos || !quoted) { + return pos; + } + + if (data[pos] == '"') { + ++pos; + } + + return pos; +} + +void ImapParser::splitVersionedKey(const QByteArray &data, QByteArray &key, int &version) +{ + const int startPos = data.indexOf('['); + const int endPos = data.indexOf(']'); + if (startPos != -1 && endPos != -1) { + if (endPos > startPos) { + bool ok = false; + + version = data.mid(startPos + 1, endPos - startPos - 1).toInt(&ok); + if (!ok) { + version = 0; + } + + key = data.left(startPos); + } + } else { + key = data; + version = 0; + } +} + +ImapParser::ImapParser() + : d(new Private) +{ + reset(); +} + +ImapParser::~ImapParser() +{ + delete d; +} + +bool ImapParser::parseNextLine(const QByteArray &readBuffer) +{ + d->continuation = false; + + // first line, get the tag + if (d->tagBuffer.isEmpty()) { + const int startOfData = ImapParser::parseString(readBuffer, d->tagBuffer); + if (startOfData < readBuffer.length() && startOfData >= 0) { + d->dataBuffer = readBuffer.mid(startOfData + 1); + } + + } else { + d->dataBuffer += readBuffer; + } + + // literal read in progress + if (d->literalSize > 0) { + d->literalSize -= readBuffer.size(); + + // still not everything read + if (d->literalSize > 0) { + return false; + } + + // check the remaining (non-literal) part for parentheses + if (d->literalSize < 0) { + // the following looks strange but works since literalSize can be negative here + d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer, readBuffer.length() + static_cast(d->literalSize)); + + // check if another literal read was started + if (d->checkLiteralStart(readBuffer, readBuffer.length() + static_cast(d->literalSize))) { + return false; + } + } + + // literal string finished but still open parentheses + if (d->parenthesesCount > 0) { + return false; + } + + } else { + // open parentheses + d->parenthesesCount += ImapParser::parenthesesBalance(readBuffer); + + // start new literal read + if (d->checkLiteralStart(readBuffer)) { + return false; + } + + // still open parentheses + if (d->parenthesesCount > 0) { + return false; + } + + // just a normal response, fall through + } + + return true; +} + +void ImapParser::parseBlock(const QByteArray &data) +{ + Q_ASSERT(d->literalSize >= data.size()); + d->literalSize -= data.size(); + d->dataBuffer += data; +} + +QByteArray ImapParser::tag() const +{ + return d->tagBuffer; +} + +QByteArray ImapParser::data() const +{ + return d->dataBuffer; +} + +void ImapParser::reset() +{ + d->dataBuffer.clear(); + d->tagBuffer.clear(); + d->parenthesesCount = 0; + d->literalSize = 0; + d->continuation = false; +} + +bool ImapParser::continuationStarted() const +{ + return d->continuation; +} + +qint64 ImapParser::continuationSize() const +{ + return d->literalSize; +} diff --git a/src/private/imapparser_p.h b/src/private/imapparser_p.h new file mode 100644 index 0000000..f0bbf4a --- /dev/null +++ b/src/private/imapparser_p.h @@ -0,0 +1,196 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" + +#include "imapset_p.h" + +#include +#include +#include + +namespace Akonadi +{ +/** + Parser for IMAP messages. +*/ +class AKONADIPRIVATE_EXPORT ImapParser +{ +public: + /** + Parses the next parenthesized list in @p data starting from @p start + and puts the result into @p result. The number of used characters is + returned. + This does not recurse into sub-lists. + @param data Source data. + @param result The parsed list. + @param start Start parsing at this index. + */ + static int parseParenthesizedList(const QByteArray &data, QList &result, int start = 0); + static int parseParenthesizedList(const QByteArray &data, QVarLengthArray &result, int start = 0); + + /** + Parse the next string in @p data (quoted or literal) starting from @p start + and puts the result into @p result. The number of used characters is returned + (this is not equal to result.length()!). + @param data Source data. + @param result Parsed string, quotation, literal marker, etc. are removed, + 'NIL' is transformed into an empty QByteArray. + @param start start parsing at this index. + */ + static int parseString(const QByteArray &data, QByteArray &result, int start = 0); + + /** + Parses the next quoted string from @p data starting at @p start and puts it into + @p result. The number of parsed characters is returned (this is not equal to result.length()!). + @param data Source data. + @param result Parsed string, quotation is removed and 'NIL' is transformed to an empty QByteArray. + @param start Start parsing at this index. + */ + static int parseQuotedString(const QByteArray &data, QByteArray &result, int start = 0); + + /** + Returns the number of leading espaces in @p data starting from @p start. + @param data The source data. + @param start Start parsing at this index. + */ + static int stripLeadingSpaces(const QByteArray &data, int start = 0); + + /** + Returns the parentheses balance for the given data, considering quotes. + @param data The source data. + @param start Start parsing at this index. + */ + static int parenthesesBalance(const QByteArray &data, int start = 0); + + /** + Joins a QByteArray list with the given separator. + @param list The QByteArray list to join. + @param separator The separator. + */ + static QByteArray join(const QList &list, const QByteArray &separator); + + /** + Joins a QByteArray set with the given separator. + @param set The QByteArray set to join. + @param separator The separator. + */ + static QByteArray join(const QSet &set, const QByteArray &separator); + + /** + Same as parseString(), but with additional UTF-8 decoding of the result. + @param data Source data. + @param result Parsed string, quotation, literal marker, etc. are removed, + 'NIL' is transformed into an empty QString. UTF-8 decoding is applied.. + @param start Start parsing at this index. + */ + static int parseString(const QByteArray &data, QString &result, int start = 0); + + /** + Parses the next integer number from @p data starting at start and puts it into + @p result. The number of characters parsed is returned (this is not the parsed result!). + @param data Source data. + @param result Parsed integer number, invalid if ok is false. + @param ok Set to false if the parsing failed. + @param start Start parsing at this index. + */ + static int parseNumber(const QByteArray &data, qint64 &result, bool *ok = nullptr, int start = 0); + + /** + Quotes the given QByteArray. + @param data Source data. + */ + static QByteArray quote(const QByteArray &data); + + /** + Parse an IMAP sequence set. + @param data source data. + @param result The parse sequence set. + @param start start parsing at this index. + @return end position of parsing. + */ + static int parseSequenceSet(const QByteArray &data, ImapSet &result, int start = 0); + + /** + Parse an IMAP date/time value. + @param data source data. + @param dateTime The result date/time. + @param start Start parsing at this index. + @return end position of parsing. + */ + static int parseDateTime(const QByteArray &data, QDateTime &dateTime, int start = 0); + + /** + Split a versioned key of the form 'key[version]' into its components. + @param data The versioned key. + @param key The unversioned key. + @param version The version of the key or 0 if no version was set. + */ + static void splitVersionedKey(const QByteArray &data, QByteArray &key, int &version); + + /** + Constructs a new IMAP parser. + */ + ImapParser(); + + /** + Destroys an IMAP parser. + */ + ~ImapParser(); + + /** + Parses the given line. + @returns True if an IMAP message was parsed completely, false if more data is needed. + @todo read from a QIODevice directly to avoid an extra line buffer + */ + bool parseNextLine(const QByteArray &readBuffer); + + /** + Parses the given block of data. + Note: This currently only handles continuation blocks. + @param data The data to parse. + */ + void parseBlock(const QByteArray &data); + + /** + Returns the tag of the parsed message. + Only valid if parseNextLine() returned true. + */ + QByteArray tag() const; + + /** + Return the raw data of the parsed IMAP message. + Only valid if parseNextLine() returned true. + */ + QByteArray data() const; + + /** + Resets the internal state of the parser. Call before parsing + a new IMAP message. + */ + void reset(); + + /** + Returns true if the last parsed line contained a literal continuation, + ie. readiness for receiving literal data needs to be indicated. + */ + bool continuationStarted() const; + + /** + Returns the expected size of liteal data. + */ + qint64 continuationSize() const; + +private: + Q_DISABLE_COPY(ImapParser) + class Private; + Private *const d; +}; + +} + diff --git a/src/private/imapset.cpp b/src/private/imapset.cpp new file mode 100644 index 0000000..f5b9598 --- /dev/null +++ b/src/private/imapset.cpp @@ -0,0 +1,295 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "datastream_p_p.h" +#include "imapset_p.h" + +#include + +#include + +namespace Akonadi +{ +class ImapInterval::Private : public QSharedData +{ +public: + Id begin = 0; + Id end = 0; +}; + +class ImapSet::Private : public QSharedData +{ +public: + template void add(const T &values) + { + T vals = values; + std::sort(vals.begin(), vals.end()); + for (int i = 0; i < vals.count(); ++i) { + const int begin = vals[i]; + Q_ASSERT(begin >= 0); + if (i == vals.count() - 1) { + intervals << ImapInterval(begin, begin); + break; + } + do { + ++i; + Q_ASSERT(vals[i] >= 0); + if (vals[i] != (vals[i - 1] + 1)) { + --i; + break; + } + } while (i < vals.count() - 1); + intervals << ImapInterval(begin, vals[i]); + } + } + + ImapInterval::List intervals; +}; + +ImapInterval::ImapInterval() + : d(new Private) +{ +} + +ImapInterval::ImapInterval(const ImapInterval &other) + : d(other.d) +{ +} + +ImapInterval::ImapInterval(Id begin, Id end) + : d(new Private) +{ + d->begin = begin; + d->end = end; +} + +ImapInterval::~ImapInterval() = default; + +ImapInterval &ImapInterval::operator=(const ImapInterval &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool ImapInterval::operator==(const ImapInterval &other) const +{ + return (d->begin == other.d->begin && d->end == other.d->end); +} + +ImapInterval::Id ImapInterval::size() const +{ + if (!d->begin && !d->end) { + return 0; + } + + return (d->end - d->begin + 1); +} + +bool ImapInterval::hasDefinedBegin() const +{ + return (d->begin != 0); +} + +ImapInterval::Id ImapInterval::begin() const +{ + return d->begin; +} + +bool ImapInterval::hasDefinedEnd() const +{ + return (d->end != 0); +} + +ImapInterval::Id ImapInterval::end() const +{ + if (hasDefinedEnd()) { + return d->end; + } + + return std::numeric_limits::max(); +} + +void ImapInterval::setBegin(Id value) +{ + Q_ASSERT(value >= 0); + Q_ASSERT(value <= d->end || !hasDefinedEnd()); + d->begin = value; +} + +void ImapInterval::setEnd(Id value) +{ + Q_ASSERT(value >= 0); + Q_ASSERT(value >= d->begin || !hasDefinedBegin()); + d->end = value; +} + +QByteArray Akonadi::ImapInterval::toImapSequence() const +{ + if (size() == 0) { + return QByteArray(); + } + + if (size() == 1) { + return QByteArray::number(d->begin); + } + + QByteArray rv = QByteArray::number(d->begin) + ':'; + + if (hasDefinedEnd()) { + rv += QByteArray::number(d->end); + } else { + rv += '*'; + } + + return rv; +} + +ImapSet::ImapSet() + : d(new Private) +{ +} + +ImapSet::ImapSet(Id id) + : d(new Private) +{ + add(QVector() << id); +} + +ImapSet::ImapSet(const QVector &ids) + : d(new Private) +{ + add(ids); +} + +ImapSet::ImapSet(const QList &ids) + : d(new Private) +{ + add(ids); +} + +ImapSet::ImapSet(const ImapInterval &interval) + : d(new Private) +{ + add(interval); +} + +ImapSet::ImapSet(const ImapSet &other) + : d(other.d) +{ +} + +ImapSet::~ImapSet() +{ +} + +ImapSet ImapSet::all() +{ + ImapSet set; + set.add(ImapInterval(1, 0)); + return set; +} + +ImapSet &ImapSet::operator=(const ImapSet &other) +{ + if (this != &other) { + d = other.d; + } + + return *this; +} + +bool ImapSet::operator==(const ImapSet &other) const +{ + return d->intervals == other.d->intervals; +} + +void ImapSet::add(const QVector &values) +{ + d->add(values); +} + +void ImapSet::add(const QList &values) +{ + d->add(values); +} + +void ImapSet::add(const QSet &values) +{ + QVector v; + v.reserve(values.size()); + for (QSet::ConstIterator iter = values.constBegin(); iter != values.constEnd(); ++iter) { + v.push_back(*iter); + } + + add(v); +} + +void ImapSet::add(const ImapInterval &interval) +{ + d->intervals << interval; +} + +QByteArray ImapSet::toImapSequenceSet() const +{ + QByteArray rv; + for (auto iter = d->intervals.cbegin(), end = d->intervals.cend(); iter != end; ++iter) { + if (iter != d->intervals.cbegin()) { + rv += ','; + } + rv += iter->toImapSequence(); + } + + return rv; +} + +ImapInterval::List ImapSet::intervals() const +{ + return d->intervals; +} + +bool ImapSet::isEmpty() const +{ + return d->intervals.isEmpty() || (d->intervals.size() == 1 && d->intervals.at(0).size() == 0); +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapInterval &interval) +{ + return stream << interval.d->begin << interval.d->end; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapInterval &interval) +{ + return stream >> interval.d->begin >> interval.d->end; +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapSet &set) +{ + return stream << set.d->intervals; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapSet &set) +{ + return stream >> set.d->intervals; +} + +} // namespace Akonadi + +using namespace Akonadi; + +QDebug operator<<(QDebug d, const Akonadi::ImapInterval &interval) +{ + d << interval.toImapSequence(); + return d; +} + +QDebug operator<<(QDebug d, const Akonadi::ImapSet &set) +{ + d << set.toImapSequenceSet(); + return d; +} diff --git a/src/private/imapset_p.h b/src/private/imapset_p.h new file mode 100644 index 0000000..afceb25 --- /dev/null +++ b/src/private/imapset_p.h @@ -0,0 +1,229 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" + +#include +#include +#include +#include +#include + +namespace Akonadi +{ +namespace Protocol +{ +class DataStream; +} +} + +namespace Akonadi +{ +/** + Represents a single interval in an ImapSet. + This class is implicitly shared. +*/ +class AKONADIPRIVATE_EXPORT ImapInterval +{ +public: + /** + * Describes the ids stored in the interval. + */ + using Id = qint64; + + /** + A list of ImapInterval objects. + */ + using List = QList; + + /** + Constructs an interval that covers all positive numbers. + */ + ImapInterval(); + + /** + Copy constructor. + */ + ImapInterval(const ImapInterval &other); + + /** + Create a new interval. + @param begin The begin of the interval. + @param end Keep default (0) to just set the interval begin + */ + explicit ImapInterval(Id begin, Id end = 0); + + /** + Destructor. + */ + ~ImapInterval(); + + /** + Assignment operator. + */ + ImapInterval &operator=(const ImapInterval &other); + + /** + Comparison operator. + */ + bool operator==(const ImapInterval &other) const; + + /** + Returns the size of this interval. + Size is only defined for finite intervals. + */ + Id size() const; + + /** + Returns true if this interval has a defined begin. + */ + bool hasDefinedBegin() const; + + /** + Returns the begin of this interval. The value is the smallest value part of the interval. + Only valid if begin is defined. + */ + Id begin() const; + + /** + Returns true if this intercal has been defined. + */ + bool hasDefinedEnd() const; + + /** + Returns the end of this interval. This value is the largest value part of the interval. + Only valid if hasDefinedEnd() returned true. + */ + Id end() const; + + /** + Sets the begin of the interval. + */ + void setBegin(Id value); + + /** + Sets the end of this interval. + */ + void setEnd(Id value); + + /** + Converts this set into an IMAP compatible sequence. + */ + QByteArray toImapSequence() const; + +private: + class Private; + QSharedDataPointer d; + + friend Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapInterval &interval); + friend Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapInterval &interval); +}; + +/** + Represents a set of natural numbers (1->\f$\infty\f$) in a as compact as possible form. + Used to address Akonadi items via the IMAP protocol or in the database. + This class is implicitly shared. +*/ +class AKONADIPRIVATE_EXPORT ImapSet +{ +public: + /** + * Describes the ids stored in the set. + */ + using Id = qint64; + + /** + Constructs an empty set. + */ + ImapSet(); + + ImapSet(qint64 Id); // krazy:exclude=explicit + + ImapSet(const QVector &ids); // krazy:exclude=explicit + + ImapSet(const QList &ids); // krazy:exclude=explicit + + ImapSet(const ImapInterval &interval); // krazy:exclude=explicit + + /** + Copy constructor. + */ + ImapSet(const ImapSet &other); + + /** + Destructor. + */ + ~ImapSet(); + + /** + * Returns ImapSet representing 1:* + * */ + static ImapSet all(); + + /** + Assignment operator. + */ + ImapSet &operator=(const ImapSet &other); + + bool operator==(const ImapSet &other) const; + + /** + Adds the given list of positive integer numbers to the set. + The list is sorted and split into as large as possible intervals. + No interval merging is performed. + @param values List of positive integer numbers in arbitrary order + */ + void add(const QVector &values); + void add(const QList &values); + + /** + * @overload + */ + void add(const QSet &values); + + /** + Adds the given ImapInterval to this set. + No interval merging is performed. + */ + void add(const ImapInterval &interval); + + /** + Returns a IMAP-compatible QByteArray representation of this set. + */ + QByteArray toImapSequenceSet() const; + + /** + Returns the intervals this set consists of. + */ + ImapInterval::List intervals() const; + + /** + Returns true if this set doesn't contains any values. + */ + bool isEmpty() const; + +private: + class Private; + QSharedDataPointer d; + + friend Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::ImapSet &set); + friend Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::ImapSet &set); +}; + +} + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug d, const Akonadi::ImapInterval &interval); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug d, const Akonadi::ImapSet &set); + +Q_DECLARE_TYPEINFO(Akonadi::ImapInterval, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(Akonadi::ImapSet, Q_MOVABLE_TYPE); + +Q_DECLARE_METATYPE(Akonadi::ImapInterval) +Q_DECLARE_METATYPE(Akonadi::ImapInterval::List) +Q_DECLARE_METATYPE(Akonadi::ImapSet) + diff --git a/src/private/instance.cpp b/src/private/instance.cpp new file mode 100644 index 0000000..efecf98 --- /dev/null +++ b/src/private/instance.cpp @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "instance_p.h" + +#include +#include + +using namespace Akonadi; + +namespace +{ +Q_GLOBAL_STATIC(QString, sIdentifier) // NOLINT(readability-redundant-member-init) + +void loadIdentifier() +{ + *sIdentifier = QString::fromUtf8(qgetenv("AKONADI_INSTANCE")); + if (sIdentifier->isNull()) { + // QString is null by default, which means it wasn't initialized + // yet. Set it to empty when it is initialized + *sIdentifier = QStringLiteral(""); // clazy:exclude=empty-qstringliteral + } +} +} // namespace + +bool Instance::hasIdentifier() +{ + if (::sIdentifier->isNull()) { + ::loadIdentifier(); + } + return !sIdentifier->isEmpty(); +} + +void Instance::setIdentifier(const QString &identifier) +{ + if (identifier.isNull()) { + qunsetenv("AKONADI_INSTANCE"); + *::sIdentifier = QStringLiteral(""); // clazy:exclude=empty-qstringliteral + } else { + *::sIdentifier = identifier; + qputenv("AKONADI_INSTANCE", identifier.toUtf8()); + } +} + +QString Instance::identifier() +{ + if (::sIdentifier->isNull()) { + ::loadIdentifier(); + } + return *::sIdentifier; +} diff --git a/src/private/instance_p.h b/src/private/instance_p.h new file mode 100644 index 0000000..09b3a4d --- /dev/null +++ b/src/private/instance_p.h @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include "akonadiprivate_export.h" + +class QString; + +namespace Akonadi +{ +namespace Instance +{ +AKONADIPRIVATE_EXPORT bool hasIdentifier(); +AKONADIPRIVATE_EXPORT void setIdentifier(const QString &identifier); +AKONADIPRIVATE_EXPORT QString identifier(); +} +} + diff --git a/src/private/protocol.cpp b/src/private/protocol.cpp new file mode 100644 index 0000000..a74fed9 --- /dev/null +++ b/src/private/protocol.cpp @@ -0,0 +1,848 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * SPDX-FileCopyrightText: 2016 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "datastream_p_p.h" +#include "imapset_p.h" +#include "protocol_p.h" +#include "scope_p.h" + +#include +#include + +#include +#include +#include + +#include + +// clazy:excludeall=function-args-by-value + +#undef AKONADI_DECLARE_PRIVATE +#define AKONADI_DECLARE_PRIVATE(Class) \ + inline Class##Private *Class::d_func() \ + { \ + return reinterpret_cast(d_ptr.data()); \ + } \ + inline const Class##Private *Class::d_func() const \ + { \ + return reinterpret_cast(d_ptr.constData()); \ + } + +#define COMPARE(prop) ((prop) == ((decltype(this))other)->prop) + +namespace Akonadi +{ +namespace Protocol +{ +QDebug operator<<(QDebug _dbg, Command::Type type) +{ + QDebug dbg(_dbg.noquote()); + + switch (type) { + case Command::Invalid: + return dbg << "Invalid"; + + case Command::Hello: + return dbg << "Hello"; + case Command::Login: + return dbg << "Login"; + case Command::Logout: + return dbg << "Logout"; + + case Command::Transaction: + return dbg << "Transaction"; + + case Command::CreateItem: + return dbg << "CreateItem"; + case Command::CopyItems: + return dbg << "CopyItems"; + case Command::DeleteItems: + return dbg << "DeleteItems"; + case Command::FetchItems: + return dbg << "FetchItems"; + case Command::LinkItems: + return dbg << "LinkItems"; + case Command::ModifyItems: + return dbg << "ModifyItems"; + case Command::MoveItems: + return dbg << "MoveItems"; + + case Command::CreateCollection: + return dbg << "CreateCollection"; + case Command::CopyCollection: + return dbg << "CopyCollection"; + case Command::DeleteCollection: + return dbg << "DeleteCollection"; + case Command::FetchCollections: + return dbg << "FetchCollections"; + case Command::FetchCollectionStats: + return dbg << "FetchCollectionStats"; + case Command::ModifyCollection: + return dbg << "ModifyCollection"; + case Command::MoveCollection: + return dbg << "MoveCollection"; + + case Command::Search: + return dbg << "Search"; + case Command::SearchResult: + return dbg << "SearchResult"; + case Command::StoreSearch: + return dbg << "StoreSearch"; + + case Command::CreateTag: + return dbg << "CreateTag"; + case Command::DeleteTag: + return dbg << "DeleteTag"; + case Command::FetchTags: + return dbg << "FetchTags"; + case Command::ModifyTag: + return dbg << "ModifyTag"; + + case Command::FetchRelations: + return dbg << "FetchRelations"; + case Command::ModifyRelation: + return dbg << "ModifyRelation"; + case Command::RemoveRelations: + return dbg << "RemoveRelations"; + + case Command::SelectResource: + return dbg << "SelectResource"; + + case Command::StreamPayload: + return dbg << "StreamPayload"; + case Command::ItemChangeNotification: + return dbg << "ItemChangeNotification"; + case Command::CollectionChangeNotification: + return dbg << "CollectionChangeNotification"; + case Command::TagChangeNotification: + return dbg << "TagChangeNotification"; + case Command::RelationChangeNotification: + return dbg << "RelationChangeNotification"; + case Command::SubscriptionChangeNotification: + return dbg << "SubscriptionChangeNotification"; + case Command::DebugChangeNotification: + return dbg << "DebugChangeNotification"; + case Command::CreateSubscription: + return dbg << "CreateSubscription"; + case Command::ModifySubscription: + return dbg << "ModifySubscription"; + + case Command::_ResponseBit: + Q_ASSERT(false); + return dbg << static_cast(type); + } + + Q_ASSERT(false); + return dbg << static_cast(type); +} + +template DataStream &operator<<(DataStream &stream, const QSharedPointer &ptr) +{ + Protocol::serialize(stream, ptr); + return stream; +} + +template DataStream &operator>>(DataStream &stream, QSharedPointer &ptr) +{ + ptr = Protocol::deserialize(stream.device()).staticCast(); + return stream; +} + +/******************************************************************************/ + +Command::Command(quint8 type) + : mType(type) +{ +} + +bool Command::operator==(const Command &other) const +{ + return mType == other.mType; +} + +void Command::toJson(QJsonObject &json) const +{ + json[QStringLiteral("response")] = static_cast(mType & Command::_ResponseBit); + +#define case_label(x) \ + case Command::x: { \ + json[QStringLiteral("type")] = QStringLiteral(#x); \ + } break; + + switch (mType & ~Command::_ResponseBit) { + case_label(Invalid) case_label(Hello) + + case_label(Login) case_label(Logout) + + case_label(Transaction) + + case_label(CreateItem) case_label(CopyItems) case_label(DeleteItems) case_label(FetchItems) case_label(LinkItems) case_label(ModifyItems) + case_label(MoveItems) + + case_label(CreateCollection) case_label(CopyCollection) case_label(DeleteCollection) case_label(FetchCollections) + case_label(FetchCollectionStats) case_label(ModifyCollection) case_label(MoveCollection) + + case_label(Search) case_label(SearchResult) case_label(StoreSearch) + + case_label(CreateTag) case_label(DeleteTag) case_label(FetchTags) case_label(ModifyTag) + + case_label(FetchRelations) case_label(ModifyRelation) case_label(RemoveRelations) + + case_label(SelectResource) + + case_label(StreamPayload) case_label(CreateSubscription) case_label(ModifySubscription) + + case_label(DebugChangeNotification) case_label(ItemChangeNotification) + case_label(CollectionChangeNotification) case_label(TagChangeNotification) + case_label(RelationChangeNotification) case_label(SubscriptionChangeNotification) + } +#undef case_label +} + +DataStream &operator<<(DataStream &stream, const Command &cmd) +{ + return stream << cmd.mType; +} + +DataStream &operator>>(DataStream &stream, Command &cmd) +{ + return stream >> cmd.mType; +} + +QDebug operator<<(QDebug dbg, const Command &cmd) +{ + return dbg.noquote() << ((cmd.mType & Command::_ResponseBit) ? "Response:" : "Command:") << static_cast(cmd.mType & ~Command::_ResponseBit) + << "\n"; +} + +void toJson(const Akonadi::Protocol::Command *command, QJsonObject &json) +{ +#define case_notificationlabel(x, class) \ + case Command::x: { \ + static_cast(command)->toJson(json); \ + } break; +#define case_commandlabel(x, cmd, resp) \ + case Command::x: { \ + static_cast(command)->toJson(json); \ + } break; \ + case Command::x | Command::_ResponseBit: { \ + static_cast(command)->toJson(json); \ + } break; + + switch (command->mType) { + case Command::Invalid: + break; + + case Command::Hello | Command::_ResponseBit: { + static_cast(command)->toJson(json); + } break; + case_commandlabel(Login, LoginCommand, LoginResponse) case_commandlabel(Logout, LogoutCommand, LogoutResponse) + + case_commandlabel(Transaction, TransactionCommand, TransactionResponse) + + case_commandlabel(CreateItem, CreateItemCommand, CreateItemResponse) case_commandlabel(CopyItems, CopyItemsCommand, CopyItemsResponse) + case_commandlabel(DeleteItems, DeleteItemsCommand, DeleteItemsResponse) case_commandlabel(FetchItems, FetchItemsCommand, FetchItemsResponse) + case_commandlabel(LinkItems, LinkItemsCommand, LinkItemsResponse) case_commandlabel( + ModifyItems, + ModifyItemsCommand, + ModifyItemsResponse) case_commandlabel(MoveItems, MoveItemsCommand, MoveItemsResponse) + + case_commandlabel(CreateCollection, CreateCollectionCommand, CreateCollectionResponse) case_commandlabel( + CopyCollection, + CopyCollectionCommand, + CopyCollectionResponse) case_commandlabel(DeleteCollection, DeleteCollectionCommand, DeleteCollectionResponse) + case_commandlabel(FetchCollections, FetchCollectionsCommand, FetchCollectionsResponse) case_commandlabel( + FetchCollectionStats, + FetchCollectionStatsCommand, + FetchCollectionStatsResponse) case_commandlabel(ModifyCollection, ModifyCollectionCommand, ModifyCollectionResponse) + case_commandlabel(MoveCollection, MoveCollectionCommand, MoveCollectionResponse) + + case_commandlabel(Search, SearchCommand, SearchResponse) case_commandlabel( + SearchResult, + SearchResultCommand, + SearchResultResponse) case_commandlabel(StoreSearch, StoreSearchCommand, StoreSearchResponse) + + case_commandlabel(CreateTag, CreateTagCommand, CreateTagResponse) + case_commandlabel(DeleteTag, DeleteTagCommand, DeleteTagResponse) + case_commandlabel(FetchTags, FetchTagsCommand, FetchTagsResponse) + case_commandlabel(ModifyTag, ModifyTagCommand, ModifyTagResponse) + + case_commandlabel(FetchRelations, FetchRelationsCommand, FetchRelationsResponse) + case_commandlabel(ModifyRelation, ModifyRelationCommand, ModifyRelationResponse) + case_commandlabel(RemoveRelations, RemoveRelationsCommand, RemoveRelationsResponse) + + case_commandlabel(SelectResource, SelectResourceCommand, SelectResourceResponse) + + case_commandlabel(StreamPayload, StreamPayloadCommand, StreamPayloadResponse) + case_commandlabel( + CreateSubscription, + CreateSubscriptionCommand, + CreateSubscriptionResponse) case_commandlabel(ModifySubscription, + ModifySubscriptionCommand, + ModifySubscriptionResponse) + + case_notificationlabel(DebugChangeNotification, DebugChangeNotification) + case_notificationlabel(ItemChangeNotification, ItemChangeNotification) + case_notificationlabel(CollectionChangeNotification, + CollectionChangeNotification) + case_notificationlabel(TagChangeNotification, + TagChangeNotification) + case_notificationlabel(RelationChangeNotification, + RelationChangeNotification) + case_notificationlabel(SubscriptionChangeNotification, + SubscriptionChangeNotification) + } +#undef case_notificationlabel +#undef case_commandlabel +} + +/******************************************************************************/ + +Response::Response() + : Command(Command::Invalid | Command::_ResponseBit) + , mErrorCode(0) +{ +} + +Response::Response(Command::Type type) + : Command(type | Command::_ResponseBit) + , mErrorCode(0) +{ +} + +bool Response::operator==(const Response &other) const +{ + return *static_cast(this) == static_cast(other) && mErrorCode == other.mErrorCode && mErrorMsg == other.mErrorMsg; +} + +void Response::toJson(QJsonObject &json) const +{ + static_cast(this)->toJson(json); + if (isError()) { + QJsonObject error; + error[QStringLiteral("code")] = errorCode(); + error[QStringLiteral("message")] = errorMessage(); + json[QStringLiteral("error")] = error; + } else { + json[QStringLiteral("error")] = false; + } +} + +DataStream &operator<<(DataStream &stream, const Response &cmd) +{ + return stream << static_cast(cmd) << cmd.mErrorCode << cmd.mErrorMsg; +} + +DataStream &operator>>(DataStream &stream, Response &cmd) +{ + return stream >> static_cast(cmd) >> cmd.mErrorCode >> cmd.mErrorMsg; +} + +QDebug operator<<(QDebug dbg, const Response &resp) +{ + return dbg.noquote() << static_cast(resp) << "Error code:" << resp.mErrorCode << "\n" + << "Error msg:" << resp.mErrorMsg << "\n"; +} + +/******************************************************************************/ + +class FactoryPrivate +{ +public: + typedef CommandPtr (*CommandFactoryFunc)(); + typedef ResponsePtr (*ResponseFactoryFunc)(); + + FactoryPrivate() + { + // Session management + registerType(); + registerType(); + registerType(); + + // Transactions + registerType(); + + // Items + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + + // Collections + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + + // Search + registerType(); + registerType(); + registerType(); + + // Tag + registerType(); + registerType(); + registerType(); + registerType(); + + // Relation + registerType(); + registerType(); + registerType(); + + // Resources + registerType(); + + // Other...? + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + registerType(); + } + + // clang has problem resolving the right qHash() overload for Command::Type, + // so use its underlying integer type instead + QHash::type, QPair> registrar; + +private: + template static CommandPtr commandFactoryFunc() + { + return QSharedPointer::create(); + } + template static ResponsePtr responseFactoryFunc() + { + return QSharedPointer::create(); + } + + template void registerType() + { + CommandFactoryFunc cmdFunc = &commandFactoryFunc; + ResponseFactoryFunc respFunc = &responseFactoryFunc; + registrar.insert(T, qMakePair(cmdFunc, respFunc)); + } +}; + +Q_GLOBAL_STATIC(FactoryPrivate, sFactoryPrivate) // NOLINT(readability-redundant-member-init) + +CommandPtr Factory::command(Command::Type type) +{ + auto iter = sFactoryPrivate->registrar.constFind(type); + if (iter == sFactoryPrivate->registrar.constEnd()) { + return QSharedPointer::create(); + } + return iter->first(); +} + +ResponsePtr Factory::response(Command::Type type) +{ + auto iter = sFactoryPrivate->registrar.constFind(type); + if (iter == sFactoryPrivate->registrar.constEnd()) { + return QSharedPointer::create(); + } + return iter->second(); +} + +/******************************************************************************/ + +/******************************************************************************/ + +bool ItemFetchScope::operator==(const ItemFetchScope &other) const +{ + return mRequestedParts == other.mRequestedParts && mChangedSince == other.mChangedSince && mAncestorDepth == other.mAncestorDepth && mFlags == other.mFlags; +} + +QVector ItemFetchScope::requestedPayloads() const +{ + QVector rv; + std::copy_if(mRequestedParts.begin(), mRequestedParts.end(), std::back_inserter(rv), [](const QByteArray &ba) { + return ba.startsWith("PLD:"); + }); + return rv; +} + +void ItemFetchScope::setFetch(FetchFlags attributes, bool fetch) +{ + if (fetch) { + mFlags |= attributes; + if (attributes & FullPayload) { + if (!mRequestedParts.contains(AKONADI_PARAM_PLD_RFC822)) { + mRequestedParts << AKONADI_PARAM_PLD_RFC822; + } + } + } else { + mFlags &= ~attributes; + } +} + +bool ItemFetchScope::fetch(FetchFlags flags) const +{ + if (flags == None) { + return mFlags == None; + } else { + return mFlags & flags; + } +} + +void ItemFetchScope::toJson(QJsonObject &json) const +{ + json[QStringLiteral("flags")] = static_cast(mFlags); + json[QStringLiteral("ChangedSince")] = mChangedSince.toString(); + json[QStringLiteral("AncestorDepth")] = static_cast::type>(mAncestorDepth); + + QJsonArray requestedPartsArray; + for (const auto &part : std::as_const(mRequestedParts)) { + requestedPartsArray.append(QString::fromUtf8(part)); + } + json[QStringLiteral("RequestedParts")] = requestedPartsArray; +} + +QDebug operator<<(QDebug dbg, ItemFetchScope::AncestorDepth depth) +{ + switch (depth) { + case ItemFetchScope::NoAncestor: + return dbg << "No ancestor"; + case ItemFetchScope::ParentAncestor: + return dbg << "Parent ancestor"; + case ItemFetchScope::AllAncestors: + return dbg << "All ancestors"; + } + Q_UNREACHABLE(); +} + +DataStream &operator<<(DataStream &stream, const ItemFetchScope &scope) +{ + return stream << scope.mRequestedParts << scope.mChangedSince << scope.mAncestorDepth << scope.mFlags; +} + +DataStream &operator>>(DataStream &stream, ItemFetchScope &scope) +{ + return stream >> scope.mRequestedParts >> scope.mChangedSince >> scope.mAncestorDepth >> scope.mFlags; +} + +QDebug operator<<(QDebug dbg, const ItemFetchScope &scope) +{ + return dbg.noquote() << "FetchScope(\n" + << "Fetch Flags:" << scope.mFlags << "\n" + << "Changed Since:" << scope.mChangedSince << "\n" + << "Ancestor Depth:" << scope.mAncestorDepth << "\n" + << "Requested Parts:" << scope.mRequestedParts << ")\n"; +} + +/******************************************************************************/ + +ScopeContext::ScopeContext(Type type, qint64 id) +{ + if (type == ScopeContext::Tag) { + mTagCtx = id; + } else if (type == ScopeContext::Collection) { + mColCtx = id; + } +} + +ScopeContext::ScopeContext(Type type, const QString &ctx) +{ + if (type == ScopeContext::Tag) { + mTagCtx = ctx; + } else if (type == ScopeContext::Collection) { + mColCtx = ctx; + } +} + +bool ScopeContext::operator==(const ScopeContext &other) const +{ + return mColCtx == other.mColCtx && mTagCtx == other.mTagCtx; +} + +void ScopeContext::toJson(QJsonObject &json) const +{ + if (isEmpty()) { + json[QStringLiteral("scopeContext")] = false; + } else if (hasContextId(ScopeContext::Tag)) { + json[QStringLiteral("scopeContext")] = QStringLiteral("tag"); + json[QStringLiteral("TagID")] = contextId(ScopeContext::Tag); + } else if (hasContextId(ScopeContext::Collection)) { + json[QStringLiteral("scopeContext")] = QStringLiteral("collection"); + json[QStringLiteral("ColID")] = contextId(ScopeContext::Collection); + } else if (hasContextRID(ScopeContext::Tag)) { + json[QStringLiteral("scopeContext")] = QStringLiteral("tagrid"); + json[QStringLiteral("TagRID")] = contextRID(ScopeContext::Tag); + } else if (hasContextRID(ScopeContext::Collection)) { + json[QStringLiteral("scopeContext")] = QStringLiteral("colrid"); + json[QStringLiteral("ColRID")] = contextRID(ScopeContext::Collection); + } +} + +DataStream &operator<<(DataStream &stream, const ScopeContext &context) +{ + // We don't have a custom generic DataStream streaming operator for QVariant + // because it's very hard, esp. without access to QVariant private + // stuff, so we have to decompose it manually here. + QVariant::Type vType = context.mColCtx.type(); + stream << vType; + if (vType == QVariant::LongLong) { + stream << context.mColCtx.toLongLong(); + } else if (vType == QVariant::String) { + stream << context.mColCtx.toString(); + } + + vType = context.mTagCtx.type(); + stream << vType; + if (vType == QVariant::LongLong) { + stream << context.mTagCtx.toLongLong(); + } else if (vType == QVariant::String) { + stream << context.mTagCtx.toString(); + } + + return stream; +} + +DataStream &operator>>(DataStream &stream, ScopeContext &context) +{ + QVariant::Type vType; + qint64 id; + QString rid; + + for (ScopeContext::Type type : {ScopeContext::Collection, ScopeContext::Tag}) { + stream >> vType; + if (vType == QVariant::LongLong) { + stream >> id; + context.setContext(type, id); + } else if (vType == QVariant::String) { + stream >> rid; + context.setContext(type, rid); + } + } + + return stream; +} + +QDebug operator<<(QDebug _dbg, const ScopeContext &ctx) +{ + QDebug dbg(_dbg.noquote()); + dbg << "ScopeContext("; + if (ctx.isEmpty()) { + dbg << "empty"; + } else if (ctx.hasContextId(ScopeContext::Tag)) { + dbg << "Tag ID:" << ctx.contextId(ScopeContext::Tag); + } else if (ctx.hasContextId(ScopeContext::Collection)) { + dbg << "Col ID:" << ctx.contextId(ScopeContext::Collection); + } else if (ctx.hasContextRID(ScopeContext::Tag)) { + dbg << "Tag RID:" << ctx.contextRID(ScopeContext::Tag); + } else if (ctx.hasContextRID(ScopeContext::Collection)) { + dbg << "Col RID:" << ctx.contextRID(ScopeContext::Collection); + } + return dbg << ")\n"; +} + +/******************************************************************************/ + +ChangeNotification::ChangeNotification(Command::Type type) + : Command(type) +{ +} + +bool ChangeNotification::operator==(const ChangeNotification &other) const +{ + return static_cast(*this) == other && mSessionId == other.mSessionId; + // metadata are not compared +} + +QList ChangeNotification::itemsToUids(const QVector &items) +{ + QList rv; + rv.reserve(items.size()); + std::transform(items.cbegin(), items.cend(), std::back_inserter(rv), [](const FetchItemsResponse &item) { + return item.id(); + }); + return rv; +} + +bool ChangeNotification::isRemove() const +{ + switch (type()) { + case Command::Invalid: + return false; + case Command::ItemChangeNotification: + return static_cast(this)->operation() == ItemChangeNotification::Remove; + case Command::CollectionChangeNotification: + return static_cast(this)->operation() == CollectionChangeNotification::Remove; + case Command::TagChangeNotification: + return static_cast(this)->operation() == TagChangeNotification::Remove; + case Command::RelationChangeNotification: + return static_cast(this)->operation() == RelationChangeNotification::Remove; + case Command::SubscriptionChangeNotification: + return static_cast(this)->operation() == SubscriptionChangeNotification::Remove; + case Command::DebugChangeNotification: + return false; + default: + Q_ASSERT_X(false, __FUNCTION__, "Unknown ChangeNotification type"); + } + + return false; +} + +bool ChangeNotification::isMove() const +{ + switch (type()) { + case Command::Invalid: + return false; + case Command::ItemChangeNotification: + return static_cast(this)->operation() == ItemChangeNotification::Move; + case Command::CollectionChangeNotification: + return static_cast(this)->operation() == CollectionChangeNotification::Move; + case Command::TagChangeNotification: + case Command::RelationChangeNotification: + case Command::SubscriptionChangeNotification: + case Command::DebugChangeNotification: + return false; + default: + Q_ASSERT_X(false, __FUNCTION__, "Unknown ChangeNotification type"); + } + + return false; +} + +bool ChangeNotification::appendAndCompress(ChangeNotificationList &list, const ChangeNotificationPtr &msg) +{ + // It is likely that compressable notifications are within the last few notifications, so avoid searching a list that is potentially huge + static const int maxCompressionSearchLength = 10; + int searchCounter = 0; + // There are often multiple Collection Modify notifications in the queue, + // so we optimize for this case. + + if (msg->type() == Command::CollectionChangeNotification) { + const auto &cmsg = Protocol::cmdCast(msg); + if (cmsg.operation() == CollectionChangeNotification::Modify) { + // We are iterating from end, since there's higher probability of finding + // matching notification + for (auto iter = list.end(), begin = list.begin(); iter != begin;) { + --iter; + if ((*iter)->type() == Protocol::Command::CollectionChangeNotification) { + auto &it = Protocol::cmdCast(*iter); + const auto &msgCol = cmsg.collection(); + const auto &itCol = it.collection(); + if (msgCol.id() == itCol.id() && msgCol.remoteId() == itCol.remoteId() && msgCol.remoteRevision() == itCol.remoteRevision() + && msgCol.resource() == itCol.resource() && cmsg.destinationResource() == it.destinationResource() + && cmsg.parentCollection() == it.parentCollection() && cmsg.parentDestCollection() == it.parentDestCollection()) { + // both are modifications, merge them together and drop the new one + if (cmsg.operation() == CollectionChangeNotification::Modify && it.operation() == CollectionChangeNotification::Modify) { + const auto parts = it.changedParts(); + it.setChangedParts(parts + cmsg.changedParts()); + return false; + } + + // we found Add notification, which means we can drop this modification + if (it.operation() == CollectionChangeNotification::Add) { + return false; + } + } + } + searchCounter++; + if (searchCounter >= maxCompressionSearchLength) { + break; + } + } + } + } + + // All other cases are just append, as the compression becomes too expensive in large batches + list.append(msg); + return true; +} + +void ChangeNotification::toJson(QJsonObject &json) const +{ + static_cast(this)->toJson(json); + json[QStringLiteral("session")] = QString::fromUtf8(mSessionId); + + QJsonArray metadata; + for (const auto &m : std::as_const(mMetaData)) { + metadata.append(QString::fromUtf8(m)); + } + json[QStringLiteral("metadata")] = metadata; +} + +DataStream &operator<<(DataStream &stream, const ChangeNotification &ntf) +{ + return stream << static_cast(ntf) << ntf.mSessionId; +} + +DataStream &operator>>(DataStream &stream, ChangeNotification &ntf) +{ + return stream >> static_cast(ntf) >> ntf.mSessionId; +} + +QDebug operator<<(QDebug dbg, const ChangeNotification &ntf) +{ + return dbg.noquote() << static_cast(ntf) << "Session:" << ntf.mSessionId << "\n" + << "MetaData:" << ntf.mMetaData << "\n"; +} + +DataStream &operator>>(DataStream &stream, ChangeNotification::Relation &relation) +{ + return stream >> relation.type >> relation.leftId >> relation.rightId; +} + +DataStream &operator<<(DataStream &stream, const ChangeNotification::Relation &relation) +{ + return stream << relation.type << relation.leftId << relation.rightId; +} + +QDebug operator<<(QDebug _dbg, const ChangeNotification::Relation &rel) +{ + QDebug dbg(_dbg.noquote()); + return dbg << "Left: " << rel.leftId << ", Right:" << rel.rightId << ", Type: " << rel.type; +} + +} // namespace Protocol +} // namespace Akonadi + +// Helpers for the generated code +namespace Akonadi +{ +namespace Protocol +{ +template class Container> inline bool containerComparator(const Container &c1, const Container &c2) +{ + return c1 == c2; +} + +template class Container> +inline bool containerComparator(const Container> &c1, const Container> &c2) +{ + if (c1.size() != c2.size()) { + return false; + } + + for (auto it1 = c1.cbegin(), it2 = c2.cbegin(), end1 = c1.cend(); it1 != end1; ++it1, ++it2) { + if (**it1 != **it2) { + return false; + } + } + return true; +} + +} // namespace Protocol +} // namespace Akonadi + +/******************************************************************************/ + +// Here comes the generated protocol implementation +#include "protocol_gen.cpp" + +/******************************************************************************/ diff --git a/src/private/protocol.xml b/src/private/protocol.xml new file mode 100644 index 0000000..b005868 --- /dev/null +++ b/src/private/protocol.xml @@ -0,0 +1,1112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/private/protocol_exception_p.h b/src/private/protocol_exception_p.h new file mode 100644 index 0000000..fdacb71 --- /dev/null +++ b/src/private/protocol_exception_p.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +// krazy:excludeall=dpointer,inline + +#pragma once + +#include "akonadiprivate_export.h" + +#include +#include + +#include + +namespace Akonadi +{ +class AKONADIPRIVATE_EXPORT ProtocolException : public std::exception +{ +public: + explicit ProtocolException(const char *what) + : std::exception() + , mWhat(what) + { + std::cerr << "ProtocolException thrown:" << what << std::endl; + } + + ProtocolException(const ProtocolException &) = delete; + ProtocolException &operator=(const ProtocolException &) = delete; + + const char *what() const throw() override + { + return mWhat.constData(); + } + +private: + QByteArray mWhat; +}; +} // namespace Akonadi + diff --git a/src/private/protocol_p.h b/src/private/protocol_p.h new file mode 100644 index 0000000..25bc0ef --- /dev/null +++ b/src/private/protocol_p.h @@ -0,0 +1,717 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2015 Daniel Vrátil + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" + +#include +#include +#include +#include +#include +#include + +#include "scope_p.h" +#include "tristate_p.h" + +// clazy:excludeall=function-args-by-value + +/** + @file protocol_p.h Shared constants used in the communication protocol between + the Akonadi server and its clients. +*/ + +namespace Akonadi +{ +namespace Protocol +{ +class Factory; +class DataStream; + +class Command; +class Response; +class ItemFetchScope; +class ScopeContext; +class ChangeNotification; + +using Attributes = QMap; + +} // namespace Protocol +} // namespace Akonadi + +namespace Akonadi +{ +namespace Protocol +{ +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::Command &cmd); +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::Command &cmd); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::Command &cmd); + +AKONADIPRIVATE_EXPORT void toJson(const Command *cmd, QJsonObject &json); + +using CommandPtr = QSharedPointer; + +class AKONADIPRIVATE_EXPORT Command +{ +public: + enum Type : quint8 { + Invalid = 0, + + // Session management + Hello = 1, + Login, + Logout, + + // Transactions + Transaction = 10, + + // Items + CreateItem = 20, + CopyItems, + DeleteItems, + FetchItems, + LinkItems, + ModifyItems, + MoveItems, + + // Collections + CreateCollection = 40, + CopyCollection, + DeleteCollection, + FetchCollections, + FetchCollectionStats, + ModifyCollection, + MoveCollection, + + // Search + Search = 60, + SearchResult, + StoreSearch, + + // Tag + CreateTag = 70, + DeleteTag, + FetchTags, + ModifyTag, + + // Relation + FetchRelations = 80, + ModifyRelation, + RemoveRelations, + + // Resources + SelectResource = 90, + + // Other + StreamPayload = 100, + + // Notifications + ItemChangeNotification = 110, + CollectionChangeNotification, + TagChangeNotification, + RelationChangeNotification, + SubscriptionChangeNotification, + DebugChangeNotification, + CreateSubscription, + ModifySubscription, + + // _MaxValue = 127 + _ResponseBit = 0x80U // reserved + }; + + explicit Command() = default; + explicit Command(const Command &) = default; + Command(Command &&) = default; + ~Command() = default; + + Command &operator=(const Command &) = default; + Command &operator=(Command &&) = default; + + bool operator==(const Command &other) const; + inline bool operator!=(const Command &other) const + { + return !operator==(other); + } + + inline Type type() const + { + return static_cast(mType & ~_ResponseBit); + } + inline bool isValid() const + { + return type() != Invalid; + } + inline bool isResponse() const + { + return mType & _ResponseBit; + } + + void toJson(QJsonObject &stream) const; + +protected: + explicit Command(quint8 type); + + quint8 mType = Invalid; + // unused 7 bytes + +private: + friend class Factory; + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::Command &cmd); + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::Command &cmd); + friend AKONADIPRIVATE_EXPORT QDebug operator<<(::QDebug dbg, const Akonadi::Protocol::Command &cmd); + friend AKONADIPRIVATE_EXPORT void toJson(const Akonadi::Protocol::Command *cmd, QJsonObject &json); +}; + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, Command::Type type); + +} // namespace Protocol +} // namespace Akonadi + +Q_DECLARE_METATYPE(Akonadi::Protocol::Command::Type) +Q_DECLARE_METATYPE(Akonadi::Protocol::CommandPtr) + +namespace Akonadi +{ +namespace Protocol +{ +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::Response &cmd); +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::Response &cmd); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::Response &response); + +using ResponsePtr = QSharedPointer; + +class AKONADIPRIVATE_EXPORT Response : public Command +{ +public: + explicit Response(); + explicit Response(const Response &) = default; + Response(Response &&) = default; + Response &operator=(const Response &) = default; + Response &operator=(Response &&) = default; + + inline void setError(int code, const QString &message) + { + mErrorCode = code; + mErrorMsg = message; + } + + bool operator==(const Response &other) const; + inline bool operator!=(const Response &other) const + { + return !operator==(other); + } + + inline bool isError() const + { + return mErrorCode > 0; + } + + inline int errorCode() const + { + return mErrorCode; + } + inline QString errorMessage() const + { + return mErrorMsg; + } + + void toJson(QJsonObject &json) const; + +protected: + explicit Response(Command::Type type); + + int mErrorCode; + QString mErrorMsg; + +private: + friend class Factory; + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::Response &cmd); + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::Response &cmd); + friend AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::Response &cmd); +}; + +} // namespace Protocol +} // namespace Akonadi + +namespace Akonadi +{ +namespace Protocol +{ +template inline const X &cmdCast(const QSharedPointer &p) +{ + return static_cast(*p); +} + +template inline X &cmdCast(QSharedPointer &p) +{ + return static_cast(*p); +} + +class AKONADIPRIVATE_EXPORT Factory +{ +public: + static CommandPtr command(Command::Type type); + static ResponsePtr response(Command::Type type); + +private: + template friend AKONADIPRIVATE_EXPORT CommandPtr deserialize(QIODevice *device); +}; + +AKONADIPRIVATE_EXPORT void serialize(DataStream &stream, const CommandPtr &command); +AKONADIPRIVATE_EXPORT CommandPtr deserialize(QIODevice *device); +AKONADIPRIVATE_EXPORT QString debugString(const Command &command); +AKONADIPRIVATE_EXPORT inline QString debugString(const CommandPtr &command) +{ + return debugString(*command); +} + +} // namespace Protocol +} // namespace Akonadi + +namespace Akonadi +{ +namespace Protocol +{ +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::ItemFetchScope &scope); +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::ItemFetchScope &scope); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::ItemFetchScope &scope); + +class AKONADIPRIVATE_EXPORT ItemFetchScope +{ +public: + enum FetchFlag : int { + None = 0, + CacheOnly = 1 << 0, + CheckCachedPayloadPartsOnly = 1 << 1, + FullPayload = 1 << 2, + AllAttributes = 1 << 3, + Size = 1 << 4, + MTime = 1 << 5, + RemoteRevision = 1 << 6, + IgnoreErrors = 1 << 7, + Flags = 1 << 8, + RemoteID = 1 << 9, + GID = 1 << 10, + Tags = 1 << 11, + Relations = 1 << 12, + VirtReferences = 1 << 13 + }; + Q_DECLARE_FLAGS(FetchFlags, FetchFlag) + + enum AncestorDepth : ushort { + NoAncestor, + ParentAncestor, + AllAncestors, + }; + + explicit ItemFetchScope() = default; + ItemFetchScope(const ItemFetchScope &) = default; + ItemFetchScope(ItemFetchScope &&other) = default; + ~ItemFetchScope() = default; + + ItemFetchScope &operator=(const ItemFetchScope &) = default; + ItemFetchScope &operator=(ItemFetchScope &&) = default; + + bool operator==(const ItemFetchScope &other) const; + inline bool operator!=(const ItemFetchScope &other) const + { + return !operator==(other); + } + + inline void setRequestedParts(const QVector &requestedParts) + { + mRequestedParts = requestedParts; + } + inline QVector requestedParts() const + { + return mRequestedParts; + } + QVector requestedPayloads() const; + + inline void setChangedSince(const QDateTime &changedSince) + { + mChangedSince = changedSince; + } + inline QDateTime changedSince() const + { + return mChangedSince; + } + + inline void setAncestorDepth(AncestorDepth depth) + { + mAncestorDepth = depth; + } + inline AncestorDepth ancestorDepth() const + { + return mAncestorDepth; + } + + inline bool cacheOnly() const + { + return mFlags & CacheOnly; + } + inline bool checkCachedPayloadPartsOnly() const + { + return mFlags & CheckCachedPayloadPartsOnly; + } + inline bool fullPayload() const + { + return mFlags & FullPayload; + } + inline bool allAttributes() const + { + return mFlags & AllAttributes; + } + inline bool fetchSize() const + { + return mFlags & Size; + } + inline bool fetchMTime() const + { + return mFlags & MTime; + } + inline bool fetchRemoteRevision() const + { + return mFlags & RemoteRevision; + } + inline bool ignoreErrors() const + { + return mFlags & IgnoreErrors; + } + inline bool fetchFlags() const + { + return mFlags & Flags; + } + inline bool fetchRemoteId() const + { + return mFlags & RemoteID; + } + inline bool fetchGID() const + { + return mFlags & GID; + } + inline bool fetchTags() const + { + return mFlags & Tags; + } + inline bool fetchRelations() const + { + return mFlags & Relations; + } + inline bool fetchVirtualReferences() const + { + return mFlags & VirtReferences; + } + + void setFetch(FetchFlags attributes, bool fetch = true); + bool fetch(FetchFlags flags) const; + + void toJson(QJsonObject &json) const; + +private: + AncestorDepth mAncestorDepth = NoAncestor; + // 2 bytes free + FetchFlags mFlags = None; + QVector mRequestedParts; + QDateTime mChangedSince; + + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, + const Akonadi::Protocol::ItemFetchScope &scope); + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::ItemFetchScope &scope); + friend AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::ItemFetchScope &scope); +}; + +} // namespace Protocol +} // namespace Akonadi + +Q_DECLARE_OPERATORS_FOR_FLAGS(Akonadi::Protocol::ItemFetchScope::FetchFlags) + +namespace Akonadi +{ +namespace Protocol +{ +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::ScopeContext &ctx); +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::ScopeContext &ctx); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::ScopeContext &ctx); + +class AKONADIPRIVATE_EXPORT ScopeContext +{ +public: + enum Type : uchar { + Any = 0, + Collection, + Tag, + }; + + explicit ScopeContext() = default; + ScopeContext(Type type, qint64 id); + ScopeContext(Type type, const QString &id); + ScopeContext(const ScopeContext &) = default; + ScopeContext(ScopeContext &&) = default; + ~ScopeContext() = default; + + ScopeContext &operator=(const ScopeContext &) = default; + ScopeContext &operator=(ScopeContext &&) = default; + + bool operator==(const ScopeContext &other) const; + inline bool operator!=(const ScopeContext &other) const + { + return !operator==(other); + } + + inline bool isEmpty() const + { + return mColCtx.isNull() && mTagCtx.isNull(); + } + + inline void setContext(Type type, qint64 id) + { + setCtx(type, id); + } + inline void setContext(Type type, const QString &id) + { + setCtx(type, id); + } + inline void clearContext(Type type) + { + setCtx(type, QVariant()); + } + + inline bool hasContextId(Type type) const + { + return ctx(type).type() == QVariant::LongLong; + } + inline qint64 contextId(Type type) const + { + return hasContextId(type) ? ctx(type).toLongLong() : 0; + } + + inline bool hasContextRID(Type type) const + { + return ctx(type).type() == QVariant::String; + } + inline QString contextRID(Type type) const + { + return hasContextRID(type) ? ctx(type).toString() : QString(); + } + + void toJson(QJsonObject &json) const; + +private: + QVariant mColCtx; + QVariant mTagCtx; + + inline QVariant ctx(Type type) const + { + return type == Collection ? mColCtx : type == Tag ? mTagCtx : QVariant(); + } + + inline void setCtx(Type type, const QVariant &v) + { + if (type == Collection) { + mColCtx = v; + } else if (type == Tag) { + mTagCtx = v; + } + } + + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, + const Akonadi::Protocol::ScopeContext &context); + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::ScopeContext &context); + friend AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::ScopeContext &ctx); +}; + +} // namespace Protocol +} // namespace akonadi + +namespace Akonadi +{ +namespace Protocol +{ +class FetchItemsResponse; +using FetchItemsResponsePtr = QSharedPointer; + +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::ChangeNotification &ntf); +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::ChangeNotification &ntf); +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::ChangeNotification &ntf); + +using ChangeNotificationPtr = QSharedPointer; +using ChangeNotificationList = QVector; + +class AKONADIPRIVATE_EXPORT ChangeNotification : public Command +{ +public: + static QList itemsToUids(const QVector &items); + + class Relation + { + public: + Relation() = default; + Relation(const Relation &) = default; + Relation(Relation &&) = default; + inline Relation(qint64 leftId, qint64 rightId, const QString &type) + : leftId(leftId) + , rightId(rightId) + , type(type) + { + } + + Relation &operator=(const Relation &) = default; + Relation &operator=(Relation &&) = default; + + inline bool operator==(const Relation &other) const + { + return leftId == other.leftId && rightId == other.rightId && type == other.type; + } + + void toJson(QJsonObject &json) const + { + json[QStringLiteral("leftId")] = leftId; + json[QStringLiteral("rightId")] = rightId; + json[QStringLiteral("type")] = type; + } + + qint64 leftId = -1; + qint64 rightId = -1; + QString type; + }; + + ChangeNotification &operator=(const ChangeNotification &) = default; + ChangeNotification &operator=(ChangeNotification &&) = default; + + bool operator==(const ChangeNotification &other) const; + inline bool operator!=(const ChangeNotification &other) const + { + return !operator==(other); + } + + bool isRemove() const; + bool isMove() const; + + inline QByteArray sessionId() const + { + return mSessionId; + } + inline void setSessionId(const QByteArray &sessionId) + { + mSessionId = sessionId; + } + + inline void addMetadata(const QByteArray &metadata) + { + mMetaData << metadata; + } + inline void removeMetadata(const QByteArray &metadata) + { + mMetaData.removeAll(metadata); + } + QVector metadata() const + { + return mMetaData; + } + + static bool appendAndCompress(ChangeNotificationList &list, const ChangeNotificationPtr &msg); + + void toJson(QJsonObject &json) const; + +protected: + explicit ChangeNotification() = default; + explicit ChangeNotification(Command::Type type); + ChangeNotification(const ChangeNotification &) = default; + ChangeNotification(ChangeNotification &&) = default; + + QByteArray mSessionId; + + // For internal use only: Akonadi server can add some additional information + // that might be useful when evaluating the notification for example, but + // it is never transferred to clients + QVector mMetaData; + + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, + const Akonadi::Protocol::ChangeNotification &ntf); + friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::ChangeNotification &ntf); + friend AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::ChangeNotification &ntf); +}; + +inline uint qHash(const ChangeNotification::Relation &rel) +{ + return ::qHash(rel.leftId + rel.rightId); +} + +// TODO: Internalize? +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, + const Akonadi::Protocol::ChangeNotification::Relation &relation); +AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, + Akonadi::Protocol::ChangeNotification::Relation &relation); + +} // namespace Protocol +} // namespace Akonadi + +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotificationPtr) +Q_DECLARE_METATYPE(Akonadi::Protocol::ChangeNotificationList) + +/******************************************************************************/ + +// Here comes the actual generated Protocol. See protocol.xml for definitions, +// and genprotocol folder for the generator. +#include "protocol_gen.h" + +/******************************************************************************/ + +// Command parameters +#define AKONADI_PARAM_ATR "ATR:" +#define AKONADI_PARAM_CACHEPOLICY "CACHEPOLICY" +#define AKONADI_PARAM_DISPLAY "DISPLAY" +#define AKONADI_PARAM_ENABLED "ENABLED" +#define AKONADI_PARAM_FLAGS "FLAGS" +#define AKONADI_PARAM_TAGS "TAGS" +#define AKONADI_PARAM_GID "GID" +#define AKONADI_PARAM_INDEX "INDEX" +#define AKONADI_PARAM_MIMETYPE "MIMETYPE" +#define AKONADI_PARAM_NAME "NAME" +#define AKONADI_PARAM_PARENT "PARENT" +#define AKONADI_PARAM_PERSISTENTSEARCH "PERSISTENTSEARCH" +#define AKONADI_PARAM_PLD "PLD:" +#define AKONADI_PARAM_PLD_RFC822 "PLD:RFC822" +#define AKONADI_PARAM_RECURSIVE "RECURSIVE" +#define AKONADI_PARAM_REMOTE "REMOTE" +#define AKONADI_PARAM_REMOTEID "REMOTEID" +#define AKONADI_PARAM_REMOTEREVISION "REMOTEREVISION" +#define AKONADI_PARAM_REVISION "REV" +#define AKONADI_PARAM_SIZE "SIZE" +#define AKONADI_PARAM_SYNC "SYNC" +#define AKONADI_PARAM_TAG "TAG" +#define AKONADI_PARAM_TYPE "TYPE" +#define AKONADI_PARAM_VIRTUAL "VIRTUAL" + +// Flags +#define AKONADI_FLAG_GID "\\Gid" +#define AKONADI_FLAG_IGNORED "$IGNORED" +#define AKONADI_FLAG_MIMETYPE "\\MimeType" +#define AKONADI_FLAG_REMOTEID "\\RemoteId" +#define AKONADI_FLAG_REMOTEREVISION "\\RemoteRevision" +#define AKONADI_FLAG_TAG "\\Tag" +#define AKONADI_FLAG_RTAG "\\RTag" +#define AKONADI_FLAG_SEEN "\\SEEN" + +// Attributes +#define AKONADI_ATTRIBUTE_HIDDEN "ATR:HIDDEN" +#define AKONADI_ATTRIBUTE_MESSAGES "MESSAGES" +#define AKONADI_ATTRIBUTE_UNSEEN "UNSEEN" + +// special resource names +#define AKONADI_SEARCH_RESOURCE "akonadi_search_resource" + +namespace Akonadi +{ +static const QString CollectionMimeType = QStringLiteral("inode/directory"); +static const QString VirtualCollectionMimeType = QStringLiteral("application/x-vnd.akonadi.collection.virtual"); + +} + diff --git a/src/private/protocolgen/CMakeLists.txt b/src/private/protocolgen/CMakeLists.txt new file mode 100644 index 0000000..93273a1 --- /dev/null +++ b/src/private/protocolgen/CMakeLists.txt @@ -0,0 +1,16 @@ +project(protocolgen) + +add_executable(protocolgen) +target_sources(protocolgen PRIVATE + main.cpp + cppgenerator.cpp + cpphelper.cpp + nodetree.cpp + typehelper.cpp + xmlparser.cpp +) + +set_target_properties(protocolgen PROPERTIES MACOSX_BUNDLE FALSE) +target_link_libraries(protocolgen + Qt::Core +) diff --git a/src/private/protocolgen/cppgenerator.cpp b/src/private/protocolgen/cppgenerator.cpp new file mode 100644 index 0000000..3c0af25 --- /dev/null +++ b/src/private/protocolgen/cppgenerator.cpp @@ -0,0 +1,836 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "cppgenerator.h" +#include "cpphelper.h" +#include "nodetree.h" +#include "typehelper.h" + +#include + +#include + +CppGenerator::CppGenerator() +{ +} + +CppGenerator::~CppGenerator() +{ +} + +bool CppGenerator::generate(Node const *node) +{ + Q_ASSERT(node->type() == Node::Document); + + mHeaderFile.setFileName(QStringLiteral("protocol_gen.h")); + if (!mHeaderFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + std::cerr << qPrintable(mHeaderFile.errorString()) << std::endl; + return false; + } + mHeader.setDevice(&mHeaderFile); + + mImplFile.setFileName(QStringLiteral("protocol_gen.cpp")); + if (!mImplFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + std::cerr << qPrintable(mImplFile.errorString()) << std::endl; + return false; + } + mImpl.setDevice(&mImplFile); + + return generateDocument(static_cast(node)); +} + +void CppGenerator::writeHeaderHeader(DocumentNode const *node) +{ + mHeader << "// This is an auto-generated file.\n" + "// Any changes to this file will be overwritten\n" + "\n" + "// clazy:excludeall=function-args-by-value\n" + "\n" + "namespace Akonadi {\n" + "namespace Protocol {\n" + "\n" + "AKONADIPRIVATE_EXPORT int version();\n" + "\n"; + + // Forward declarations + for (const auto *child : std::as_const(node->children())) { + if (child->type() == Node::Class) { + mHeader << "class " << static_cast(child)->className() << ";\n"; + } + } + + mHeader << "\n" + "} // namespace Protocol\n" + "} // namespace Akonadi\n\n"; +} + +void CppGenerator::writeHeaderFooter(DocumentNode const * /*node*/) +{ + // Nothing to do +} + +void CppGenerator::writeImplHeader(DocumentNode const *node) +{ + mImpl << "// This is an auto-generated file.\n" + "// Any changes to this file will be overwritten\n" + "\n" + "// clazy:excludeall=function-args-by-value\n" + "\n" + "namespace Akonadi {\n" + "namespace Protocol {\n" + "\n" + "int version()\n" + "{\n" + " return " + << node->version() + << ";\n" + "}\n" + "\n"; +} + +void CppGenerator::writeImplFooter(DocumentNode const * /*unused*/) +{ + mImpl << "} // namespace Protocol\n" + "} // namespace Akonadi\n"; +} + +bool CppGenerator::generateDocument(DocumentNode const *node) +{ + writeHeaderHeader(node); + writeImplHeader(node); + + writeImplSerializer(node); + + for (const auto *classNode : node->children()) { + if (!generateClass(static_cast(classNode))) { + return false; + } + } + + writeHeaderFooter(node); + writeImplFooter(node); + + return true; +} + +void CppGenerator::writeImplSerializer(DocumentNode const *node) +{ + mImpl << "void serialize(DataStream &stream, const CommandPtr &cmd)\n" + "{\n" + " switch (static_cast(cmd->type() | (cmd->isResponse() ? Command::_ResponseBit : 0))) {\n" + " case Command::Invalid:\n" + " stream << cmdCast(cmd);\n" + " break;\n" + " case Command::Invalid | Command::_ResponseBit:\n" + " stream << cmdCast(cmd);\n" + " break;\n"; + for (const auto *child : std::as_const(node->children())) { + const auto *classNode = static_cast(child); + if (classNode->classType() == ClassNode::Response) { + mImpl << " case Command::" << classNode->name() + << " | Command::_ResponseBit:\n" + " stream << cmdCast<" + << classNode->className() + << ">(cmd);\n" + " break;\n"; + } else if (classNode->classType() == ClassNode::Command) { + mImpl << " case Command::" << classNode->name() + << ":\n" + " stream << cmdCast<" + << classNode->className() + << ">(cmd);\n" + " break;\n"; + } else if (classNode->classType() == ClassNode::Notification) { + mImpl << " case Command::" << classNode->name() + << "Notification:\n" + " stream << cmdCast<" + << classNode->className() + << ">(cmd);\n" + " break;\n"; + } + } + mImpl << " }\n" + "}\n\n"; + + mImpl << "CommandPtr deserialize(QIODevice *device)\n" + "{\n" + " DataStream stream(device);\n" + " stream.waitForData(sizeof(Command::Type));\n" + " Command::Type cmdType;\n" + " if (Q_UNLIKELY(device->peek((char *) &cmdType, sizeof(Command::Type)) != sizeof(Command::Type))) {\n" + " throw ProtocolException(\"Failed to peek command type\");\n" + " }\n" + " CommandPtr cmd;\n" + " if (cmdType & Command::_ResponseBit) {\n" + " cmd = Factory::response(Command::Type(cmdType & ~Command::_ResponseBit));\n" + " } else {\n" + " cmd = Factory::command(cmdType);\n" + " }\n\n" + " switch (static_cast(cmdType)) {\n" + " case Command::Invalid:\n" + " stream >> cmdCast(cmd);\n" + " return cmd;\n" + " case Command::Invalid | Command::_ResponseBit:\n" + " stream >> cmdCast(cmd);\n" + " return cmd;\n"; + for (const auto *child : std::as_const(node->children())) { + const auto *classNode = static_cast(child); + if (classNode->classType() == ClassNode::Response) { + mImpl << " case Command::" << classNode->name() + << " | Command::_ResponseBit:\n" + " stream >> cmdCast<" + << classNode->className() + << ">(cmd);\n" + " return cmd;\n"; + } else if (classNode->classType() == ClassNode::Command) { + mImpl << " case Command::" << classNode->name() + << ":\n" + " stream >> cmdCast<" + << classNode->className() + << ">(cmd);\n" + " return cmd;\n"; + } else if (classNode->classType() == ClassNode::Notification) { + mImpl << " case Command::" << classNode->name() + << "Notification:\n" + " stream >> cmdCast<" + << classNode->className() + << ">(cmd);\n" + " return cmd;\n"; + } + } + mImpl << " }\n" + " return CommandPtr::create();\n" + "}\n" + "\n"; + + mImpl << "QString debugString(const Command &cmd)\n" + "{\n" + " QString out;\n" + " switch (static_cast(cmd.type() | (cmd.isResponse() ? Command::_ResponseBit : 0))) {\n" + " case Command::Invalid:\n" + " QDebug(&out).noquote() << static_cast(cmd);\n" + " return out;\n" + " case Command::Invalid | Command::_ResponseBit:\n" + " QDebug(&out).noquote() << static_cast(cmd);\n" + " return out;\n"; + for (const auto *child : std::as_const(node->children())) { + const auto *classNode = static_cast(child); + if (classNode->classType() == ClassNode::Response) { + mImpl << " case Command::" << classNode->name() + << " | Command::_ResponseBit:\n" + " QDebug(&out).noquote() << static_castclassName() + << " &>(cmd);\n" + " return out;\n"; + } else if (classNode->classType() == ClassNode::Command) { + mImpl << " case Command::" << classNode->name() + << ":\n" + " QDebug(&out).noquote() << static_castclassName() + << " &>(cmd);\n" + " return out;\n"; + } else if (classNode->classType() == ClassNode::Notification) { + mImpl << " case Command::" << classNode->name() + << "Notification:\n" + " QDebug(&out).noquote() << static_castclassName() + << " &>(cmd);\n" + " return out;\n"; + } + } + mImpl << " }\n" + " return QString();\n" + "}\n" + "\n"; +} + +void CppGenerator::writeHeaderEnum(EnumNode const *node) +{ + mHeader << " enum " << node->name() << " {\n"; + for (const auto *enumChild : node->children()) { + Q_ASSERT(enumChild->type() == Node::EnumValue); + const auto *const valueNode = static_cast(enumChild); + mHeader << " " << valueNode->name(); + if (!valueNode->value().isEmpty()) { + mHeader << " = " << valueNode->value(); + } + mHeader << ",\n"; + } + mHeader << " };\n"; + if (node->enumType() == EnumNode::TypeFlag) { + mHeader << " Q_DECLARE_FLAGS(" << node->name() << "s, " << node->name() << ")\n\n"; + } +} + +void CppGenerator::writeHeaderClass(ClassNode const *node) +{ + // Begin class + const QString parentClass = node->parentClassName(); + const bool isTypeClass = node->classType() == ClassNode::Class; + + mHeader << "namespace Akonadi {\n" + "namespace Protocol {\n\n" + "AKONADIPRIVATE_EXPORT DataStream &operator<<(DataStream &stream, const " + << node->className() + << " &obj);\n" + "AKONADIPRIVATE_EXPORT DataStream &operator>>(DataStream &stream, " + << node->className() + << " &obj);\n" + "AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const " + << node->className() + << " &obj);\n" + "\n" + "using " + << node->className() << "Ptr = QSharedPointer<" << node->className() + << ">;\n" + "\n"; + if (isTypeClass) { + mHeader << "class AKONADIPRIVATE_EXPORT " << node->className() << "\n"; + } else { + mHeader << "class AKONADIPRIVATE_EXPORT " << node->className() << " : public " << parentClass << "\n"; + } + mHeader << "{\n\n" + "public:\n"; + + // Enums + for (const auto *child : node->children()) { + if (child->type() == Node::Enum) { + const auto *const enumNode = static_cast(child); + writeHeaderEnum(enumNode); + } + } + + // Ctors, dtor + for (const auto *child : std::as_const(node->children())) { + if (child->type() == Node::Ctor) { + const auto *const ctor = static_cast(child); + const auto args = ctor->arguments(); + mHeader << " explicit " << node->className() << "("; + for (int i = 0; i < args.count(); ++i) { + const auto &arg = args[i]; + if (TypeHelper::isNumericType(arg.type) || TypeHelper::isBoolType(arg.type)) { + mHeader << arg.type << " " << arg.name; + } else { + mHeader << "const " << arg.type << " &" << arg.name; + } + if (!arg.defaultValue.isEmpty()) { + mHeader << " = " << arg.defaultValue; + } + if (i < args.count() - 1) { + mHeader << ", "; + } + } + mHeader << ");\n"; + } + } + + mHeader << " " << node->className() << "(const " << node->className() + << " &) = default;\n" + " " + << node->className() << "(" << node->className() + << " &&) = default;\n" + " ~" + << node->className() + << "() = default;\n" + "\n" + " " + << node->className() << " &operator=(const " << node->className() + << " &) = default;\n" + " " + << node->className() << " &operator=(" << node->className() + << " &&) = default;\n" + " bool operator==(const " + << node->className() + << " &other) const;\n" + " inline bool operator!=(const " + << node->className() << " &other) const { return !operator==(other); }\n"; + + // Properties + for (const auto *child : node->children()) { + if (child->type() == Node::Property) { + const auto *const prop = static_cast(child); + if (prop->asReference()) { + mHeader << " inline const " << prop->type() << " &" << prop->name() << "() const { return " << prop->mVariableName() + << "; }\n" + " inline " + << prop->type() << " &" << prop->name() << "() { return " << prop->mVariableName() << "; }\n"; + } else { + mHeader << " inline " << prop->type() << " " << prop->name() << "() const { return " << prop->mVariableName() << "; }\n"; + } + if (!prop->readOnly()) { + if (auto *setter = prop->setter()) { + mHeader << " void " << setter->name << "(const " << setter->type << " &" << prop->name() << ");\n"; + } else if (!prop->dependencies().isEmpty()) { + QString varType; + if (TypeHelper::isNumericType(prop->type()) || TypeHelper::isBoolType(prop->type())) { + varType = QLatin1Char('(') + prop->type() + QLatin1Char(' '); + } else { + varType = QLatin1String("(const ") + prop->type() + QLatin1String(" &"); + } + mHeader << " void " << prop->setterName() << varType << prop->name() << ");\n"; + } else { + QString varType; + if (TypeHelper::isNumericType(prop->type()) || TypeHelper::isBoolType(prop->type())) { + varType = QLatin1Char('(') + prop->type() + QLatin1Char(' '); + } else { + varType = QLatin1String("(const ") + prop->type() + QLatin1String(" &"); + } + mHeader << " inline void " << prop->setterName() << varType << prop->name() << ") { " << prop->mVariableName() << " = " << prop->name() + << "; }\n"; + if (!TypeHelper::isNumericType(prop->type()) && !TypeHelper::isBoolType(prop->type())) { + mHeader << " inline void " << prop->setterName() << "(" << prop->type() << " &&" << prop->name() << ") { " << prop->mVariableName() + << " = std::move(" << prop->name() << "); }\n"; + } + } + } + mHeader << "\n"; + } + } + mHeader << " void toJson(QJsonObject &stream) const;\n"; + + // End of class + mHeader << "protected:\n"; + const auto properties = node->properties(); + for (const auto *prop : properties) { + mHeader << " " << prop->type() << " " << prop->mVariableName(); + const auto defaultValue = prop->defaultValue(); + const bool isDefaultValue = !defaultValue.isEmpty(); + const bool isNumeric = TypeHelper::isNumericType(prop->type()); + const bool isBool = TypeHelper::isBoolType(prop->type()); + if (isDefaultValue) { + mHeader << " = " << defaultValue; + } else if (isNumeric) { + mHeader << " = 0"; + } else if (isBool) { + mHeader << " = false"; + } + mHeader << ";\n"; + } + + mHeader << "\n" + "private:\n" + " friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator<<(Akonadi::Protocol::DataStream &stream, const Akonadi::Protocol::" + << node->className() + << " &obj);\n" + " friend AKONADIPRIVATE_EXPORT Akonadi::Protocol::DataStream &operator>>(Akonadi::Protocol::DataStream &stream, Akonadi::Protocol::" + << node->className() + << " &obj);\n" + " friend AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, const Akonadi::Protocol::" + << node->className() + << " &obj);\n" + "};\n\n" + "} // namespace Protocol\n" + "} // namespace Akonadi\n" + "\n"; + mHeader << "Q_DECLARE_METATYPE(Akonadi::Protocol::" << node->className() << ")\n\n"; + if (node->classType() != ClassNode::Class) { + mHeader << "Q_DECLARE_METATYPE(Akonadi::Protocol::" << node->className() << "Ptr)\n\n"; + } +} + +void CppGenerator::writeImplSerializer(PropertyNode const *node, const char *streamingOperator) +{ + const auto deps = node->dependencies(); + if (deps.isEmpty()) { + mImpl << " stream " << streamingOperator << " obj." << node->mVariableName() << ";\n"; + } else { + mImpl << " if ("; + auto it = deps.cend(); + while (1 + 1 == 2) { + --it; + const QString mVar = it.key(); + mImpl << "(obj." + << "m" << mVar[0].toUpper() << mVar.midRef(1) << " & " << it.value() << ")"; + if (it == deps.cbegin()) { + break; + } else { + mImpl << " && "; + } + } + mImpl << ") {\n" + " stream " + << streamingOperator << " obj." << node->mVariableName() + << ";\n" + " }\n"; + } +} + +void CppGenerator::writeImplClass(ClassNode const *node) +{ + const QString parentClass = node->parentClassName(); + const auto &children = node->children(); + const auto properties = node->properties(); + + // Ctors + for (const auto *child : children) { + if (child->type() == Node::Ctor) { + const auto *const ctor = static_cast(child); + const auto args = ctor->arguments(); + mImpl << node->className() << "::" << node->className() << "("; + for (int i = 0; i < args.count(); ++i) { + const auto &arg = args[i]; + if (TypeHelper::isNumericType(arg.type) || TypeHelper::isBoolType(arg.type)) { + mImpl << arg.type << " " << arg.name; + } else { + mImpl << "const " << arg.type << " &" << arg.name; + } + if (i < args.count() - 1) { + mImpl << ", "; + } + } + mImpl << ")\n"; + char startChar = ','; + if (!parentClass.isEmpty()) { + const QString type = node->name() + ((node->classType() == ClassNode::Notification) ? QStringLiteral("Notification") : QString()); + mImpl << " : " << parentClass << "(Command::" << type << ")\n"; + } else { + startChar = ':'; + } + for (const auto *prop : properties) { + auto arg = std::find_if(args.cbegin(), args.cend(), [prop](const CtorNode::Argument &arg) { + return arg.name == prop->name(); + }); + if (arg != args.cend()) { + mImpl << " " << startChar << " " << prop->mVariableName() << "(" << arg->name << ")\n"; + startChar = ','; + } + } + mImpl << "{\n" + "}\n" + "\n"; + } + } + + // Comparison operator + mImpl << "bool " << node->className() << "::operator==(const " << node->className() + << " &other) const\n" + "{\n"; + mImpl << " return true // simplifies generation\n"; + if (!parentClass.isEmpty()) { + mImpl << " && " << parentClass << "::operator==(other)\n"; + } + for (const auto *prop : properties) { + if (prop->isPointer()) { + mImpl << " && *" << prop->mVariableName() << " == *other." << prop->mVariableName() << "\n"; + } else if (TypeHelper::isContainer(prop->type())) { + mImpl << " && containerComparator(" << prop->mVariableName() << ", other." << prop->mVariableName() << ")\n"; + } else { + mImpl << " && " << prop->mVariableName() << " == other." << prop->mVariableName() << "\n"; + } + } + mImpl << " ;\n" + "}\n" + "\n"; + + // non-trivial setters + for (const auto *prop : properties) { + if (prop->readOnly()) { + continue; + } + + if (auto *const setter = prop->setter()) { + mImpl << "void " << node->className() << "::" << setter->name << "(const " << setter->type + << " &val)\n" + "{\n"; + if (!setter->append.isEmpty()) { + mImpl << " m" << setter->append[0].toUpper() << setter->append.midRef(1) << " << val;\n"; + } + if (!setter->remove.isEmpty()) { + const QString mVar = QLatin1String("m") + setter->remove[0].toUpper() + setter->remove.midRef(1); + mImpl << " auto it = std::find(" << mVar << ".begin(), " << mVar + << ".end(), val);\n" + " if (it != " + << mVar + << ".end()) {\n" + " " + << mVar + << ".erase(it);\n" + " }\n"; + } + writeImplPropertyDependencies(prop); + mImpl << "}\n\n"; + } else if (!prop->dependencies().isEmpty()) { + if (TypeHelper::isNumericType(prop->type()) || TypeHelper::isBoolType(prop->type())) { + mImpl << "void " << node->className() << "::" << prop->setterName() << "(" << prop->type() + << " val)\n" + "{\n" + " " + << prop->mVariableName() << " = val;\n"; + + } else { + mImpl << "void " << node->className() << "::" << prop->setterName() << "(const " << prop->type() + << " &val)\n" + "{\n" + " " + << prop->mVariableName() << " = val;\n"; + } + writeImplPropertyDependencies(prop); + mImpl << "}\n\n"; + } + } + + // serialize + auto serializeProperties = properties; + CppHelper::sortMembersForSerialization(serializeProperties); + + mImpl << "DataStream &operator<<(DataStream &stream, const " << node->className() + << " &obj)\n" + "{\n"; + if (!parentClass.isEmpty()) { + mImpl << " stream << static_cast(obj);\n"; + } + for (const auto *prop : std::as_const(serializeProperties)) { + writeImplSerializer(prop, "<<"); + } + mImpl << " return stream;\n" + "}\n" + "\n"; + + // deserialize + mImpl << "DataStream &operator>>(DataStream &stream, " << node->className() + << " &obj)\n" + "{\n"; + if (!parentClass.isEmpty()) { + mImpl << " stream >> static_cast<" << parentClass << " &>(obj);\n"; + } + for (const auto *prop : std::as_const(serializeProperties)) { + writeImplSerializer(prop, ">>"); + } + mImpl << " return stream;\n" + "}\n" + "\n"; + + // debug + mImpl << "QDebug operator<<(QDebug dbg, const " << node->className() + << " &obj)\n" + "{\n"; + if (!parentClass.isEmpty()) { + mImpl << " dbg.noquote() << static_cast(obj)\n"; + } else { + mImpl << " dbg.noquote()\n"; + } + + for (const auto *prop : std::as_const(serializeProperties)) { + if (prop->isPointer()) { + mImpl << " << \"" << prop->name() << ":\" << *obj." << prop->mVariableName() << " << \"\\n\"\n"; + } else if (TypeHelper::isContainer(prop->type())) { + mImpl << " << \"" << prop->name() + << ": [\\n\";\n" + " for (const auto &type : std::as_const(obj." + << prop->mVariableName() + << ")) {\n" + " dbg.noquote() << \" \" << "; + if (TypeHelper::isPointerType(TypeHelper::containerType(prop->type()))) { + mImpl << "*type"; + } else { + mImpl << "type"; + } + mImpl << " << \"\\n\";\n" + " }\n" + " dbg.noquote() << \"]\\n\"\n"; + } else { + mImpl << " << \"" << prop->name() << ":\" << obj." << prop->mVariableName() << " << \"\\n\"\n"; + } + } + mImpl << " ;\n" + " return dbg;\n" + "}\n" + "\n"; + + // toJson + mImpl << "void " << node->className() + << "::toJson(QJsonObject &json) const\n" + "{\n"; + if (!parentClass.isEmpty()) { + mImpl << " static_cast(this)->toJson(json);\n"; + } else if (serializeProperties.isEmpty()) { + mImpl << " Q_UNUSED(json)\n"; + } + for (const auto *prop : std::as_const(serializeProperties)) { + if (prop->isPointer()) { + mImpl << " {\n" + " QJsonObject jsonObject;\n" + " " + << prop->mVariableName() + << "->toJson(jsonObject);\n" + " json[QStringLiteral(\"" + << prop->name() + << "\")] = jsonObject;\n" + " }\n"; + } else if (TypeHelper::isContainer(prop->type())) { + const auto &containerType = TypeHelper::containerType(prop->type()); + mImpl << " {\n" + " QJsonArray jsonArray;\n" + " for (const auto &type : std::as_const(" + << prop->mVariableName() << ")) {\n"; + if (TypeHelper::isPointerType(containerType)) { + mImpl << " QJsonObject jsonObject;\n" + " type->toJson(jsonObject); /* " + << containerType + << " */\n" + " jsonArray.append(jsonObject);\n"; + } else if (TypeHelper::isNumericType(containerType) || TypeHelper::isBoolType(containerType)) { + mImpl << " jsonArray.append(type); /* " << containerType << " */\n"; + } else if (containerType == QLatin1String("QByteArray")) { + mImpl << " jsonArray.append(QString::fromUtf8(type)); /* " << containerType << "*/\n"; + } else if (TypeHelper::isBuiltInType(containerType)) { + if (TypeHelper::containerType(prop->type()) == QLatin1String("Akonadi::Protocol::ChangeNotification::Relation")) { + mImpl << " QJsonObject jsonObject;\n" + " type.toJson(jsonObject); /* " + << containerType + << " */\n" + " jsonArray.append(jsonObject);\n"; + } else { + mImpl << " jsonArray.append(type); /* " << containerType << " */\n"; + } + } else { + mImpl << " QJsonObject jsonObject;\n" + " type.toJson(jsonObject); /* " + << containerType + << " */\n" + " jsonArray.append(jsonObject);\n"; + } + mImpl << " }\n" + << " json[QStringLiteral(\"" << prop->name() << "\")] = jsonArray;\n" + << " }\n"; + } else if (prop->type() == QLatin1String("uint")) { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = static_cast(" << prop->mVariableName() << ");/* " << prop->type() << " */\n"; + } else if (TypeHelper::isNumericType(prop->type()) || TypeHelper::isBoolType(prop->type())) { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = " << prop->mVariableName() << ";/* " << prop->type() << " */\n"; + } else if (TypeHelper::isBuiltInType(prop->type())) { + if (prop->type() == QLatin1String("QStringList")) { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = QJsonArray::fromStringList(" << prop->mVariableName() << ");/* " + << prop->type() << " */\n"; + } else if (prop->type() == QLatin1String("QDateTime")) { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = " << prop->mVariableName() << ".toString()/* " << prop->type() << " */;\n"; + } else if (prop->type() == QLatin1String("QByteArray")) { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = QString::fromUtf8(" << prop->mVariableName() << ")/* " << prop->type() + << " */;\n"; + } else if (prop->type() == QLatin1String("Scope")) { + mImpl << " {\n" + " QJsonObject jsonObject;\n" + " " + << prop->mVariableName() << ".toJson(jsonObject); /* " << prop->type() + << " */\n" + " json[QStringLiteral(\"" + << prop->name() << "\")] = " + << "jsonObject;\n" + " }\n"; + } else if (prop->type() == QLatin1String("Tristate")) { + mImpl << " switch (" << prop->mVariableName() + << ") {\n;" + " case Tristate::True:\n" + " json[QStringLiteral(\"" + << prop->name() + << "\")] = QStringLiteral(\"True\");\n" + " break;\n" + " case Tristate::False:\n" + " json[QStringLiteral(\"" + << prop->name() + << "\")] = QStringLiteral(\"False\");\n" + " break;\n" + " case Tristate::Undefined:\n" + " json[QStringLiteral(\"" + << prop->name() + << "\")] = QStringLiteral(\"Undefined\");\n" + " break;\n" + " }\n"; + } else if (prop->type() == QLatin1String("Akonadi::Protocol::Attributes")) { + mImpl << " {\n" + " QJsonObject jsonObject;\n" + " auto i = " + << prop->mVariableName() + << ".constBegin();\n" + " const auto &end = " + << prop->mVariableName() + << ".constEnd();\n" + " while (i != end) {\n" + " jsonObject[QString::fromUtf8(i.key())] = QString::fromUtf8(i.value());\n" + " ++i;\n" + " }\n" + " json[QStringLiteral(\"" + << prop->name() + << "\")] = jsonObject;\n" + " }\n"; + } else if (prop->type() == QLatin1String("ModifySubscriptionCommand::ModifiedParts") + || prop->type() == QLatin1String("ModifyTagCommand::ModifiedParts") + || prop->type() == QLatin1String("ModifyCollectionCommand::ModifiedParts") + || prop->type() == QLatin1String("ModifyItemsCommand::ModifiedParts") + || prop->type() == QLatin1String("CreateItemCommand::MergeModes")) { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = static_cast(" << prop->mVariableName() << ");/* " << prop->type() + << "*/\n"; + } else { + mImpl << " json[QStringLiteral(\"" << prop->name() << "\")] = " << prop->mVariableName() << ";/* " << prop->type() << "*/\n"; + } + } else { + mImpl << " {\n" + " QJsonObject jsonObject;\n" + " " + << prop->mVariableName() << ".toJson(jsonObject); /* " << prop->type() + << " */\n" + " json[QStringLiteral(\"" + << prop->name() + << "\")] = jsonObject;\n" + " }\n"; + } + } + mImpl << "}\n" + "\n"; +} + +void CppGenerator::writeImplPropertyDependencies(const PropertyNode *node) +{ + const auto deps = node->dependencies(); + QString key; + QStringList values; + QString enumType; + for (auto it = deps.cbegin(), end = deps.cend(); it != end; ++it) { + if (key != it.key()) { + key = it.key(); + const auto children = node->parent()->children(); + for (const auto *child : children) { + if (child->type() == Node::Property && child != node) { + const auto *prop = static_cast(child); + if (prop->name() == key) { + enumType = prop->type(); + break; + } + } + } + if (!values.isEmpty()) { + mImpl << " m" << key[0].toUpper() +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + << QStringView(key).mid(1) +#else + << key.midRef(1) +#endif + << " |= " << enumType << "(" << values.join(QLatin1String(" | ")) << ");\n"; + values.clear(); + } + } + values << *it; + } + + if (!values.isEmpty()) { + mImpl << " m" << key[0].toUpper() +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + << QStringView(key).mid(1) +#else + << key.midRef(1) +#endif + << " |= " << enumType << "(" << values.join(QLatin1String(" | ")) << ");\n"; + } +} + +bool CppGenerator::generateClass(ClassNode const *node) +{ + writeHeaderClass(node); + + mImpl << "\n\n/************************* " << node->className() << " *************************/\n\n"; + writeImplClass(node); + + return true; +} diff --git a/src/private/protocolgen/cppgenerator.h b/src/private/protocolgen/cppgenerator.h new file mode 100644 index 0000000..72e7daa --- /dev/null +++ b/src/private/protocolgen/cppgenerator.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class Node; +class DocumentNode; +class ClassNode; +class EnumNode; +class PropertyNode; + +class CppGenerator +{ +public: + explicit CppGenerator(); + ~CppGenerator(); + + bool generate(Node const *node); + +private: + bool generateDocument(DocumentNode const *node); + bool generateClass(ClassNode const *node); + +private: + void writeHeaderHeader(DocumentNode const *node); + void writeHeaderFooter(DocumentNode const *node); + void writeHeaderClass(ClassNode const *node); + void writeHeaderEnum(EnumNode const *node); + + void writeImplHeader(DocumentNode const *node); + void writeImplFooter(DocumentNode const *node); + void writeImplSerializer(DocumentNode const *node); + void writeImplClass(ClassNode const *node); + void writeImplSerializer(PropertyNode const *node, const char *streamingOperator); + + void writeImplPropertyDependencies(PropertyNode const *node); + +private: + QFile mHeaderFile; + QTextStream mHeader; + QFile mImplFile; + QTextStream mImpl; +}; + diff --git a/src/private/protocolgen/cpphelper.cpp b/src/private/protocolgen/cpphelper.cpp new file mode 100644 index 0000000..9505c5d --- /dev/null +++ b/src/private/protocolgen/cpphelper.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "cpphelper.h" +#include "nodetree.h" +#include "typehelper.h" + +#include +#include +#include +#include +#include +#include +namespace +{ +class Dummy; + +// FIXME: This is based on hacks and guesses, does not work for generated types +// and does not consider alignment. It should be good enough (TM) for our needs, +// but it would be nice to make it smarter, for example by looking up type sizes +// from the Node tree and understanding enums and QFlags types. +size_t typeSize(const QString &typeName) +{ + static QHash typeSizeLookup = {{"Scope", sizeof(QSharedDataPointer)}, + {"ScopeContext", sizeof(QSharedDataPointer)}, + {"QSharedPointer", sizeof(QSharedPointer)}, + {"Tristate", sizeof(qint8)}, + {"Akonadi::Protocol::Attributes", sizeof(QMap)}, + {"QSet", sizeof(QSet)}, + {"QVector", sizeof(QVector)}}; + + QByteArray tn; + // Don't you just loooove hacks? + // TODO: Extract underlying type during XML parsing + if (typeName.startsWith(QLatin1String("Akonadi::Protocol")) && typeName.endsWith(QLatin1String("Ptr"))) { + tn = "QSharedPointer"; + } else { + tn = TypeHelper::isContainer(typeName) ? TypeHelper::containerName(typeName).toUtf8() : typeName.toUtf8(); + } + auto it = typeSizeLookup.find(tn); + if (it == typeSizeLookup.end()) { + const auto typeId = QMetaType::type(tn); + const int size = QMetaType(typeId).sizeOf(); + // for types of unknown size int + it = typeSizeLookup.insert(tn, size ? size_t(size) : sizeof(int)); + } + return *it; +} + +} // namespace + +void CppHelper::sortMembers(QVector &props) +{ + std::sort(props.begin(), props.end(), [](PropertyNode const *lhs, PropertyNode const *rhs) { + return typeSize(lhs->type()) > typeSize(rhs->type()); + }); +} + +void CppHelper::sortMembersForSerialization(QVector &props) +{ + std::sort(props.begin(), props.end(), [](PropertyNode const *lhs, PropertyNode const *rhs) { + return lhs->dependencies().isEmpty() > rhs->dependencies().isEmpty(); + }); +} diff --git a/src/private/protocolgen/cpphelper.h b/src/private/protocolgen/cpphelper.h new file mode 100644 index 0000000..f2a5160 --- /dev/null +++ b/src/private/protocolgen/cpphelper.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +class PropertyNode; + +#include + +namespace CppHelper +{ +void sortMembers(QVector &props); + +void sortMembersForSerialization(QVector &props); + +} // namespace CppHelper + diff --git a/src/private/protocolgen/main.cpp b/src/private/protocolgen/main.cpp new file mode 100644 index 0000000..c5191df --- /dev/null +++ b/src/private/protocolgen/main.cpp @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include "cppgenerator.h" +#include "xmlparser.h" + +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + QCommandLineParser parser; + parser.addPositionalArgument(QStringLiteral("file"), QStringLiteral("File")); + parser.addHelpOption(); + parser.process(app); + + const auto args = parser.positionalArguments(); + if (args.isEmpty()) { + std::cerr << "No file specified" << std::endl; + return 1; + } + + XmlParser xmlParser; + if (!xmlParser.parse(args[0])) { + return -1; + } + + CppGenerator cppGenerator; + if (!cppGenerator.generate(xmlParser.tree())) { + return -2; + } else { + return 0; + } +} diff --git a/src/private/protocolgen/nodetree.cpp b/src/private/protocolgen/nodetree.cpp new file mode 100644 index 0000000..34f25f6 --- /dev/null +++ b/src/private/protocolgen/nodetree.cpp @@ -0,0 +1,309 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "nodetree.h" +#include "cpphelper.h" +#include "typehelper.h" + +Node::Node(NodeType type, Node *parent) + : mParent(parent) + , mType(type) +{ + if (parent) { + parent->appendNode(this); + } +} + +Node::~Node() +{ + qDeleteAll(mChildren); +} + +Node::NodeType Node::type() const +{ + return mType; +} + +Node *Node::parent() const +{ + return mParent; +} + +void Node::appendNode(Node *child) +{ + child->mParent = this; + mChildren.push_back(child); +} + +const QVector &Node::children() const +{ + return mChildren; +} + +DocumentNode::DocumentNode(int version) + : Node(Document, nullptr) + , mVersion(version) +{ +} + +int DocumentNode::version() const +{ + return mVersion; +} + +ClassNode::ClassNode(const QString &name, ClassType type, DocumentNode *parent) + : Node(Node::Class, parent) + , mName(name) + , mClassType(type) +{ +} + +QString ClassNode::name() const +{ + return mName; +} + +ClassNode::ClassType ClassNode::classType() const +{ + return mClassType; +} + +QString ClassNode::className() const +{ + switch (mClassType) { + case Class: + return mName; + case Command: + return mName + QStringLiteral("Command"); + case Response: + return mName + QStringLiteral("Response"); + case Notification: + return mName + QStringLiteral("Notification"); + default: + Q_ASSERT(false); + return QString(); + } +} + +QString ClassNode::parentClassName() const +{ + switch (mClassType) { + case Class: + return QString(); + case Command: + return QStringLiteral("Command"); + case Response: + return QStringLiteral("Response"); + case Notification: + return QStringLiteral("ChangeNotification"); + case Invalid: + Q_ASSERT(false); + return QString(); + } + Q_UNREACHABLE(); +} + +ClassNode::ClassType ClassNode::elementNameToType(const QStringRef &name) +{ + if (name == QLatin1String("class")) { + return Class; + } else if (name == QLatin1String("command")) { + return Command; + } else if (name == QLatin1String("response")) { + return Response; + } else if (name == QLatin1String("notification")) { + return Notification; + } else { + return Invalid; + } +} + +QVector ClassNode::properties() const +{ + QVector rv; + for (const auto node : std::as_const(mChildren)) { + if (node->type() == Node::Property) { + rv << static_cast(node); + } + } + CppHelper::sortMembers(rv); + return rv; +} + +CtorNode::CtorNode(const QVector &args, ClassNode *parent) + : Node(Ctor, parent) + , mArgs(args) +{ +} + +CtorNode::~CtorNode() +{ +} + +QVector CtorNode::arguments() const +{ + return mArgs; +} + +void CtorNode::setArgumentType(const QString &name, const QString &type) +{ + for (auto &arg : mArgs) { + if (arg.name == name) { + arg.type = type; + break; + } + } +} + +EnumNode::EnumNode(const QString &name, EnumType type, ClassNode *parent) + : Node(Enum, parent) + , mName(name) + , mEnumType(type) +{ +} + +QString EnumNode::name() const +{ + return mName; +} + +EnumNode::EnumType EnumNode::enumType() const +{ + return mEnumType; +} + +EnumNode::EnumType EnumNode::elementNameToType(const QStringRef &name) +{ + if (name == QLatin1String("enum")) { + return TypeEnum; + } else if (name == QLatin1String("flag")) { + return TypeFlag; + } else { + return TypeInvalid; + } +} + +EnumValueNode::EnumValueNode(const QString &name, EnumNode *parent) + : Node(EnumValue, parent) + , mName(name) + , mValue() +{ +} + +QString EnumValueNode::name() const +{ + return mName; +} + +void EnumValueNode::setValue(const QString &value) +{ + mValue = value; +} + +QString EnumValueNode::value() const +{ + return mValue; +} + +PropertyNode::PropertyNode(const QString &name, const QString &type, ClassNode *parent) + : Node(Property, parent) + , mName(name) + , mType(type) + , mSetter(nullptr) + , mReadOnly(false) + , mAsReference(false) +{ +} + +PropertyNode::~PropertyNode() +{ + delete mSetter; +} + +QString PropertyNode::type() const +{ + return mType; +} + +QString PropertyNode::name() const +{ + return mName; +} + +void PropertyNode::setDefaultValue(const QString &defaultValue) +{ + mDefaultValue = defaultValue; +} + +QString PropertyNode::defaultValue() const +{ + return mDefaultValue; +} + +bool PropertyNode::readOnly() const +{ + return mReadOnly; +} + +void PropertyNode::setReadOnly(bool readOnly) +{ + mReadOnly = readOnly; +} + +bool PropertyNode::asReference() const +{ + return mAsReference; +} + +void PropertyNode::setAsReference(bool asReference) +{ + mAsReference = asReference; +} + +bool PropertyNode::isPointer() const +{ + return TypeHelper::isPointerType(mType); +} + +QMultiMap PropertyNode::dependencies() const +{ + return mDepends; +} + +void PropertyNode::addDependency(const QString &enumVar, const QString &enumValue) +{ + mDepends.insert(enumVar, enumValue); +} + +void PropertyNode::setSetter(Setter *setter) +{ + mSetter = setter; +} + +PropertyNode::Setter *PropertyNode::setter() const +{ + return mSetter; +} + +QString PropertyNode::mVariableName() const +{ + return QStringLiteral("m") + mName[0].toUpper() + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + QStringView(mName).mid(1); +#else + mName.midRef(1); +#endif +} + +QString PropertyNode::setterName() const +{ + return QStringLiteral("set") + mName[0].toUpper() + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + QStringView(mName).mid(1); +#else + mName.midRef(1); +#endif +} diff --git a/src/private/protocolgen/nodetree.h b/src/private/protocolgen/nodetree.h new file mode 100644 index 0000000..be97d37 --- /dev/null +++ b/src/private/protocolgen/nodetree.h @@ -0,0 +1,191 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class Node +{ +public: + enum NodeType { + Document, + Class, + Ctor, + Enum, + EnumValue, + Property, + }; + + Node(NodeType type, Node *parent); + Node(const Node &) = delete; + Node(Node &&) = delete; + virtual ~Node(); + + Node &operator=(const Node &) = delete; + Node &operator=(Node &&) = delete; + + NodeType type() const; + Node *parent() const; + + void appendNode(Node *child); + + const QVector &children() const; + +protected: + Node *mParent; + QVector mChildren; + NodeType mType; +}; + +class DocumentNode : public Node +{ +public: + DocumentNode(int version); + + int version() const; + +private: + int mVersion; +}; + +class PropertyNode; +class ClassNode : public Node +{ +public: + enum ClassType { + Invalid, + Class, + Command, + Response, + Notification, + }; + + ClassNode(const QString &name, ClassType type, DocumentNode *parent); + QString name() const; + ClassType classType() const; + QString className() const; + QString parentClassName() const; + QVector properties() const; + + static ClassType elementNameToType(const QStringRef &name); + +private: + QString mName; + ClassType mClassType; +}; + +class CtorNode : public Node +{ +public: + struct Argument { + QString name; + QString type; + QString defaultValue; + + QString mVariableName() const + { + return QStringLiteral("m") + name[0].toUpper() + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + QStringView(name).mid(1); +#else + name.midRef(1); +#endif + } + }; + + CtorNode(const QVector &args, ClassNode *parent); + ~CtorNode(); + + QVector arguments() const; + void setArgumentType(const QString &name, const QString &type); + +private: + QVector mArgs; +}; + +class EnumNode : public Node +{ +public: + enum EnumType { + TypeInvalid, + TypeEnum, + TypeFlag, + }; + + EnumNode(const QString &name, EnumType type, ClassNode *parent); + + QString name() const; + EnumType enumType() const; + + static EnumType elementNameToType(const QStringRef &name); + +private: + QString mName; + EnumType mEnumType; +}; + +class EnumValueNode : public Node +{ +public: + EnumValueNode(const QString &name, EnumNode *parent); + + QString name() const; + void setValue(const QString &value); + QString value() const; + +private: + QString mName; + QString mValue; +}; + +class PropertyNode : public Node +{ +public: + struct Setter { + QString name; + QString type; + QString append; + QString remove; + }; + + PropertyNode(const QString &name, const QString &type, ClassNode *parent); + ~PropertyNode(); + + QString type() const; + QString name() const; + + void setDefaultValue(const QString &defaultValue); + QString defaultValue() const; + + bool readOnly() const; + void setReadOnly(bool readOnly); + + bool asReference() const; + void setAsReference(bool asReference); + + bool isPointer() const; + + QMultiMap dependencies() const; + void addDependency(const QString &enumVar, const QString &enumValue); + + Setter *setter() const; + void setSetter(Setter *setter); + + QString mVariableName() const; + QString setterName() const; + +private: + QString mName; + QString mType; + QString mDefaultValue; + QMultiMap mDepends; + Setter *mSetter; + bool mReadOnly; + bool mAsReference; +}; + diff --git a/src/private/protocolgen/typehelper.cpp b/src/private/protocolgen/typehelper.cpp new file mode 100644 index 0000000..b654f3f --- /dev/null +++ b/src/private/protocolgen/typehelper.cpp @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "typehelper.h" +#include "nodetree.h" + +#include +#include + +bool TypeHelper::isNumericType(const QString &name) +{ + const int metaTypeId = QMetaType::type(qPrintable(name)); + if (metaTypeId == -1) { + return false; + } + + switch (metaTypeId) { + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::Double: + case QMetaType::Long: + case QMetaType::LongLong: + case QMetaType::Short: + case QMetaType::ULong: + case QMetaType::ULongLong: + case QMetaType::UShort: + case QMetaType::Float: + return true; + default: + return false; + } +} + +bool TypeHelper::isBoolType(const QString &name) +{ + const int metaTypeId = QMetaType::type(qPrintable(name)); + if (metaTypeId == -1) { + return false; + } + + switch (metaTypeId) { + case QMetaType::Bool: + return true; + default: + return false; + } +} + +bool TypeHelper::isBuiltInType(const QString &type) +{ + // TODO: should be smarter than this.... + return !type.startsWith(QLatin1String("Akonadi::Protocol")) || type == QLatin1String("Akonadi::Protocol::Attributes") // typedef to QMap + || (type.startsWith(QLatin1String("Akonadi::Protocol")) // enums + && type.count(QStringLiteral("::")) > 2); +} + +bool TypeHelper::isContainer(const QString &type) +{ + const int tplB = type.indexOf(QLatin1Char('<')); + const int tplE = type.lastIndexOf(QLatin1Char('>')); + return tplB > -1 && tplE > -1 && tplB < tplE; +} + +QString TypeHelper::containerType(const QString &type) +{ + const int tplB = type.indexOf(QLatin1Char('<')); + const int tplE = type.indexOf(QLatin1Char('>')); + return type.mid(tplB + 1, tplE - tplB - 1); +} + +QString TypeHelper::containerName(const QString &type) +{ + const int tplB = type.indexOf(QLatin1Char('<')); + return type.left(tplB); +} + +bool TypeHelper::isPointerType(const QString &type) +{ + return type.endsWith(QLatin1String("Ptr")); +} diff --git a/src/private/protocolgen/typehelper.h b/src/private/protocolgen/typehelper.h new file mode 100644 index 0000000..7f7b1bc --- /dev/null +++ b/src/private/protocolgen/typehelper.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +class QString; + +namespace TypeHelper +{ +bool isNumericType(const QString &name); +bool isBoolType(const QString &name); + +/** + * Returns true if @p node is of C++ or Qt type, C++ if it's a generated type + */ +bool isBuiltInType(const QString &type); + +bool isContainer(const QString &type); + +QString containerType(const QString &type); +QString containerName(const QString &type); +bool isPointerType(const QString &type); + +} diff --git a/src/private/protocolgen/xmlparser.cpp b/src/private/protocolgen/xmlparser.cpp new file mode 100644 index 0000000..070bd2e --- /dev/null +++ b/src/private/protocolgen/xmlparser.cpp @@ -0,0 +1,276 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmlparser.h" +#include "nodetree.h" + +#include + +#include + +#define qPrintableRef(x) reinterpret_cast((x).unicode()) + +XmlParser::XmlParser() +{ +} + +XmlParser::~XmlParser() +{ +} + +Node const *XmlParser::tree() const +{ + return mTree.get(); +} + +bool XmlParser::parse(const QString &filename) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) { + std::cerr << qPrintable(file.errorString()); + return false; + } + + mReader.setDevice(&file); + while (!mReader.atEnd()) { + mReader.readNext(); + if (mReader.isStartElement() && mReader.name() == QLatin1String("protocol")) { + if (!parseProtocol()) { + return false; + } + } + } + + return true; +} + +bool XmlParser::parseProtocol() +{ + Q_ASSERT(mReader.name() == QLatin1String("protocol")); + + const auto attrs = mReader.attributes(); + if (!attrs.hasAttribute(QLatin1String("version"))) { + printError(QStringLiteral("Missing \"version\" attribute in tag!")); + return false; + } + + auto documentNode = std::make_unique(attrs.value(QLatin1String("version")).toInt()); + + while (!mReader.atEnd() && !(mReader.isEndElement() && mReader.name() == QLatin1String("protocol"))) { + mReader.readNext(); + if (mReader.isStartElement()) { + const auto elemName = mReader.name(); + if (elemName == QLatin1String("class") || elemName == QLatin1String("command") || elemName == QLatin1String("response") + || elemName == QLatin1String("notification")) { + if (!parseCommand(documentNode.get())) { + return false; + } + } else { + printError(QStringLiteral("Unsupported tag: ").append(mReader.name())); + return false; + } + } + } + + mTree = std::move(documentNode); + + return true; +} + +bool XmlParser::parseCommand(DocumentNode *documentNode) +{ + const auto attrs = mReader.attributes(); + if (!attrs.hasAttribute(QLatin1String("name"))) { + printError(QStringLiteral("Missing \"name\" attribute in command tag!")); + return false; + } + + auto classNode = new ClassNode(attrs.value(QLatin1String("name")).toString(), ClassNode::elementNameToType(mReader.name()), documentNode); + new CtorNode({}, classNode); + + while (!mReader.atEnd() && !(mReader.isEndElement() && classNode->classType() == ClassNode::elementNameToType(mReader.name()))) { + mReader.readNext(); + if (mReader.isStartElement()) { + if (mReader.name() == QLatin1String("ctor")) { + if (!parseCtor(classNode)) { + return false; + } + } else if (mReader.name() == QLatin1String("enum") || mReader.name() == QLatin1String("flag")) { + if (!parseEnum(classNode)) { + return false; + } + } else if (mReader.name() == QLatin1String("param")) { + if (!parseParam(classNode)) { + return false; + } + } else { + printError(QStringLiteral("Unsupported tag: ").append(mReader.name())); + return false; + } + } + } + + return true; +} + +bool XmlParser::parseCtor(ClassNode *classNode) +{ + QVector args; + while (!mReader.atEnd() && !(mReader.isEndElement() && (mReader.name() == QLatin1String("ctor")))) { + mReader.readNext(); + if (mReader.isStartElement()) { + if (mReader.name() == QLatin1String("arg")) { + const auto attrs = mReader.attributes(); + const QString name = attrs.value(QLatin1String("name")).toString(); + const QString def = attrs.value(QLatin1String("default")).toString(); + args << CtorNode::Argument{name, QString(), def}; + } else { + printError(QStringLiteral("Unsupported tag: ").append(mReader.name())); + return false; + } + } + } + new CtorNode(args, classNode); + + return true; +} + +bool XmlParser::parseEnum(ClassNode *classNode) +{ + const auto attrs = mReader.attributes(); + if (!attrs.hasAttribute(QLatin1String("name"))) { + printError(QStringLiteral("Missing \"name\" attribute in enum/flag tag!")); + return false; + } + + auto enumNode = new EnumNode(attrs.value(QLatin1String("name")).toString(), EnumNode::elementNameToType(mReader.name()), classNode); + + while (!mReader.atEnd() && !(mReader.isEndElement() && (enumNode->enumType() == EnumNode::elementNameToType(mReader.name())))) { + mReader.readNext(); + if (mReader.isStartElement()) { + if (mReader.name() == QLatin1String("value")) { + if (!parseEnumValue(enumNode)) { + return false; + } + } else { + printError(QStringLiteral("Invalid tag inside of enum/flag tag: ").append(mReader.name())); + return false; + } + } + } + + return true; +} + +bool XmlParser::parseEnumValue(EnumNode *enumNode) +{ + Q_ASSERT(mReader.name() == QLatin1String("value")); + + const auto attrs = mReader.attributes(); + if (!attrs.hasAttribute(QLatin1String("name"))) { + printError(QStringLiteral("Missing \"name\" attribute in tag!")); + return false; + } + + auto valueNode = new EnumValueNode(attrs.value(QLatin1String("name")).toString(), enumNode); + if (attrs.hasAttribute(QLatin1String("value"))) { + valueNode->setValue(attrs.value(QLatin1String("value")).toString()); + } + + return true; +} + +bool XmlParser::parseParam(ClassNode *classNode) +{ + Q_ASSERT(mReader.name() == QLatin1String("param")); + + const auto attrs = mReader.attributes(); + if (!attrs.hasAttribute(QLatin1String("name"))) { + printError(QStringLiteral("Missing \"name\" attribute in tag!")); + return false; + } + if (!attrs.hasAttribute(QLatin1String("type"))) { + printError(QStringLiteral("Missing \"type\" attribute in tag!")); + return false; + } + + const auto name = attrs.value(QLatin1String("name")).toString(); + const auto type = attrs.value(QLatin1String("type")).toString(); + + for (const auto child : classNode->children()) { + if (child->type() == Node::Ctor) { + auto ctor = const_cast(static_cast(child)); + ctor->setArgumentType(name, type); + } + } + + auto paramNode = new PropertyNode(name, type, classNode); + + if (attrs.hasAttribute(QLatin1String("default"))) { + paramNode->setDefaultValue(attrs.value(QLatin1String("default")).toString()); + } + if (attrs.hasAttribute(QLatin1String("readOnly"))) { + paramNode->setReadOnly(attrs.value(QLatin1String("readOnly")) == QLatin1String("true")); + } + if (attrs.hasAttribute(QLatin1String("asReference"))) { + paramNode->setAsReference(attrs.value(QLatin1String("asReference")) == QLatin1String("true")); + } + + while (!mReader.atEnd() && !(mReader.isEndElement() && mReader.name() == QLatin1String("param"))) { + mReader.readNext(); + if (mReader.isStartElement()) { + if (mReader.name() == QLatin1String("setter")) { + if (!parseSetter(paramNode)) { + return false; + } + } else if (mReader.name() == QLatin1String("depends")) { + auto dependsAttrs = mReader.attributes(); + if (!dependsAttrs.hasAttribute(QLatin1String("enum"))) { + printError(QStringLiteral("Missing \"enum\" attribute in tag!")); + return false; + } + if (!dependsAttrs.hasAttribute(QLatin1String("value"))) { + printError(QStringLiteral("Missing \"value\" attribute in tag!")); + return false; + } + paramNode->addDependency(dependsAttrs.value(QLatin1String("enum")).toString(), dependsAttrs.value(QLatin1String("value")).toString()); + } else { + printError(QStringLiteral("Unknown tag: ").append(mReader.name())); + return false; + } + } + } + + return true; +} + +bool XmlParser::parseSetter(PropertyNode *parent) +{ + const auto attrs = mReader.attributes(); + auto setter = new PropertyNode::Setter; + setter->name = attrs.value(QLatin1String("name")).toString(); + setter->type = attrs.value(QLatin1String("type")).toString(); + + while (!mReader.atEnd() && !(mReader.isEndElement() && mReader.name() == QLatin1String("setter"))) { + mReader.readNext(); + if (mReader.isStartElement()) { + if (mReader.name() == QLatin1String("append")) { + setter->append = mReader.attributes().value(QLatin1String("name")).toString(); + } else if (mReader.name() == QLatin1String("remove")) { + setter->remove = mReader.attributes().value(QLatin1String("name")).toString(); + } + } + } + + parent->setSetter(setter); + + return true; +} + +void XmlParser::printError(const QString &error) +{ + std::cerr << "Error:" << mReader.lineNumber() << ":" << mReader.columnNumber() << ": " << qPrintable(error) << std::endl; +} diff --git a/src/private/protocolgen/xmlparser.h b/src/private/protocolgen/xmlparser.h new file mode 100644 index 0000000..40ee1fc --- /dev/null +++ b/src/private/protocolgen/xmlparser.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class Node; +class DocumentNode; +class EnumNode; +class ClassNode; +class PropertyNode; + +class XmlParser +{ +public: + explicit XmlParser(); + ~XmlParser(); + + bool parse(const QString &filename); + + Node const *tree() const; + +private: + bool parseProtocol(); + bool parseCommand(DocumentNode *parent); + bool parseEnum(ClassNode *parent); + bool parseEnumValue(EnumNode *parent); + bool parseParam(ClassNode *parent); + bool parseCtor(ClassNode *parent); + bool parseSetter(PropertyNode *parent); + + void printError(const QString &error); + +private: + QXmlStreamReader mReader; + std::unique_ptr mTree; +}; + diff --git a/src/private/scope.cpp b/src/private/scope.cpp new file mode 100644 index 0000000..1f9d66e --- /dev/null +++ b/src/private/scope.cpp @@ -0,0 +1,407 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "datastream_p_p.h" +#include "scope_p.h" + +#include +#include +#include + +#include "imapset_p.h" + +namespace Akonadi +{ +class ScopePrivate : public QSharedData +{ +public: + ImapSet uidSet; + QStringList ridSet; + QVector hridChain; + QStringList gidSet; + Scope::SelectionScope scope = Scope::Invalid; +}; + +Scope::HRID::HRID() + : id(-1) +{ +} + +Scope::HRID::HRID(qint64 id, const QString &remoteId) + : id(id) + , remoteId(remoteId) +{ +} + +Scope::HRID::HRID(const HRID &other) + : id(other.id) + , remoteId(other.remoteId) +{ +} + +Scope::HRID::HRID(HRID &&other) noexcept + : id(other.id) +{ + remoteId.swap(other.remoteId); +} + +Scope::HRID &Scope::HRID::operator=(const HRID &other) +{ + if (*this == other) { + return *this; + } + + id = other.id; + remoteId = other.remoteId; + return *this; +} + +Scope::HRID &Scope::HRID::operator=(HRID &&other) noexcept +{ + if (*this == other) { + return *this; + } + + id = other.id; + remoteId.swap(other.remoteId); + return *this; +} + +bool Scope::HRID::isEmpty() const +{ + return id <= 0 && remoteId.isEmpty(); +} + +bool Scope::HRID::operator==(const HRID &other) const +{ + return id == other.id && remoteId == other.remoteId; +} + +void Scope::HRID::toJson(QJsonObject &json) const +{ + json[QStringLiteral("ID")] = id; + json[QStringLiteral("RemoteID")] = remoteId; +} + +Scope::Scope() + : d(new ScopePrivate) +{ +} + +Scope::Scope(qint64 id) + : d(new ScopePrivate) +{ + setUidSet(id); +} + +Scope::Scope(const ImapSet &set) + : d(new ScopePrivate) +{ + setUidSet(set); +} + +Scope::Scope(const ImapInterval &interval) + : d(new ScopePrivate) +{ + setUidSet(interval); +} + +Scope::Scope(const QVector &interval) + : d(new ScopePrivate) +{ + setUidSet(interval); +} + +Scope::Scope(SelectionScope scope, const QStringList &ids) + : d(new ScopePrivate) +{ + Q_ASSERT(scope == Rid || scope == Gid); + if (scope == Rid) { + d->scope = scope; + d->ridSet = ids; + } else if (scope == Gid) { + d->scope = scope; + d->gidSet = ids; + } +} + +Scope::Scope(const QVector &hrid) + : d(new ScopePrivate) +{ + d->scope = HierarchicalRid; + d->hridChain = hrid; +} + +Scope::Scope(const Scope &other) + : d(other.d) +{ +} + +Scope::Scope(Scope &&other) noexcept +{ + d.swap(other.d); +} + +Scope::~Scope() +{ +} + +Scope &Scope::operator=(const Scope &other) +{ + d = other.d; + return *this; +} + +Scope &Scope::operator=(Scope &&other) noexcept +{ + d.swap(other.d); + return *this; +} + +bool Scope::operator==(const Scope &other) const +{ + if (d->scope != other.d->scope) { + return false; + } + + switch (d->scope) { + case Uid: + return d->uidSet == other.d->uidSet; + case Gid: + return d->gidSet == other.d->gidSet; + case Rid: + return d->ridSet == other.d->ridSet; + case HierarchicalRid: + return d->hridChain == other.d->hridChain; + case Invalid: + return true; + } + + Q_ASSERT(false); + return false; +} + +bool Scope::operator!=(const Scope &other) const +{ + return !(*this == other); +} + +Scope::SelectionScope Scope::scope() const +{ + return d->scope; +} + +bool Scope::isEmpty() const +{ + switch (d->scope) { + case Invalid: + return true; + case Uid: + return d->uidSet.isEmpty(); + case Rid: + return d->ridSet.isEmpty(); + case HierarchicalRid: + return d->hridChain.isEmpty(); + case Gid: + return d->gidSet.isEmpty(); + } + + Q_ASSERT(false); + return true; +} + +void Scope::setUidSet(const ImapSet &uidSet) +{ + d->scope = Uid; + d->uidSet = uidSet; +} + +ImapSet Scope::uidSet() const +{ + return d->uidSet; +} + +void Scope::setRidSet(const QStringList &ridSet) +{ + d->scope = Rid; + d->ridSet = ridSet; +} + +QStringList Scope::ridSet() const +{ + return d->ridSet; +} + +void Scope::setHRidChain(const QVector &hridChain) +{ + d->scope = HierarchicalRid; + d->hridChain = hridChain; +} + +QVector Scope::hridChain() const +{ + return d->hridChain; +} + +void Scope::setGidSet(const QStringList &gidSet) +{ + d->scope = Gid; + d->gidSet = gidSet; +} + +QStringList Scope::gidSet() const +{ + return d->gidSet; +} + +qint64 Scope::uid() const +{ + if (d->uidSet.intervals().size() == 1 && d->uidSet.intervals().at(0).size() == 1) { + return d->uidSet.intervals().at(0).begin(); + } + + // TODO: Error handling! + return -1; +} + +QString Scope::rid() const +{ + if (d->ridSet.size() != 1) { + // TODO: Error handling! + Q_ASSERT(d->ridSet.size() == 1); + return QString(); + } + return d->ridSet.at(0); +} + +QString Scope::gid() const +{ + if (d->gidSet.size() != 1) { + // TODO: Error handling! + Q_ASSERT(d->gidSet.size() == 1); + return QString(); + } + return d->gidSet.at(0); +} + +void Scope::toJson(QJsonObject &json) const +{ + switch (scope()) { + case Scope::Uid: + json[QStringLiteral("type")] = QStringLiteral("UID"); + json[QStringLiteral("value")] = QString::fromUtf8(uidSet().toImapSequenceSet()); + break; + case Scope::Rid: + json[QStringLiteral("type")] = QStringLiteral("RID"); + json[QStringLiteral("value")] = QJsonArray::fromStringList(ridSet()); + break; + case Scope::Gid: + json[QStringLiteral("type")] = QStringLiteral("GID"); + json[QStringLiteral("value")] = QJsonArray::fromStringList(gidSet()); + break; + case Scope::HierarchicalRid: { + const auto &chain = hridChain(); + QJsonArray hridArray; + for (const auto &hrid : chain) { + QJsonObject obj; + hrid.toJson(obj); + hridArray.append(obj); + } + json[QStringLiteral("type")] = QStringLiteral("HRID"); + json[QStringLiteral("value")] = hridArray; + } break; + default: + json[QStringLiteral("type")] = QStringLiteral("invalid"); + json[QStringLiteral("value")] = QJsonValue(static_cast(scope())); + } +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::Scope &scope) +{ + stream << static_cast(scope.d->scope); + switch (scope.d->scope) { + case Scope::Invalid: + return stream; + case Scope::Uid: + stream << scope.d->uidSet; + return stream; + case Scope::Rid: + stream << scope.d->ridSet; + return stream; + case Scope::HierarchicalRid: + stream << scope.d->hridChain; + return stream; + case Scope::Gid: + stream << scope.d->gidSet; + return stream; + } + + return stream; +} + +Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::Scope::HRID &hrid) +{ + return stream << hrid.id << hrid.remoteId; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::Scope::HRID &hrid) +{ + return stream >> hrid.id >> hrid.remoteId; +} + +Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::Scope &scope) +{ + scope.d->uidSet = ImapSet(); + scope.d->ridSet.clear(); + scope.d->hridChain.clear(); + scope.d->gidSet.clear(); + + stream >> reinterpret_cast(scope.d->scope); + switch (scope.d->scope) { + case Scope::Invalid: + return stream; + case Scope::Uid: + stream >> scope.d->uidSet; + return stream; + case Scope::Rid: + stream >> scope.d->ridSet; + return stream; + case Scope::HierarchicalRid: + stream >> scope.d->hridChain; + return stream; + case Scope::Gid: + stream >> scope.d->gidSet; + return stream; + } + + return stream; +} + +} // namespace Akonadi + +using namespace Akonadi; + +QDebug operator<<(QDebug dbg, const Akonadi::Scope::HRID &hrid) +{ + return dbg.nospace() << "(ID: " << hrid.id << ", RemoteID: " << hrid.remoteId << ")"; +} + +QDebug operator<<(QDebug dbg, const Akonadi::Scope &scope) +{ + switch (scope.scope()) { + case Scope::Uid: + return dbg.nospace() << "UID " << scope.uidSet(); + case Scope::Rid: + return dbg.nospace() << "RID " << scope.ridSet(); + case Scope::Gid: + return dbg.nospace() << "GID " << scope.gidSet(); + case Scope::HierarchicalRid: + return dbg.nospace() << "HRID " << scope.hridChain(); + default: + return dbg.nospace() << "Invalid scope"; + } +} diff --git a/src/private/scope_p.h b/src/private/scope_p.h new file mode 100644 index 0000000..946bc64 --- /dev/null +++ b/src/private/scope_p.h @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2009 Volker Krause + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +// krazy:excludeall=dpointer + +#pragma once + +#include "akonadiprivate_export.h" + +#include + +#include +class QJsonObject; +#include + +namespace Akonadi +{ +class ImapSet; +class ImapInterval; + +namespace Protocol +{ +class DataStream; +} + +class ScopePrivate; +class AKONADIPRIVATE_EXPORT Scope +{ +public: + enum SelectionScope : uchar { + Invalid = 0, + Uid = 1 << 0, + Rid = 1 << 1, + HierarchicalRid = 1 << 2, + Gid = 1 << 3, + }; + + class AKONADIPRIVATE_EXPORT HRID + { + public: + HRID(); + explicit HRID(qint64 id, const QString &remoteId = QString()); + HRID(const HRID &other); + HRID(HRID &&other) noexcept; + + HRID &operator=(const HRID &other); + HRID &operator=(HRID &&other) noexcept; + + ~HRID() = default; + + bool isEmpty() const; + bool operator==(const HRID &other) const; + + void toJson(QJsonObject &json) const; + + qint64 id; + QString remoteId; + }; + + explicit Scope(); + Scope(SelectionScope scope, const QStringList &ids); + + /* UID */ + Scope(qint64 id); // krazy:exclude=explicit + Scope(const ImapSet &uidSet); // krazy:exclude=explicit + Scope(const ImapInterval &interval); // krazy:exclude=explicit + Scope(const QVector &interval); // krazy:exclude=explicit + Scope(const QVector &hridChain); // krazy:exclude=explicit + + Scope(const Scope &other); + Scope(Scope &&other) noexcept; + ~Scope(); + + Scope &operator=(const Scope &other); + Scope &operator=(Scope &&other) noexcept; + + bool operator==(const Scope &other) const; + bool operator!=(const Scope &other) const; + + SelectionScope scope() const; + + bool isEmpty() const; + + ImapSet uidSet() const; + void setUidSet(const ImapSet &uidSet); + + void setRidSet(const QStringList &ridSet); + QStringList ridSet() const; + + void setHRidChain(const QVector &ridChain); + QVector hridChain() const; + + void setGidSet(const QStringList &gidChain); + QStringList gidSet() const; + + qint64 uid() const; + QString rid() const; + QString gid() const; + + void toJson(QJsonObject &json) const; + +private: + QSharedDataPointer d; + friend class ScopePrivate; + + friend Protocol::DataStream &operator<<(Protocol::DataStream &stream, const Akonadi::Scope &scope); + friend Protocol::DataStream &operator>>(Protocol::DataStream &stream, Akonadi::Scope &scope); +}; + +} // namespace Akonadi + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug debug, const Akonadi::Scope &scope); + diff --git a/src/private/standarddirs.cpp b/src/private/standarddirs.cpp new file mode 100644 index 0000000..954529f --- /dev/null +++ b/src/private/standarddirs.cpp @@ -0,0 +1,186 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadiprivate_debug.h" +#include "instance_p.h" +#include "standarddirs_p.h" + +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +namespace +{ +QString buildFullRelPath(const char *resource, const QString &relPath) +{ + QString fullRelPath = QStringLiteral("/akonadi"); +#ifdef Q_OS_WIN + // On Windows all Generic*Location fall into ~/AppData/Local so we need to disambiguate + // inside the "akonadi" folder whether it's data or config. + fullRelPath += QLatin1Char('/') + QString::fromLocal8Bit(resource); +#else + Q_UNUSED(resource) +#endif + + if (Akonadi::Instance::hasIdentifier()) { + fullRelPath += QLatin1String("/instance/") + Akonadi::Instance::identifier(); + } + if (!relPath.isEmpty()) { + fullRelPath += QLatin1Char('/') + relPath; + } + return fullRelPath; +} + +} // namespace + +QString StandardDirs::configFile(const QString &configFile, FileAccessMode openMode) +{ + const QString savePath = StandardDirs::saveDir("config") + QLatin1Char('/') + configFile; + if (openMode == WriteOnly) { + return savePath; + } + + auto path = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, QLatin1String("akonadi/") + configFile); + // HACK: when using instance namespaces, ignore the non-namespaced file + if (Akonadi::Instance::hasIdentifier() && path.startsWith(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation))) { + path.clear(); + } + + if (path.isEmpty()) { + return savePath; + } else if (openMode == ReadOnly || path == savePath) { + return path; + } + + // file found in system paths and mode is ReadWrite, thus + // we copy to the home path location and return this path + QFile::copy(path, savePath); + return savePath; +} + +QString StandardDirs::serverConfigFile(FileAccessMode openMode) +{ + return configFile(QStringLiteral("akonadiserverrc"), openMode); +} + +QString StandardDirs::connectionConfigFile(FileAccessMode openMode) +{ + return configFile(QStringLiteral("akonadiconnectionrc"), openMode); +} + +QString StandardDirs::agentsConfigFile(FileAccessMode openMode) +{ + return configFile(QStringLiteral("agentsrc"), openMode); +} + +QString StandardDirs::agentConfigFile(const QString &identifier, FileAccessMode openMode) +{ + return configFile(QStringLiteral("agent_config_") + identifier, openMode); +} + +QString StandardDirs::saveDir(const char *resource, const QString &relPath) +{ + const QString fullRelPath = buildFullRelPath(resource, relPath); + QString fullPath; + if (qstrncmp(resource, "config", 6) == 0) { + fullPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + fullRelPath; + } else if (qstrncmp(resource, "data", 4) == 0) { + fullPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + fullRelPath; + } else if (qstrncmp(resource, "runtime", 7) == 0) { + fullPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + fullRelPath; + } else { + qt_assert_x(__FUNCTION__, "Invalid resource type", __FILE__, __LINE__); + return {}; + } + + // ensure directory exists or is created + QFileInfo fileInfo(fullPath); + if (fileInfo.exists()) { + if (fileInfo.isDir()) { + return fullPath; + } else { + qCWarning(AKONADIPRIVATE_LOG) << "StandardDirs::saveDir: '" << fileInfo.absoluteFilePath() << "' exists but is not a directory"; + } + } else { + if (!QDir::home().mkpath(fileInfo.absoluteFilePath())) { + qCWarning(AKONADIPRIVATE_LOG) << "StandardDirs::saveDir: failed to create directory '" << fileInfo.absoluteFilePath() << "'"; + } else { + return fullPath; + } + } + + return {}; +} + +QString StandardDirs::locateResourceFile(const char *resource, const QString &relPath) +{ + const QString fullRelPath = buildFullRelPath(resource, relPath); + QVector userLocations; + QStandardPaths::StandardLocation genericLocation; + if (qstrncmp(resource, "config", 6) == 0) { + userLocations = {QStandardPaths::AppConfigLocation, QStandardPaths::ConfigLocation}; + genericLocation = QStandardPaths::GenericConfigLocation; + } else if (qstrncmp(resource, "data", 4) == 0) { + userLocations = {QStandardPaths::AppLocalDataLocation, QStandardPaths::AppDataLocation}; + genericLocation = QStandardPaths::GenericDataLocation; + } else { + qt_assert_x(__FUNCTION__, "Invalid resource type", __FILE__, __LINE__); + return {}; + } + + const auto locateFile = [](QStandardPaths::StandardLocation location, const QString &relPath) -> QString { + const auto path = QStandardPaths::locate(location, relPath); + if (!path.isEmpty()) { + QFileInfo file(path); + if (file.exists() && file.isFile() && file.isReadable()) { + return path; + } + } + return {}; + }; + + // Always honor instance in user-specific locations + for (const auto location : std::as_const(userLocations)) { + const auto path = locateFile(location, fullRelPath); + if (!path.isEmpty()) { + return path; + } + } + + // First try instance-specific path in generic locations + auto path = locateFile(genericLocation, fullRelPath); + if (!path.isEmpty()) { + return path; + } + + // Fallback to global instance path in generic locations + path = locateFile(genericLocation, QLatin1String("/akonadi/") + relPath); + if (!path.isEmpty()) { + return path; + } + + return {}; +} + +QStringList StandardDirs::locateAllResourceDirs(const QString &relPath) +{ + return QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, relPath, QStandardPaths::LocateDirectory); +} + +QString StandardDirs::findExecutable(const QString &executableName) +{ + QString executable = QStandardPaths::findExecutable(executableName, {qApp->applicationDirPath()}); + if (executable.isEmpty()) { + executable = QStandardPaths::findExecutable(executableName); + } + return executable; +} diff --git a/src/private/standarddirs_p.h b/src/private/standarddirs_p.h new file mode 100644 index 0000000..3d59421 --- /dev/null +++ b/src/private/standarddirs_p.h @@ -0,0 +1,100 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiprivate_export.h" + +#include + +namespace Akonadi +{ +/** + * Convenience wrappers on top of QStandardPaths that are instance namespace aware. + * @since 1.7 + */ +namespace StandardDirs +{ +/** + * @brief Open mode flags for resource files + * + * FileAccessMode is a typedef for QFlags. It stores + * a OR combination of FileAccessFlag values + */ +enum FileAccessMode { + ReadOnly = 0x1, + WriteOnly = 0x2, + ReadWrite = ReadOnly | WriteOnly, +}; + +/** + * Returns path to the config file @p configFile. + */ +AKONADIPRIVATE_EXPORT QString configFile(const QString &configFile, FileAccessMode openMode = ReadOnly); + +/** + * Returns the full path to the server config file (akonadiserverrc). + */ +AKONADIPRIVATE_EXPORT QString serverConfigFile(FileAccessMode openMode = ReadOnly); + +/** + * Returns the full path to the connection config file (akonadiconnectionrc). + */ +AKONADIPRIVATE_EXPORT QString connectionConfigFile(FileAccessMode openMode = ReadOnly); + +/** + * Returns the full path to the agentsrc config file + */ +AKONADIPRIVATE_EXPORT QString agentsConfigFile(FileAccessMode openMode = ReadOnly); + +/** + * Returns the full path to config file of agent @p identifier. + * + * Never returns empty string. + * + * @param identifier identifier of the agent (akonadi_foo_resource_0) + */ +AKONADIPRIVATE_EXPORT QString agentConfigFile(const QString &identifier, FileAccessMode openMode = ReadOnly); + +/** + * Instance-aware wrapper for QStandardPaths + * @note @p relPath does not need to include the "akonadi/" folder. + */ +AKONADIPRIVATE_EXPORT QString saveDir(const char *resource, const QString &relPath = QString()); + +/** + * @brief Searches the resource specific directories for a given file + * + * Convenience method for finding a given file (with optional relative path) + * in any of the configured base directories for a given resource type. + * + * Will check the user local directory first and then process the system + * wide path list according to the inherent priority. + * + * @param resource a named resource type, e.g. "config" + * @param relPath relative path of a file to look for, e.g."akonadi/akonadiserverrc" + * + * @returns the file path of the first match, or @c QString() if no such relative path + * exists in any of the base directories or if a match is not a file + */ +AKONADIPRIVATE_EXPORT QString locateResourceFile(const char *resource, const QString &relPath); + +/** + * Equivalent to QStandardPaths::locateAll() but always includes at least the + * default Akonadi compile prefix. + */ +AKONADIPRIVATE_EXPORT QStringList locateAllResourceDirs(const QString &relPath); + +/** + * Equivalent to QStandardPaths::findExecutable() but it looks in + * qApp->applicationDirPath() first. + */ + +AKONADIPRIVATE_EXPORT QString findExecutable(const QString &relPath); + +} +} + diff --git a/src/private/tristate.cpp b/src/private/tristate.cpp new file mode 100644 index 0000000..1fb6518 --- /dev/null +++ b/src/private/tristate.cpp @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "tristate_p.h" + +using namespace Akonadi; + +QDebug operator<<(QDebug dbg, Tristate tristate) +{ + switch (tristate) { + case Tristate::True: + return dbg << "True"; + case Tristate::False: + return dbg << "False"; + case Tristate::Undefined: + return dbg << "Undefined"; + } + + Q_ASSERT(false); + return dbg; +} diff --git a/src/private/tristate_p.h b/src/private/tristate_p.h new file mode 100644 index 0000000..788d0de --- /dev/null +++ b/src/private/tristate_p.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2015 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include "akonadiprivate_export.h" + +namespace Akonadi +{ +enum class Tristate : qint8 { + False = 0, + True = 1, + Undefined = 2, +}; +} + +Q_DECLARE_METATYPE(Akonadi::Tristate) + +AKONADIPRIVATE_EXPORT QDebug operator<<(QDebug dbg, Akonadi::Tristate tristate); + diff --git a/src/qsqlite/.no_coding_style b/src/qsqlite/.no_coding_style new file mode 100644 index 0000000..40fa8f6 --- /dev/null +++ b/src/qsqlite/.no_coding_style @@ -0,0 +1 @@ +# this directory will not be tested. diff --git a/src/qsqlite/CMakeLists.txt b/src/qsqlite/CMakeLists.txt new file mode 100644 index 0000000..a72dd03 --- /dev/null +++ b/src/qsqlite/CMakeLists.txt @@ -0,0 +1,31 @@ +set(QSqlite_SRCS + src/sqlite_blocking.cpp + src/qsql_sqlite.cpp + src/smain.cpp +) + +message(STATUS "Building QSQLITE3 driver") + +set(QSQLITE_INSTALL_PREFIX "${KDE_INSTALL_PLUGINDIR}/sqldrivers") + +# TODO KF6: Use Qt6::CorePrivate and Qt6::SqlPrivate +include_directories( + ${Qt5Core_PRIVATE_INCLUDE_DIRS} + ${Qt5Sql_PRIVATE_INCLUDE_DIRS} +) + +add_library(qsqlite3 SHARED ${QSqlite_SRCS} ${QSqlite_MOC_SRCS}) + +set_target_properties(qsqlite3 PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/sqldrivers") + +target_link_libraries(qsqlite3 + Qt::Core + Qt::Sql + Sqlite::Sqlite +) + +INSTALL(TARGETS qsqlite3 + RUNTIME DESTINATION ${QSQLITE_INSTALL_PREFIX} + LIBRARY DESTINATION ${QSQLITE_INSTALL_PREFIX} + ARCHIVE DESTINATION ${QSQLITE_INSTALL_PREFIX} +) diff --git a/src/qsqlite/README b/src/qsqlite/README new file mode 100644 index 0000000..72864b6 --- /dev/null +++ b/src/qsqlite/README @@ -0,0 +1,3 @@ +This is a sliglty adjusted version of the QSQLITE driver. Install this driver +somewhere in the QT_PLUGIN_PATH and use it in akonadi by setting the driver +to QSQLITE3. diff --git a/src/qsqlite/src/qsql_sqlite.cpp b/src/qsqlite/src/qsql_sqlite.cpp new file mode 100644 index 0000000..916e2e9 --- /dev/null +++ b/src/qsqlite/src/qsql_sqlite.cpp @@ -0,0 +1,818 @@ +/**************************************************************************** +** +** SPDX-FileCopyrightText: 2009 Nokia Corporation and /or its subsidiary(-ies). +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** SPDX-FileCopyrightText: LGPL-2.1-only WITH Qt-LGPL-exception-1.1 OR GPL-3.0-only +** +****************************************************************************/ + +#include "qsql_sqlite.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined Q_OS_WIN +#include +#else +#include +#endif + +#include + +#include "sqlite_blocking.h" +#include + +Q_DECLARE_OPAQUE_POINTER(sqlite3 *) +Q_DECLARE_OPAQUE_POINTER(sqlite3_stmt *) + +Q_DECLARE_METATYPE(sqlite3 *) +Q_DECLARE_METATYPE(sqlite3_stmt *) + +QT_BEGIN_NAMESPACE + +static QString _q_escapeIdentifier(const QString &identifier) +{ + QString res = identifier; + if (!identifier.isEmpty() && identifier.at(0) != QString(QLatin1Char('"')) && identifier.right(1) != QString(QLatin1Char('"'))) { + res.replace(QLatin1Char('"'), QStringLiteral("\"\"")); + res.prepend(QLatin1Char('"')).append(QLatin1Char('"')); + res.replace(QLatin1Char('.'), QStringLiteral("\".\"")); + } + return res; +} + +static QVariant::Type qGetColumnType(const QString &tpName) +{ + const QString typeName = tpName.toLower(); + + if (typeName == QLatin1String("integer") || typeName == QLatin1String("int")) { + return QVariant::Int; + } + if (typeName == QLatin1String("double") || typeName == QLatin1String("float") || typeName == QLatin1String("real") + || typeName.startsWith(QLatin1String("numeric"))) { + return QVariant::Double; + } + if (typeName == QLatin1String("blob")) { + return QVariant::ByteArray; + } + if (typeName == QLatin1String("boolean") || typeName == QLatin1String("bool")) { + return QVariant::Bool; + } + return QVariant::String; +} + +static QSqlError qMakeError(sqlite3 *access, const QString &descr, QSqlError::ErrorType type, int errorCode = -1) +{ + return QSqlError(descr, QString(reinterpret_cast(sqlite3_errmsg16(access))), type, QString::number(errorCode)); +} + +class QSQLiteResultPrivate; + +class QSQLiteResult : public QSqlCachedResult +{ + friend class QSQLiteDriver; + friend class QSQLiteResultPrivate; + +public: + explicit QSQLiteResult(const QSQLiteDriver *db); + ~QSQLiteResult() override; + QVariant handle() const override; + +protected: + bool gotoNext(QSqlCachedResult::ValueCache &row, int idx) override; + bool reset(const QString &query) override; + bool prepare(const QString &query) override; + bool exec() override; + int size() override; + int numRowsAffected() override; + QVariant lastInsertId() const override; + QSqlRecord record() const override; + void detachFromResultSet() override; + void virtual_hook(int id, void *data) override; + +private: + Q_DECLARE_PRIVATE(QSQLiteResult) +}; + +class QSQLiteDriverPrivate : public QSqlDriverPrivate +{ +public: + inline QSQLiteDriverPrivate() + : access(nullptr) + { + dbmsType = QSqlDriver::SQLite; + } + sqlite3 *access; + QList results; +}; + +class QSQLiteResultPrivate : public QSqlCachedResultPrivate +{ +public: + QSQLiteResultPrivate(QSQLiteResult *res, const QSQLiteDriver *drv); + + void cleanup(); + bool fetchNext(QSqlCachedResult::ValueCache &values, int idx, bool initialFetch); + // initializes the recordInfo and the cache + void initColumns(bool emptyResultset); + void finalize(); + + sqlite3 *access; + + sqlite3_stmt *stmt; + + bool skippedStatus; // the status of the fetchNext() that's skipped + bool skipRow; // skip the next fetchNext()? + QSqlRecord rInf; + QVector firstRow; + + Q_DECLARE_PUBLIC(QSQLiteResult) +}; + +QSQLiteResultPrivate::QSQLiteResultPrivate(QSQLiteResult *res, const QSQLiteDriver *drv) + : QSqlCachedResultPrivate(res, drv) + , access(nullptr) + , stmt(nullptr) + , skippedStatus(false) + , skipRow(false) +{ +} + +void QSQLiteResultPrivate::cleanup() +{ + Q_Q(QSQLiteResult); + finalize(); + rInf.clear(); + skippedStatus = false; + skipRow = false; + q->setAt(QSql::BeforeFirstRow); + q->setActive(false); + q->cleanup(); +} + +void QSQLiteResultPrivate::finalize() +{ + if (!stmt) { + return; + } + + sqlite3_finalize(stmt); + stmt = nullptr; +} + +void QSQLiteResultPrivate::initColumns(bool emptyResultset) +{ + Q_Q(QSQLiteResult); + int nCols = sqlite3_column_count(stmt); + if (nCols <= 0) { + return; + } + + q->init(nCols); + + for (int i = 0; i < nCols; ++i) { + QString colName = QString::fromUtf16(static_cast(sqlite3_column_name16(stmt, i))).remove(QLatin1Char('"')); + + // must use typeName for resolving the type to match QSqliteDriver::record + QString typeName = QString::fromUtf16(static_cast(sqlite3_column_decltype16(stmt, i))); + + // sqlite3_column_type is documented to have undefined behavior if the result set is empty + int stp = emptyResultset ? -1 : sqlite3_column_type(stmt, i); + + QVariant::Type fieldType; + + if (typeName.isEmpty()) { + fieldType = qGetColumnType(typeName); + } else { + // Get the proper type for the field based on stp value + switch (stp) { + case SQLITE_INTEGER: + fieldType = QVariant::Int; + break; + case SQLITE_FLOAT: + fieldType = QVariant::Double; + break; + case SQLITE_BLOB: + fieldType = QVariant::ByteArray; + break; + case SQLITE_TEXT: + fieldType = QVariant::String; + break; + case SQLITE_NULL: + default: + fieldType = QVariant::Invalid; + break; + } + } + + QSqlField fld(colName, fieldType); + fld.setSqlType(stp); + rInf.append(fld); + } +} + +bool QSQLiteResultPrivate::fetchNext(QSqlCachedResult::ValueCache &values, int idx, bool initialFetch) +{ + Q_Q(QSQLiteResult); + + int res; + int i; + + if (skipRow) { + // already fetched + Q_ASSERT(!initialFetch); + skipRow = false; + for (int i = 0; i < firstRow.count(); i++) { + values[i] = firstRow[i]; + } + return skippedStatus; + } + skipRow = initialFetch; + + if (initialFetch) { + firstRow.clear(); + firstRow.resize(sqlite3_column_count(stmt)); + } + + if (!stmt) { + q->setLastError(QSqlError(QCoreApplication::translate("QSQLiteResult", "Unable to fetch row"), + QCoreApplication::translate("QSQLiteResult", "No query"), + QSqlError::ConnectionError)); + q->setAt(QSql::AfterLastRow); + return false; + } + res = sqlite3_blocking_step(stmt); + + switch (res) { + case SQLITE_ROW: + // check to see if should fill out columns + if (rInf.isEmpty()) + // must be first call. + { + initColumns(false); + } + if (idx < 0 && !initialFetch) { + return true; + } + for (i = 0; i < rInf.count(); ++i) { + switch (sqlite3_column_type(stmt, i)) { + case SQLITE_BLOB: + values[i + idx] = QByteArray(static_cast(sqlite3_column_blob(stmt, i)), sqlite3_column_bytes(stmt, i)); + break; + case SQLITE_INTEGER: + values[i + idx] = sqlite3_column_int64(stmt, i); + break; + case SQLITE_FLOAT: + switch (q->numericalPrecisionPolicy()) { + case QSql::LowPrecisionInt32: + values[i + idx] = sqlite3_column_int(stmt, i); + break; + case QSql::LowPrecisionInt64: + values[i + idx] = sqlite3_column_int64(stmt, i); + break; + case QSql::LowPrecisionDouble: + case QSql::HighPrecision: + default: + values[i + idx] = sqlite3_column_double(stmt, i); + break; + }; + break; + case SQLITE_NULL: + values[i + idx] = QVariant(QVariant::String); + break; + default: + values[i + idx] = QString(reinterpret_cast(sqlite3_column_text16(stmt, i)), sqlite3_column_bytes16(stmt, i) / sizeof(QChar)); + break; + } + } + return true; + case SQLITE_DONE: + if (rInf.isEmpty()) + // must be first call. + { + initColumns(true); + } + q->setAt(QSql::AfterLastRow); + sqlite3_reset(stmt); + return false; + case SQLITE_CONSTRAINT: + case SQLITE_ERROR: + // SQLITE_ERROR is a generic error code and we must call sqlite3_reset() + // to get the specific error message. + res = sqlite3_reset(stmt); + q->setLastError(qMakeError(access, QCoreApplication::translate("QSQLiteResult", "Unable to fetch row"), QSqlError::ConnectionError, res)); + q->setAt(QSql::AfterLastRow); + return false; + case SQLITE_MISUSE: + case SQLITE_BUSY: + default: + // something wrong, don't get col info, but still return false + q->setLastError(qMakeError(access, QCoreApplication::translate("QSQLiteResult", "Unable to fetch row"), QSqlError::ConnectionError, res)); + sqlite3_reset(stmt); + q->setAt(QSql::AfterLastRow); + return false; + } + return false; +} + +QSQLiteResult::QSQLiteResult(const QSQLiteDriver *db) + : QSqlCachedResult(*new QSQLiteResultPrivate(this, db)) +{ + Q_D(QSQLiteResult); + d->access = db->d_func()->access; + const_cast(db->d_func())->results.append(this); +} + +QSQLiteResult::~QSQLiteResult() +{ + Q_D(QSQLiteResult); + const QSqlDriver *sqlDriver = driver(); + if (sqlDriver) { + const_cast(qobject_cast(sqlDriver)->d_func())->results.removeOne(this); + } + d->cleanup(); +} + +void QSQLiteResult::virtual_hook(int id, void *data) +{ + QSqlCachedResult::virtual_hook(id, data); +} + +bool QSQLiteResult::reset(const QString &query) +{ + if (!prepare(query)) { + return false; + } + return exec(); +} + +bool QSQLiteResult::prepare(const QString &query) +{ + Q_D(QSQLiteResult); + + if (!driver() || !driver()->isOpen() || driver()->isOpenError()) { + return false; + } + + d->cleanup(); + + setSelect(false); + + const void *pzTail = nullptr; + +#if (SQLITE_VERSION_NUMBER >= 3003011) + // int res = sqlite3_prepare16_v2(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), + // &d->stmt, 0); + int res = sqlite3_blocking_prepare16_v2(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), &d->stmt, &pzTail); +#else + int res = sqlite3_prepare16(d->access, query.constData(), (query.size() + 1) * sizeof(QChar), &d->stmt, &pzTail); +#endif + + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", "Unable to execute statement"), QSqlError::StatementError, res)); + d->finalize(); + return false; + } else if (pzTail && !QString(reinterpret_cast(pzTail)).trimmed().isEmpty()) { + setLastError(qMakeError(d->access, + QCoreApplication::translate("QSQLiteResult", "Unable to execute multiple statements at a time"), + QSqlError::StatementError, + SQLITE_MISUSE)); + d->finalize(); + return false; + } + return true; +} + +bool QSQLiteResult::exec() +{ + Q_D(QSQLiteResult); + const QVector values = boundValues(); + + d->skippedStatus = false; + d->skipRow = false; + d->rInf.clear(); + clearValues(); + setLastError(QSqlError()); + + int res = sqlite3_reset(d->stmt); + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", "Unable to reset statement"), QSqlError::StatementError, res)); + d->finalize(); + return false; + } + int paramCount = sqlite3_bind_parameter_count(d->stmt); + if (paramCount == values.count()) { + for (int i = 0; i < paramCount; ++i) { + res = SQLITE_OK; + const QVariant value = values.at(i); + + if (value.isNull()) { + res = sqlite3_bind_null(d->stmt, i + 1); + } else { + switch (value.type()) { + case QVariant::ByteArray: { + const auto ba = static_cast(value.constData()); + res = sqlite3_bind_blob(d->stmt, i + 1, ba->constData(), ba->size(), SQLITE_STATIC); + break; + } + case QVariant::Int: + case QVariant::Bool: + res = sqlite3_bind_int(d->stmt, i + 1, value.toInt()); + break; + case QVariant::Double: + res = sqlite3_bind_double(d->stmt, i + 1, value.toDouble()); + break; + case QVariant::UInt: + case QVariant::LongLong: + res = sqlite3_bind_int64(d->stmt, i + 1, value.toLongLong()); + break; + case QVariant::DateTime: { + const QDateTime dateTime = value.toDateTime(); + const QString str = dateTime.toString(QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz")); + res = sqlite3_bind_text16(d->stmt, i + 1, str.utf16(), str.size() * sizeof(ushort), SQLITE_TRANSIENT); + break; + } + case QVariant::Time: { + const QTime time = value.toTime(); + const QString str = time.toString(QStringLiteral("hh:mm:ss.zzz")); + res = sqlite3_bind_text16(d->stmt, i + 1, str.utf16(), str.size() * sizeof(ushort), SQLITE_TRANSIENT); + break; + } + case QVariant::String: { + // lifetime of string == lifetime of its qvariant + const auto str = static_cast(value.constData()); + res = sqlite3_bind_text16(d->stmt, i + 1, str->utf16(), (str->size()) * sizeof(QChar), SQLITE_STATIC); + break; + } + default: { + QString str = value.toString(); + // SQLITE_TRANSIENT makes sure that sqlite buffers the data + res = sqlite3_bind_text16(d->stmt, i + 1, str.utf16(), (str.size()) * sizeof(QChar), SQLITE_TRANSIENT); + break; + } + } + } + if (res != SQLITE_OK) { + setLastError(qMakeError(d->access, QCoreApplication::translate("QSQLiteResult", "Unable to bind parameters"), QSqlError::StatementError, res)); + d->finalize(); + return false; + } + } + } else { + setLastError(QSqlError(QCoreApplication::translate("QSQLiteResult", "Parameter count mismatch"), QString(), QSqlError::StatementError)); + return false; + } + d->skippedStatus = d->fetchNext(d->firstRow, 0, true); + if (lastError().isValid()) { + setSelect(false); + setActive(false); + return false; + } + setSelect(!d->rInf.isEmpty()); + setActive(true); + return true; +} + +bool QSQLiteResult::gotoNext(QSqlCachedResult::ValueCache &row, int idx) +{ + return d_func()->fetchNext(row, idx, false); +} + +int QSQLiteResult::size() +{ + return -1; +} + +int QSQLiteResult::numRowsAffected() +{ + return sqlite3_changes(d_func()->access); +} + +QVariant QSQLiteResult::lastInsertId() const +{ + if (isActive()) { + qint64 id = sqlite3_last_insert_rowid(d_func()->access); + if (id) { + return id; + } + } + return QVariant(); +} + +QSqlRecord QSQLiteResult::record() const +{ + if (!isActive() || !isSelect()) { + return QSqlRecord(); + } + return d_func()->rInf; +} + +void QSQLiteResult::detachFromResultSet() +{ + if (d_func()->stmt) { + sqlite3_reset(d_func()->stmt); + } +} + +QVariant QSQLiteResult::handle() const +{ + return QVariant::fromValue(d_func()->stmt); +} + +///////////////////////////////////////////////////////// + +QSQLiteDriver::QSQLiteDriver(QObject *parent) + : QSqlDriver(*new QSQLiteDriverPrivate, parent) +{ +} + +QSQLiteDriver::QSQLiteDriver(sqlite3 *connection, QObject *parent) + : QSqlDriver(*new QSQLiteDriverPrivate, parent) +{ + Q_D(QSQLiteDriver); + + d->access = connection; + setOpen(true); + setOpenError(false); +} + +QSQLiteDriver::~QSQLiteDriver() +{ +} + +bool QSQLiteDriver::hasFeature(DriverFeature f) const +{ + switch (f) { + case BLOB: + case Transactions: + case Unicode: + case LastInsertId: + case PreparedQueries: + case PositionalPlaceholders: + case SimpleLocking: + case FinishQuery: + case LowPrecisionNumbers: + return true; + case QuerySize: + case NamedPlaceholders: + case BatchOperations: + case EventNotifications: + case MultipleResultSets: + case CancelQuery: + return false; + } + return false; +} + +/* + SQLite dbs have no user name, passwords, hosts or ports. + just file names. +*/ +bool QSQLiteDriver::open(const QString &db, const QString &, const QString &, const QString &, int, const QString &conOpts) +{ + Q_D(QSQLiteDriver); + + if (isOpen()) { + close(); + } + + int timeout = 5000; + bool sharedCache = false; + bool openReadOnlyOption = false; + bool openUriOption = false; + + const QStringList opts = QString(conOpts).remove(QLatin1Char(' ')).split(QLatin1Char(';')); + for (const QString &option : opts) { + if (option.startsWith(QLatin1String("QSQLITE_BUSY_TIMEOUT="))) { + bool ok; + const int nt = option.midRef(21).toInt(&ok); + if (ok) { + timeout = nt; + } + } else if (option == QLatin1String("QSQLITE_OPEN_READONLY")) { + openReadOnlyOption = true; + } else if (option == QLatin1String("QSQLITE_OPEN_URI")) { + openUriOption = true; + } else if (option == QLatin1String("QSQLITE_ENABLE_SHARED_CACHE")) { + sharedCache = true; + } + } + + int openMode = (openReadOnlyOption ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)); + if (openUriOption) { + openMode |= SQLITE_OPEN_URI; + } + + sqlite3_enable_shared_cache(sharedCache); + + if (sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, nullptr) == SQLITE_OK) { + sqlite3_busy_timeout(d->access, timeout); + sqlite3_extended_result_codes(d->access, 1); + setOpen(true); + setOpenError(false); + return true; + } else { + if (d->access) { + sqlite3_close(d->access); + d->access = nullptr; + } + + setLastError(qMakeError(d->access, tr("Error opening database"), QSqlError::ConnectionError)); + setOpenError(true); + return false; + } +} + +void QSQLiteDriver::close() +{ + Q_D(QSQLiteDriver); + + if (isOpen()) { + Q_FOREACH (QSQLiteResult *result, d->results) { + result->d_func()->finalize(); + } + + if (sqlite3_close(d->access) != SQLITE_OK) + setLastError(qMakeError(d->access, tr("Error closing database"), QSqlError::ConnectionError)); + d->access = nullptr; + setOpen(false); + setOpenError(false); + } +} + +QSqlResult *QSQLiteDriver::createResult() const +{ + return new QSQLiteResult(this); +} + +bool QSQLiteDriver::beginTransaction() +{ + if (!isOpen() || isOpenError()) { + return false; + } + + QSqlQuery q(createResult()); + if (!q.exec(QStringLiteral("BEGIN"))) { + setLastError(QSqlError(tr("Unable to begin transaction"), q.lastError().databaseText(), QSqlError::TransactionError)); + return false; + } + + return true; +} + +bool QSQLiteDriver::commitTransaction() +{ + if (!isOpen() || isOpenError()) { + return false; + } + + QSqlQuery q(createResult()); + if (!q.exec(QStringLiteral("COMMIT"))) { + setLastError(QSqlError(tr("Unable to commit transaction"), q.lastError().databaseText(), QSqlError::TransactionError)); + return false; + } + + return true; +} + +bool QSQLiteDriver::rollbackTransaction() +{ + if (!isOpen() || isOpenError()) { + return false; + } + + QSqlQuery q(createResult()); + if (!q.exec(QStringLiteral("ROLLBACK"))) { + setLastError(QSqlError(tr("Unable to rollback transaction"), q.lastError().databaseText(), QSqlError::TransactionError)); + return false; + } + + return true; +} + +QStringList QSQLiteDriver::tables(QSql::TableType type) const +{ + QStringList res; + if (!isOpen()) { + return res; + } + + QSqlQuery q(createResult()); + q.setForwardOnly(true); + + QString sql = QStringLiteral( + "SELECT name FROM sqlite_master WHERE %1 " + "UNION ALL SELECT name FROM sqlite_temp_master WHERE %1"); + if ((type & QSql::Tables) && (type & QSql::Views)) { + sql = sql.arg(QStringLiteral("type='table' OR type='view'")); + } else if (type & QSql::Tables) { + sql = sql.arg(QStringLiteral("type='table'")); + } else if (type & QSql::Views) { + sql = sql.arg(QStringLiteral("type='view'")); + } else { + sql.clear(); + } + + if (!sql.isEmpty() && q.exec(sql)) { + while (q.next()) { + res.append(q.value(0).toString()); + } + } + + if (type & QSql::SystemTables) { + // there are no internal tables beside this one: + res.append(QStringLiteral("sqlite_master")); + } + + return res; +} + +static QSqlIndex qGetTableInfo(QSqlQuery &q, const QString &tableName, bool onlyPIndex = false) +{ + QString schema; + QString table(tableName); + int indexOfSeparator = tableName.indexOf(QLatin1Char('.')); + if (indexOfSeparator > -1) { + schema = tableName.left(indexOfSeparator).append(QLatin1Char('.')); + table = tableName.mid(indexOfSeparator + 1); + } + q.exec(QStringLiteral("PRAGMA ") + schema + QStringLiteral("table_info (") + _q_escapeIdentifier(table) + QLatin1Char(')')); + + QSqlIndex ind; + while (q.next()) { + bool isPk = q.value(5).toInt(); + if (onlyPIndex && !isPk) { + continue; + } + QString typeName = q.value(2).toString().toLower(); + QSqlField fld(q.value(1).toString(), qGetColumnType(typeName)); + if (isPk && (typeName == QLatin1String("integer"))) + // INTEGER PRIMARY KEY fields are auto-generated in sqlite + // INT PRIMARY KEY is not the same as INTEGER PRIMARY KEY! + { + fld.setAutoValue(true); + } + fld.setRequired(q.value(3).toInt() != 0); + fld.setDefaultValue(q.value(4)); + ind.append(fld); + } + return ind; +} + +QSqlIndex QSQLiteDriver::primaryIndex(const QString &tblname) const +{ + if (!isOpen()) { + return QSqlIndex(); + } + + QString table = tblname; + if (isIdentifierEscaped(table, QSqlDriver::TableName)) { + table = stripDelimiters(table, QSqlDriver::TableName); + } + + QSqlQuery q(createResult()); + q.setForwardOnly(true); + return qGetTableInfo(q, table, true); +} + +QSqlRecord QSQLiteDriver::record(const QString &tbl) const +{ + if (!isOpen()) { + return QSqlRecord(); + } + + QString table = tbl; + if (isIdentifierEscaped(table, QSqlDriver::TableName)) { + table = stripDelimiters(table, QSqlDriver::TableName); + } + + QSqlQuery q(createResult()); + q.setForwardOnly(true); + return qGetTableInfo(q, table); +} + +QVariant QSQLiteDriver::handle() const +{ + Q_D(const QSQLiteDriver); + return QVariant::fromValue(d->access); +} + +QString QSQLiteDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const +{ + Q_UNUSED(type) + return _q_escapeIdentifier(identifier); +} + +QT_END_NAMESPACE diff --git a/src/qsqlite/src/qsql_sqlite.h b/src/qsqlite/src/qsql_sqlite.h new file mode 100644 index 0000000..1e7049e --- /dev/null +++ b/src/qsqlite/src/qsql_sqlite.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** SPDX-FileCopyrightText: 2009 Nokia Corporation and /or its subsidiary(-ies). +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtSql module of the Qt Toolkit. +** +** SPDX-FileCopyrightText: LGPL-2.1-only WITH Qt-LGPL-exception-1.1 OR GPL-3.0-only +** +****************************************************************************/ + +#pragma once + +#include +#include +#include + +struct sqlite3; + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE +class QSQLiteDriverPrivate; +class QSQLiteResultPrivate; +class QSQLiteDriver; + +class QSQLiteDriver : public QSqlDriver +{ + Q_OBJECT + friend class QSQLiteResult; + +public: + explicit QSQLiteDriver(QObject *parent = nullptr); + explicit QSQLiteDriver(sqlite3 *connection, QObject *parent = nullptr); + ~QSQLiteDriver() override; + bool hasFeature(DriverFeature f) const override; + bool open(const QString &db, const QString &user, const QString &password, const QString &host, int port, const QString &connOpts) override; + void close() override; + QSqlResult *createResult() const override; + bool beginTransaction() override; + bool commitTransaction() override; + bool rollbackTransaction() override; + QStringList tables(QSql::TableType) const override; + + QSqlRecord record(const QString &tablename) const override; + QSqlIndex primaryIndex(const QString &table) const override; + QVariant handle() const override; + QString escapeIdentifier(const QString &identifier, IdentifierType) const override; + +private: + Q_DECLARE_PRIVATE(QSQLiteDriver) +}; + +QT_END_NAMESPACE + +QT_END_HEADER + diff --git a/src/qsqlite/src/smain.cpp b/src/qsqlite/src/smain.cpp new file mode 100644 index 0000000..f2588b6 --- /dev/null +++ b/src/qsqlite/src/smain.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** SPDX-FileCopyrightText: 2009 Nokia Corporation and /or its subsidiary(-ies). +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the plugins of the Qt Toolkit. +** +** SPDX-FileCopyrightText: LGPL-2.1-only WITH Qt-LGPL-exception-1.1 OR GPL-3.0-only +** +****************************************************************************/ + +#include +#include + +#include "qsql_sqlite.h" + +QT_BEGIN_NAMESPACE + +class QSQLiteDriverPlugin : public QSqlDriverPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QSqlDriverFactoryInterface" FILE "sqlite3.json") + +public: + QSQLiteDriverPlugin(); + + QSqlDriver *create(const QString &) override; +}; + +QSQLiteDriverPlugin::QSQLiteDriverPlugin() + : QSqlDriverPlugin() +{ +} + +QSqlDriver *QSQLiteDriverPlugin::create(const QString &name) +{ + if (name == QLatin1String("QSQLITE3")) { + auto driver = new QSQLiteDriver(); + return driver; + } + return nullptr; +} + +#include "smain.moc" + +QT_END_NAMESPACE diff --git a/src/qsqlite/src/sqlite3.json b/src/qsqlite/src/sqlite3.json new file mode 100644 index 0000000..b8fdc6b --- /dev/null +++ b/src/qsqlite/src/sqlite3.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "QSQLITE3" ] +} diff --git a/src/qsqlite/src/sqlite_blocking.cpp b/src/qsqlite/src/sqlite_blocking.cpp new file mode 100644 index 0000000..01417fc --- /dev/null +++ b/src/qsqlite/src/sqlite_blocking.cpp @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2009 Bertjan Broeksema + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "sqlite_blocking.h" + +#include + +#include "qdebug.h" +#include +#include +#include +#include + +QString debugString() +{ + return QString(QLatin1String("[QSQLITE3: ") + QString::number(quint64(QThread::currentThreadId())) + QLatin1String("] ")); +} + +/* Based on example in http://www.sqlite.org/unlock_notify.html */ + +struct UnlockNotification { + bool fired; + QWaitCondition cond; + QMutex mutex; +}; + +static void qSqlite3UnlockNotifyCb(void **apArg, int nArg) +{ + for (int i = 0; i < nArg; ++i) { + auto ntf = static_cast(apArg[i]); + ntf->mutex.lock(); + ntf->fired = true; + ntf->cond.wakeOne(); + ntf->mutex.unlock(); + } +} + +static int qSqlite3WaitForUnlockNotify(sqlite3 *db) +{ + int rc; + UnlockNotification un; + un.fired = false; + + rc = sqlite3_unlock_notify(db, qSqlite3UnlockNotifyCb, (void *)&un); + Q_ASSERT(rc == SQLITE_LOCKED || rc == SQLITE_OK); + + if (rc == SQLITE_OK) { + un.mutex.lock(); + if (!un.fired) { + un.cond.wait(&un.mutex); + } + un.mutex.unlock(); + } + + return rc; +} + +int sqlite3_blocking_step(sqlite3_stmt *pStmt) +{ + int rc; + while (SQLITE_LOCKED_SHAREDCACHE == (rc = sqlite3_step(pStmt))) { + // qDebug() << debugString() << "sqlite3_blocking_step: Waiting..."; QTime now; now.start(); + rc = qSqlite3WaitForUnlockNotify(sqlite3_db_handle(pStmt)); + // qDebug() << debugString() << "sqlite3_blocking_step: Waited for " << now.elapsed() << "ms"; + if (rc != SQLITE_OK) { + break; + } + sqlite3_reset(pStmt); + } + + return rc; +} + +int sqlite3_blocking_prepare16_v2(sqlite3 *db, const void *zSql, int nSql, sqlite3_stmt **ppStmt, const void **pzTail) +{ + int rc; + while (SQLITE_LOCKED_SHAREDCACHE == (rc = sqlite3_prepare16_v2(db, zSql, nSql, ppStmt, pzTail))) { + // qDebug() << debugString() << "sqlite3_blocking_prepare16_v2: Waiting..."; QTime now; now.start(); + rc = qSqlite3WaitForUnlockNotify(db); + // qDebug() << debugString() << "sqlite3_blocking_prepare16_v2: Waited for " << now.elapsed() << "ms"; + if (rc != SQLITE_OK) { + break; + } + } + + return rc; +} diff --git a/src/qsqlite/src/sqlite_blocking.h b/src/qsqlite/src/sqlite_blocking.h new file mode 100644 index 0000000..a43caf7 --- /dev/null +++ b/src/qsqlite/src/sqlite_blocking.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2009 Bertjan Broeksema + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +QString debugString(); + +struct sqlite3; +struct sqlite3_stmt; + +int sqlite3_blocking_prepare16_v2(sqlite3 *db, /* Database handle. */ + const void *zSql, /* SQL statement, UTF-16 encoded */ + int nSql, /* Length of zSql in bytes. */ + sqlite3_stmt **ppStmt, /* OUT: A pointer to the prepared statement */ + const void **pzTail /* OUT: Pointer to unused portion of zSql */); + +int sqlite3_blocking_step(sqlite3_stmt *pStmt); + diff --git a/src/rds/CMakeLists.txt b/src/rds/CMakeLists.txt new file mode 100644 index 0000000..0fd45de --- /dev/null +++ b/src/rds/CMakeLists.txt @@ -0,0 +1,22 @@ +########### next target ############### + +add_executable(akonadi_rds) +target_sources(akonadi_rds PRIVATE + bridgeserver.cpp + bridgeconnection.cpp + main.cpp +) + +set_target_properties(akonadi_rds PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(akonadi_rds + akonadi_shared + KF5AkonadiPrivate + Qt::Core + Qt::Network +) + +install(TARGETS akonadi_rds + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + diff --git a/src/rds/bridgeconnection.cpp b/src/rds/bridgeconnection.cpp new file mode 100644 index 0000000..6f1deb7 --- /dev/null +++ b/src/rds/bridgeconnection.cpp @@ -0,0 +1,111 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "bridgeconnection.h" + +#include + +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_UNIX +#include +#include +#endif + +BridgeConnection::BridgeConnection(QTcpSocket *remoteSocket, QObject *parent) + : QObject(parent) + , m_remoteSocket(remoteSocket) +{ + // wait for the vtable to be complete + QMetaObject::invokeMethod(this, &BridgeConnection::doConnects, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &BridgeConnection::connectLocal, Qt::QueuedConnection); +} + +BridgeConnection::~BridgeConnection() +{ + delete m_remoteSocket; +} + +void BridgeConnection::slotDataAvailable() +{ + if (m_localSocket->bytesAvailable() > 0) { + m_remoteSocket->write(m_localSocket->read(m_localSocket->bytesAvailable())); + } + if (m_remoteSocket->bytesAvailable() > 0) { + m_localSocket->write(m_remoteSocket->read(m_remoteSocket->bytesAvailable())); + } +} + +AkonadiBridgeConnection::AkonadiBridgeConnection(QTcpSocket *remoteSocket, QObject *parent) + : BridgeConnection(remoteSocket, parent) +{ + m_localSocket = new QLocalSocket(this); +} + +void AkonadiBridgeConnection::connectLocal() +{ + const QSettings connectionSettings(Akonadi::StandardDirs::connectionConfigFile(), QSettings::IniFormat); +#ifdef Q_OS_WIN // krazy:exclude=cpp + const QString namedPipe = connectionSettings.value(QLatin1String("Data/NamedPipe"), QLatin1String("Akonadi")).toString(); + (static_cast(m_localSocket))->connectToServer(namedPipe); +#else + const QString defaultSocketDir = Akonadi::StandardDirs::saveDir("data"); + const QString path = + connectionSettings.value(QStringLiteral("Data/UnixPath"), QString(defaultSocketDir + QLatin1String("/akonadiserver.socket"))).toString(); + (static_cast(m_localSocket))->connectToServer(path); +#endif +} + +DBusBridgeConnection::DBusBridgeConnection(QTcpSocket *remoteSocket, QObject *parent) + : BridgeConnection(remoteSocket, parent) +{ + m_localSocket = new QLocalSocket(this); +} + +void DBusBridgeConnection::connectLocal() +{ + // TODO: support for !Linux +#ifdef Q_OS_UNIX + const QByteArray sessionBusAddress = qgetenv("DBUS_SESSION_BUS_ADDRESS"); + const QRegularExpression rx(QStringLiteral("=(.*)[,$]")); + QRegularExpressionMatch match = rx.match(QString::fromLatin1(sessionBusAddress)); + if (match.hasMatch()) { + const QString dbusPath = match.captured(1); + qDebug() << dbusPath; + if (sessionBusAddress.contains("abstract")) { + const int fd = socket(PF_UNIX, SOCK_STREAM, 0); + Q_ASSERT(fd >= 0); + struct sockaddr_un dbus_socket_addr; + dbus_socket_addr.sun_family = PF_UNIX; + dbus_socket_addr.sun_path[0] = '\0'; // this marks an abstract unix socket on linux, something QLocalSocket doesn't support + memcpy(dbus_socket_addr.sun_path + 1, dbusPath.toLatin1().data(), dbusPath.toLatin1().size() + 1); + /*sizeof(dbus_socket_addr) gives me a too large value for some reason, although that's what QLocalSocket uses*/ + const int result = ::connect(fd, + reinterpret_cast(&dbus_socket_addr), + sizeof(dbus_socket_addr.sun_family) + dbusPath.size() + 1 /* for the leading \0 */); + Q_ASSERT(result != -1); + Q_UNUSED(result) // in release mode + (static_cast(m_localSocket))->setSocketDescriptor(fd, QLocalSocket::ConnectedState, QLocalSocket::ReadWrite); + } else { + (static_cast(m_localSocket))->connectToServer(dbusPath); + } + } +#endif +} + +void BridgeConnection::doConnects() +{ + connect(m_localSocket, &QLocalSocket::disconnected, this, &BridgeConnection::deleteLater); + connect(m_remoteSocket, &QAbstractSocket::disconnected, this, &QObject::deleteLater); + connect(m_localSocket, &QIODevice::readyRead, this, &BridgeConnection::slotDataAvailable); + connect(m_remoteSocket, &QIODevice::readyRead, this, &BridgeConnection::slotDataAvailable); + connect(m_localSocket, &QLocalSocket::connected, this, &BridgeConnection::slotDataAvailable); +} diff --git a/src/rds/bridgeconnection.h b/src/rds/bridgeconnection.h new file mode 100644 index 0000000..9f85d5d --- /dev/null +++ b/src/rds/bridgeconnection.h @@ -0,0 +1,57 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include + +class QTcpSocket; +class QLocalSocket; + +class BridgeConnection : public QObject +{ + Q_OBJECT + +public: + explicit BridgeConnection(QTcpSocket *remoteSocket, QObject *parent = nullptr); + ~BridgeConnection(); + +protected Q_SLOTS: + virtual void connectLocal() = 0; + void doConnects(); + +protected: + QLocalSocket *m_localSocket = nullptr; + +private Q_SLOTS: + void slotDataAvailable(); + +private: + QTcpSocket *const m_remoteSocket; +}; + +class AkonadiBridgeConnection : public BridgeConnection +{ + Q_OBJECT + +public: + explicit AkonadiBridgeConnection(QTcpSocket *remoteSocket, QObject *parent = nullptr); + +protected: + void connectLocal() override; +}; + +class DBusBridgeConnection : public BridgeConnection +{ + Q_OBJECT + +public: + explicit DBusBridgeConnection(QTcpSocket *remoteSocket, QObject *parent = nullptr); + +protected: + void connectLocal() override; +}; + diff --git a/src/rds/bridgeserver.cpp b/src/rds/bridgeserver.cpp new file mode 100644 index 0000000..9da59f7 --- /dev/null +++ b/src/rds/bridgeserver.cpp @@ -0,0 +1,19 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "bridgeserver.h" + +#include "exception.h" + +BridgeServerBase::BridgeServerBase(quint16 port, QObject *parent) + : QObject(parent) + , m_server(new QTcpServer(this)) +{ + connect(m_server, &QTcpServer::newConnection, this, &BridgeServerBase::slotNewConnection); + if (!m_server->listen(QHostAddress::Any, port)) { + throw Exception(QStringLiteral("Can't listen to port %1: %2").arg(port).arg(m_server->errorString())); + } +} diff --git a/src/rds/bridgeserver.h b/src/rds/bridgeserver.h new file mode 100644 index 0000000..236c4b4 --- /dev/null +++ b/src/rds/bridgeserver.h @@ -0,0 +1,42 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +class BridgeServerBase : public QObject +{ + Q_OBJECT + +public: + explicit BridgeServerBase(quint16 port, QObject *parent = nullptr); + +protected Q_SLOTS: + virtual void slotNewConnection() = 0; + +protected: + QTcpServer *const m_server; +}; + +template class BridgeServer : public BridgeServerBase +{ +public: + explicit BridgeServer(quint16 port, QObject *parent = nullptr) + : BridgeServerBase(port, parent) + { + } + +protected: + void slotNewConnection() override + { + while (m_server->hasPendingConnections()) { + new ConnectionType(m_server->nextPendingConnection(), this); + } + } +}; + diff --git a/src/rds/exception.h b/src/rds/exception.h new file mode 100644 index 0000000..d28114c --- /dev/null +++ b/src/rds/exception.h @@ -0,0 +1,23 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2010 Marc Mutz * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +#include +#include + +template class Exception : Ex +{ +public: + explicit Exception(const QString &message) + : Ex(message.toStdString()) + { + } + + ~Exception() throw() + { + } +}; + diff --git a/src/rds/main.cpp b/src/rds/main.cpp new file mode 100644 index 0000000..b32ada1 --- /dev/null +++ b/src/rds/main.cpp @@ -0,0 +1,30 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "bridgeconnection.h" +#include "bridgeserver.h" + +#include + +#include + +int main(int argc, char **argv) +{ + AkCoreApplication app(argc, argv); + app.setDescription(QStringLiteral("Akonadi Remote Debugging Server\nUse for debugging only.")); + app.parseCommandLine(); + try { + new BridgeServer(31415); + new BridgeServer(31416); + return app.exec(); + } catch (const std::exception &e) { + qDebug("Caught exception: %s", e.what()); + return EXIT_FAILURE; + } catch (...) { + qDebug("Caught unknown exception - fix the program!"); + return EXIT_FAILURE; + } +} diff --git a/src/selftest/CMakeLists.txt b/src/selftest/CMakeLists.txt new file mode 100644 index 0000000..91cfaf9 --- /dev/null +++ b/src/selftest/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(akonadiselftest main.cpp) + +target_link_libraries(akonadiselftest +PRIVATE + KF5::AkonadiWidgets + KF5::AkonadiPrivate + KF5::I18n + Qt::Sql + Qt::Widgets +) + +install(TARGETS + akonadiselftest + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) diff --git a/src/selftest/main.cpp b/src/selftest/main.cpp new file mode 100644 index 0000000..8902106 --- /dev/null +++ b/src/selftest/main.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "selftestdialog.h" + +#include + +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + QCommandLineParser parser; + KAboutData about(QStringLiteral("akonadiselftest"), + i18n("Akonadi Self Test"), + QStringLiteral("1.0"), + i18n("Checks and reports state of Akonadi server"), + KAboutLicense::GPL_V2, + i18n("(c) 2008 Volker Krause ")); + about.setupCommandLine(&parser); + + QCoreApplication::setApplicationName(QStringLiteral("akonadiselftest")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + parser.process(app); + + Akonadi::SelfTestDialog dlg; + dlg.show(); + + return app.exec(); +} diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt new file mode 100644 index 0000000..b93dc6c --- /dev/null +++ b/src/server/CMakeLists.txt @@ -0,0 +1,213 @@ +########### next target ############### +set(AKONADI_DB_SCHEMA "${CMAKE_CURRENT_SOURCE_DIR}/storage/akonadidb.xml") + +akonadi_run_xsltproc( + XSL ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities.xsl + XML ${AKONADI_DB_SCHEMA} + BASENAME entities + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities-source.xsl ${CMAKE_CURRENT_SOURCE_DIR}/storage/entities-header.xsl +) + +akonadi_run_xsltproc( + XSL ${Akonadi_SOURCE_DIR}/src/server/storage/schema.xsl + XML ${AKONADI_DB_SCHEMA} + CLASSNAME AkonadiSchema + BASENAME akonadischema + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/storage/schema-source.xsl ${CMAKE_CURRENT_SOURCE_DIR}/storage/schema-header.xsl +) + +akonadi_add_xmllint_test( + akonadidb-xmllint + XSD ${CMAKE_CURRENT_SOURCE_DIR}/storage/akonadidb.xsd + XML ${AKONADI_DB_SCHEMA} +) +akonadi_add_xmllint_test( + akonadidbupdate-xmllint + XSD ${CMAKE_CURRENT_SOURCE_DIR}/storage/akonadidb.xsd + XML ${AKONADI_DB_SCHEMA} +) + +set(libakonadiserver_SRCS + akonadi.cpp + aggregatedfetchscope.cpp + aklocalserver.cpp + akthread.cpp + commandcontext.cpp + connection.cpp + collectionscheduler.cpp + handler.cpp + handlerhelper.cpp + intervalcheck.cpp + handler/collectioncopyhandler.cpp + handler/collectioncreatehandler.cpp + handler/collectiondeletehandler.cpp + handler/collectionfetchhandler.cpp + handler/collectionmodifyhandler.cpp + handler/collectionmovehandler.cpp + handler/collectionstatsfetchhandler.cpp + handler/itemcopyhandler.cpp + handler/itemcreatehandler.cpp + handler/itemdeletehandler.cpp + handler/itemfetchhandler.cpp + handler/itemfetchhelper.cpp + handler/itemlinkhandler.cpp + handler/itemmodifyhandler.cpp + handler/itemmovehandler.cpp + handler/loginhandler.cpp + handler/logouthandler.cpp + handler/relationfetchhandler.cpp + handler/relationmodifyhandler.cpp + handler/relationremovehandler.cpp + handler/resourceselecthandler.cpp + handler/searchhandler.cpp + handler/searchhelper.cpp + handler/searchcreatehandler.cpp + handler/searchresulthandler.cpp + handler/tagcreatehandler.cpp + handler/tagdeletehandler.cpp + handler/tagfetchhandler.cpp + handler/tagfetchhelper.cpp + handler/tagmodifyhandler.cpp + handler/transactionhandler.cpp + search/agentsearchengine.cpp + search/agentsearchinstance.cpp + search/searchtaskmanager.cpp + search/searchrequest.cpp + search/searchmanager.cpp + + storage/collectionqueryhelper.cpp + storage/collectionstatistics.cpp + storage/entity.cpp + ${CMAKE_CURRENT_BINARY_DIR}/entities.cpp + ${CMAKE_CURRENT_BINARY_DIR}/akonadischema.cpp + storage/datastore.cpp + storage/dbconfig.cpp + storage/dbconfigmysql.cpp + storage/dbconfigpostgresql.cpp + storage/dbconfigsqlite.cpp + storage/dbexception.cpp + storage/dbinitializer.cpp + storage/dbinitializer_p.cpp + storage/dbintrospector.cpp + storage/dbintrospector_impl.cpp + storage/dbupdater.cpp + storage/dbtype.cpp + storage/itemqueryhelper.cpp + storage/itemretriever.cpp + storage/itemretrievalmanager.cpp + storage/itemretrievaljob.cpp + storage/itemretrievalrequest.cpp + storage/notificationcollector.cpp + storage/parthelper.cpp + storage/parttypehelper.cpp + storage/query.cpp + storage/querybuilder.cpp + storage/querycache.cpp + storage/queryhelper.cpp + storage/schematypes.cpp + storage/tagqueryhelper.cpp + storage/transaction.cpp + storage/parthelper.cpp + storage/partstreamer.cpp + storage/storagedebugger.cpp + tracer.cpp + utils.cpp + dbustracer.cpp + filetracer.cpp + notificationmanager.cpp + notificationsubscriber.cpp + resourcemanager.cpp + cachecleaner.cpp + debuginterface.cpp + preprocessorinstance.cpp + preprocessormanager.cpp + storagejanitor.cpp + storage/akonadidb.qrc +) + +set(akonadiserver_SRCS + main.cpp +) +ecm_qt_declare_logging_category(akonadiserver_SRCS HEADER akonadiserver_debug.h IDENTIFIER AKONADISERVER_LOG CATEGORY_NAME org.kde.pim.akonadiserver + DESCRIPTION "akonadi (Akonadi Server)" + OLD_CATEGORY_NAMES log_akonadiserver + EXPORT AKONADI + ) + +ecm_qt_declare_logging_category(akonadiserver_SRCS HEADER akonadiserver_search_debug.h IDENTIFIER AKONADISERVER_SEARCH_LOG CATEGORY_NAME org.kde.pim.akonadiserver.search + DESCRIPTION "akonadi (Akonadi Server Search Functionality)" + EXPORT AKONADI + ) + +qt_generate_dbus_interface(debuginterface.h org.freedesktop.Akonadi.DebugInterface.xml) + +qt_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.TracerNotification.xml dbustracer.h Akonadi::Server::DBusTracer) +qt_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Tracer.xml tracer.h Akonadi::Server::Tracer) +qt_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Server.xml akonadi.h Akonadi::Server::AkonadiServer) +qt_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.StorageDebugger.xml storage/storagedebugger.h Akonadi::Server::StorageDebugger) +qt_add_dbus_adaptor(libakonadiserver_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.DebugInterface.xml debuginterface.h Akonadi::Server::DebugInterface) +qt_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.ResourceManager.xml resourcemanager.h Akonadi::Server::ResourceManager) +qt_add_dbus_adaptor(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.PreprocessorManager.xml preprocessormanager.h Akonadi::Server::PreprocessorManager) +qt_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.AgentManager.xml agentmanagerinterface) +qt_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Resource.xml resourceinterface) +qt_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Preprocessor.xml preprocessorinterface) +qt_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Control.xml agentcontrolinterface) +qt_add_dbus_interface(libakonadiserver_SRCS ${Akonadi_SOURCE_DIR}/src/interfaces/org.freedesktop.Akonadi.Agent.Search.xml agentsearchinterface) + +add_library(libakonadiserver STATIC ${libakonadiserver_SRCS}) +#if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) +# set_target_properties(libakonadiserver PROPERTIES UNITY_BUILD ON) +# set_source_files_properties(storage/itemretrievalmanager.cpp PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON) +#endif() + +set_target_properties(libakonadiserver PROPERTIES OUTPUT_NAME akonadiserver) + +if(MYSQLD_EXECUTABLE) + target_compile_definitions(libakonadiserver PRIVATE MYSQLD_EXECUTABLE="${MYSQLD_EXECUTABLE}") +endif() + +if(MYSQLD_SCRIPTS_PATH) + target_compile_definitions(libakonadiserver PRIVATE MYSQLD_SCRIPTS_PATH="${MYSQLD_SCRIPTS_PATH}") +endif() + +if(POSTGRES_PATH) + target_compile_definitions(libakonadiserver PRIVATE POSTGRES_PATH="${POSTGRES_PATH}") +endif() + +target_link_libraries(libakonadiserver + akonadi_shared + KF5AkonadiPrivate + Qt::Core + Qt::Network + Qt::Sql + Qt::DBus + Qt::Xml +) + +add_executable(akonadiserver ${akonadiserver_SRCS}) +set_target_properties(akonadiserver PROPERTIES MACOSX_BUNDLE FALSE) +set_target_properties(akonadiserver PROPERTIES OUTPUT_NAME akonadiserver) +target_link_libraries(akonadiserver + libakonadiserver + KF5::CoreAddons +) + +install(TARGETS akonadiserver + ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + storage/mysql-global.conf + storage/mysql-global-mobile.conf + DESTINATION ${CONFIG_INSTALL_DIR}/akonadi +) + +install(FILES + search/abstractsearchplugin.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/akonadi +) + +## DBus XML files +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Akonadi.DebugInterface.xml + DESTINATION ${AKONADI_DBUS_INTERFACES_INSTALL_DIR} +) diff --git a/src/server/aggregatedfetchscope.cpp b/src/server/aggregatedfetchscope.cpp new file mode 100644 index 0000000..5460518 --- /dev/null +++ b/src/server/aggregatedfetchscope.cpp @@ -0,0 +1,492 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "aggregatedfetchscope.h" +#include + +#include +#include + +#define LOCKED_D(name) \ + Q_D(name); \ + QMutexLocker lock(&d->lock); + +namespace Akonadi +{ +namespace Server +{ +class AggregatedFetchScopePrivate +{ +public: + AggregatedFetchScopePrivate() + : lock() // recursive so that we can call our own getters/setters + { + } + + inline void addToSet(const QByteArray &value, QSet &set, QHash &count) + { + auto it = count.find(value); + if (it == count.end()) { + it = count.insert(value, 0); + set.insert(value); + } + ++(*it); + } + + inline void removeFromSet(const QByteArray &value, QSet &set, QHash &count) + { + auto it = count.find(value); + if (it == count.end()) { + return; + } + + if (--(*it) == 0) { + count.erase(it); + set.remove(value); + } + } + + inline void updateBool(bool newValue, int &store) + { + store += newValue ? 1 : -1; + } + + inline void applySet(const QSet &oldSet, const QSet &newSet, QSet &set, QHash &count) + { + const auto added = newSet - oldSet; + for (const auto &value : added) { + addToSet(value, set, count); + } + const auto removed = oldSet - newSet; + for (const auto &value : removed) { + removeFromSet(value, set, count); + } + } + +public: + mutable QRecursiveMutex lock; +}; + +class AggregatedCollectionFetchScopePrivate : public AggregatedFetchScopePrivate +{ +public: + QSet attrs; + QHash attrsCount; + int subscribers = 0; + int fetchIdOnly = 0; + int fetchStats = 0; +}; + +class AggregatedTagFetchScopePrivate : public AggregatedFetchScopePrivate +{ +public: + QSet attrs; + QHash attrsCount; + int subscribers = 0; + int fetchIdOnly = 0; + int fetchRemoteId = 0; + int doNotFetchAllAttributes = 0; +}; + +class AggregatedItemFetchScopePrivate : public AggregatedFetchScopePrivate +{ +public: + mutable Protocol::ItemFetchScope mCachedScope; + mutable bool mCachedScopeValid = false; // use std::optional for mCachedScope + + QSet parts; + QHash partsCount; + QSet tags; + QHash tagsCount; + int subscribers = 0; + int ancestors[3] = {0, 0, 0}; // 3 = size of AncestorDepth enum + int cacheOnly = 0; + int fullPayload = 0; + int allAttributes = 0; + int fetchSize = 0; + int fetchMTime = 0; + int fetchRRev = 0; + int ignoreErrors = 0; + int fetchFlags = 0; + int fetchRID = 0; + int fetchGID = 0; + int fetchTags = 0; + int fetchRelations = 0; + int fetchVRefs = 0; +}; + +} // namespace Server +} // namespace Akonadi + +using namespace Akonadi; +using namespace Akonadi::Protocol; +using namespace Akonadi::Server; + +AggregatedCollectionFetchScope::AggregatedCollectionFetchScope() + : d_ptr(new AggregatedCollectionFetchScopePrivate) +{ +} + +AggregatedCollectionFetchScope::~AggregatedCollectionFetchScope() +{ + delete d_ptr; +} + +void AggregatedCollectionFetchScope::apply(const Protocol::CollectionFetchScope &oldScope, const Protocol::CollectionFetchScope &newScope) +{ + LOCKED_D(AggregatedCollectionFetchScope) + + if (newScope.includeStatistics() != oldScope.includeStatistics()) { + d->updateBool(newScope.includeStatistics(), d->fetchStats); + } + if (newScope.fetchIdOnly() != oldScope.fetchIdOnly()) { + d->updateBool(newScope.fetchIdOnly(), d->fetchIdOnly); + } + if (newScope.attributes() != oldScope.attributes()) { + d->applySet(oldScope.attributes(), newScope.attributes(), d->attrs, d->attrsCount); + } +} + +QSet AggregatedCollectionFetchScope::attributes() const +{ + LOCKED_D(const AggregatedCollectionFetchScope) + return d->attrs; +} + +bool AggregatedCollectionFetchScope::fetchIdOnly() const +{ + LOCKED_D(const AggregatedCollectionFetchScope) + // Aggregation: we can return true only if everyone wants fetchIdOnly, + // otherwise there's at least one subscriber who wants everything + return d->fetchIdOnly == d->subscribers; +} + +bool AggregatedCollectionFetchScope::fetchStatistics() const +{ + LOCKED_D(const AggregatedCollectionFetchScope); + // Aggregation: return true if at least one subscriber wants stats + return d->fetchStats > 0; +} + +void AggregatedCollectionFetchScope::addSubscriber() +{ + LOCKED_D(AggregatedCollectionFetchScope) + ++d->subscribers; +} + +void AggregatedCollectionFetchScope::removeSubscriber() +{ + LOCKED_D(AggregatedCollectionFetchScope) + --d->subscribers; +} + +AggregatedItemFetchScope::AggregatedItemFetchScope() + : d_ptr(new AggregatedItemFetchScopePrivate) +{ +} + +AggregatedItemFetchScope::~AggregatedItemFetchScope() +{ + delete d_ptr; +} + +void AggregatedItemFetchScope::apply(const Protocol::ItemFetchScope &oldScope, const Protocol::ItemFetchScope &newScope) +{ + LOCKED_D(AggregatedItemFetchScope); + + const auto newParts = newScope.requestedParts() | AkRanges::Actions::toQSet; + const auto oldParts = oldScope.requestedParts() | AkRanges::Actions::toQSet; + if (newParts != oldParts) { + d->applySet(oldParts, newParts, d->parts, d->partsCount); + } + if (newScope.ancestorDepth() != oldScope.ancestorDepth()) { + updateAncestorDepth(oldScope.ancestorDepth(), newScope.ancestorDepth()); + } + if (newScope.cacheOnly() != oldScope.cacheOnly()) { + d->updateBool(newScope.cacheOnly(), d->cacheOnly); + } + if (newScope.fullPayload() != oldScope.fullPayload()) { + d->updateBool(newScope.fullPayload(), d->fullPayload); + } + if (newScope.allAttributes() != oldScope.allAttributes()) { + d->updateBool(newScope.allAttributes(), d->allAttributes); + } + if (newScope.fetchSize() != oldScope.fetchSize()) { + d->updateBool(newScope.fetchSize(), d->fetchSize); + } + if (newScope.fetchMTime() != oldScope.fetchMTime()) { + d->updateBool(newScope.fetchMTime(), d->fetchMTime); + } + if (newScope.fetchRemoteRevision() != oldScope.fetchRemoteRevision()) { + d->updateBool(newScope.fetchRemoteRevision(), d->fetchRRev); + } + if (newScope.ignoreErrors() != oldScope.ignoreErrors()) { + d->updateBool(newScope.ignoreErrors(), d->ignoreErrors); + } + if (newScope.fetchFlags() != oldScope.fetchFlags()) { + d->updateBool(newScope.fetchFlags(), d->fetchFlags); + } + if (newScope.fetchRemoteId() != oldScope.fetchRemoteId()) { + d->updateBool(newScope.fetchRemoteId(), d->fetchRID); + } + if (newScope.fetchGID() != oldScope.fetchGID()) { + d->updateBool(newScope.fetchGID(), d->fetchGID); + } + if (newScope.fetchTags() != oldScope.fetchTags()) { + d->updateBool(newScope.fetchTags(), d->fetchTags); + } + if (newScope.fetchRelations() != oldScope.fetchRelations()) { + d->updateBool(newScope.fetchRelations(), d->fetchRelations); + } + if (newScope.fetchVirtualReferences() != oldScope.fetchVirtualReferences()) { + d->updateBool(newScope.fetchVirtualReferences(), d->fetchVRefs); + } + + d->mCachedScopeValid = false; +} + +ItemFetchScope AggregatedItemFetchScope::toFetchScope() const +{ + LOCKED_D(const AggregatedItemFetchScope); + if (d->mCachedScopeValid) { + return d->mCachedScope; + } + + d->mCachedScope = ItemFetchScope(); + d->mCachedScope.setRequestedParts(d->parts | AkRanges::Actions::toQVector); + d->mCachedScope.setAncestorDepth(ancestorDepth()); + + d->mCachedScope.setFetch(ItemFetchScope::CacheOnly, cacheOnly()); + d->mCachedScope.setFetch(ItemFetchScope::FullPayload, fullPayload()); + d->mCachedScope.setFetch(ItemFetchScope::AllAttributes, allAttributes()); + d->mCachedScope.setFetch(ItemFetchScope::Size, fetchSize()); + d->mCachedScope.setFetch(ItemFetchScope::MTime, fetchMTime()); + d->mCachedScope.setFetch(ItemFetchScope::RemoteRevision, fetchRemoteRevision()); + d->mCachedScope.setFetch(ItemFetchScope::IgnoreErrors, ignoreErrors()); + d->mCachedScope.setFetch(ItemFetchScope::Flags, fetchFlags()); + d->mCachedScope.setFetch(ItemFetchScope::RemoteID, fetchRemoteId()); + d->mCachedScope.setFetch(ItemFetchScope::GID, fetchGID()); + d->mCachedScope.setFetch(ItemFetchScope::Tags, fetchTags()); + d->mCachedScope.setFetch(ItemFetchScope::Relations, fetchRelations()); + d->mCachedScope.setFetch(ItemFetchScope::VirtReferences, fetchVirtualReferences()); + d->mCachedScopeValid = true; + return d->mCachedScope; +} + +QSet AggregatedItemFetchScope::requestedParts() const +{ + LOCKED_D(const AggregatedItemFetchScope) + return d->parts; +} + +ItemFetchScope::AncestorDepth AggregatedItemFetchScope::ancestorDepth() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return the largest depth with at least one subscriber + if (d->ancestors[ItemFetchScope::AllAncestors] > 0) { + return ItemFetchScope::AllAncestors; + } else if (d->ancestors[ItemFetchScope::ParentAncestor] > 0) { + return ItemFetchScope::ParentAncestor; + } else { + return ItemFetchScope::NoAncestor; + } +} + +void AggregatedItemFetchScope::updateAncestorDepth(ItemFetchScope::AncestorDepth oldDepth, ItemFetchScope::AncestorDepth newDepth) +{ + LOCKED_D(AggregatedItemFetchScope) + if (d->ancestors[oldDepth] > 0) { + --d->ancestors[oldDepth]; + } + ++d->ancestors[newDepth]; +} + +bool AggregatedItemFetchScope::cacheOnly() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: we can return true only if everyone wants cached data only, + // otherwise there's at least one subscriber who wants uncached data + return d->cacheOnly == d->subscribers; +} + +bool AggregatedItemFetchScope::fullPayload() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants the + // full payload + return d->fullPayload > 0; +} + +bool AggregatedItemFetchScope::allAttributes() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants + // all attributes + return d->allAttributes > 0; +} + +bool AggregatedItemFetchScope::fetchSize() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants size + return d->fetchSize > 0; +} + +bool AggregatedItemFetchScope::fetchMTime() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants mtime + return d->fetchMTime > 0; +} + +bool AggregatedItemFetchScope::fetchRemoteRevision() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants rrev + return d->fetchRRev > 0; +} + +bool AggregatedItemFetchScope::ignoreErrors() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true only if everyone wants to ignore errors, otherwise + // there's at least one subscriber who does not want to ignore them + return d->ignoreErrors == d->subscribers; +} + +bool AggregatedItemFetchScope::fetchFlags() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants flags + return d->fetchFlags > 0; +} + +bool AggregatedItemFetchScope::fetchRemoteId() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants RID + return d->fetchRID > 0; +} + +bool AggregatedItemFetchScope::fetchGID() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants GID + return d->fetchGID > 0; +} + +bool AggregatedItemFetchScope::fetchTags() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants tags + return d->fetchTags > 0; +} + +bool AggregatedItemFetchScope::fetchRelations() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants relations + return d->fetchRelations > 0; +} + +bool AggregatedItemFetchScope::fetchVirtualReferences() const +{ + LOCKED_D(const AggregatedItemFetchScope) + // Aggregation: return true if there's at least one subscriber who wants vrefs + return d->fetchVRefs > 0; +} + +void AggregatedItemFetchScope::addSubscriber() +{ + LOCKED_D(AggregatedItemFetchScope) + ++d->subscribers; +} + +void AggregatedItemFetchScope::removeSubscriber() +{ + LOCKED_D(AggregatedItemFetchScope) + --d->subscribers; +} + +AggregatedTagFetchScope::AggregatedTagFetchScope() + : d_ptr(new AggregatedTagFetchScopePrivate) +{ +} + +AggregatedTagFetchScope::~AggregatedTagFetchScope() +{ + delete d_ptr; +} + +void AggregatedTagFetchScope::apply(const Protocol::TagFetchScope &oldScope, const Protocol::TagFetchScope &newScope) +{ + LOCKED_D(AggregatedTagFetchScope) + + if (newScope.fetchIdOnly() != oldScope.fetchIdOnly()) { + d->updateBool(newScope.fetchIdOnly(), d->fetchIdOnly); + } + if (newScope.fetchRemoteID() != oldScope.fetchRemoteID()) { + d->updateBool(newScope.fetchRemoteID(), d->fetchRemoteId); + } + if (newScope.fetchAllAttributes() != oldScope.fetchAllAttributes()) { + // Count the number of subscribers who call with false + d->updateBool(!newScope.fetchAllAttributes(), d->doNotFetchAllAttributes); + } + if (newScope.attributes() != oldScope.attributes()) { + d->applySet(oldScope.attributes(), newScope.attributes(), d->attrs, d->attrsCount); + } +} + +Protocol::TagFetchScope AggregatedTagFetchScope::toFetchScope() const +{ + Protocol::TagFetchScope tfs; + tfs.setFetchIdOnly(fetchIdOnly()); + tfs.setFetchRemoteID(fetchRemoteId()); + tfs.setFetchAllAttributes(fetchAllAttributes()); + tfs.setAttributes(attributes()); + return tfs; +} + +bool AggregatedTagFetchScope::fetchIdOnly() const +{ + LOCKED_D(const AggregatedTagFetchScope) + // Aggregation: we can return true only if everyone wants fetchIdOnly, + // otherwise there's at least one subscriber who wants everything + return d->fetchIdOnly == d->subscribers; +} + +bool AggregatedTagFetchScope::fetchRemoteId() const +{ + LOCKED_D(const AggregatedTagFetchScope) + return d->fetchRemoteId > 0; +} + +bool AggregatedTagFetchScope::fetchAllAttributes() const +{ + LOCKED_D(const AggregatedTagFetchScope) + // The default value for fetchAllAttributes is true, so we return false only if every subscriber said "do not fetch all attributes" + return d->doNotFetchAllAttributes != d->subscribers; +} + +QSet AggregatedTagFetchScope::attributes() const +{ + LOCKED_D(const AggregatedTagFetchScope) + return d->attrs; +} + +void AggregatedTagFetchScope::addSubscriber() +{ + LOCKED_D(AggregatedTagFetchScope) + ++d->subscribers; +} + +void AggregatedTagFetchScope::removeSubscriber() +{ + LOCKED_D(AggregatedTagFetchScope) + --d->subscribers; +} + +#undef LOCKED_D diff --git a/src/server/aggregatedfetchscope.h b/src/server/aggregatedfetchscope.h new file mode 100644 index 0000000..ae46c54 --- /dev/null +++ b/src/server/aggregatedfetchscope.h @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class QByteArray; + +namespace Akonadi +{ +namespace Server +{ +class AggregatedCollectionFetchScopePrivate; +class AggregatedCollectionFetchScope +{ +public: + explicit AggregatedCollectionFetchScope(); + ~AggregatedCollectionFetchScope(); + + void apply(const Protocol::CollectionFetchScope &oldScope, const Protocol::CollectionFetchScope &newScope); + + QSet attributes() const; + + bool fetchIdOnly() const; + bool fetchStatistics() const; + + void addSubscriber(); + void removeSubscriber(); + +private: + AggregatedCollectionFetchScopePrivate *const d_ptr; + Q_DECLARE_PRIVATE(AggregatedCollectionFetchScope) +}; + +class AggregatedItemFetchScopePrivate; +class AggregatedItemFetchScope +{ +public: + explicit AggregatedItemFetchScope(); + ~AggregatedItemFetchScope(); + + void apply(const Protocol::ItemFetchScope &oldScope, const Protocol::ItemFetchScope &newScope); + Protocol::ItemFetchScope toFetchScope() const; + + QSet requestedParts() const; + + Protocol::ItemFetchScope::AncestorDepth ancestorDepth() const; + void updateAncestorDepth(Protocol::ItemFetchScope::AncestorDepth oldDepth, Protocol::ItemFetchScope::AncestorDepth newDepth); + + bool cacheOnly() const; + bool fullPayload() const; + bool allAttributes() const; + bool fetchSize() const; + bool fetchMTime() const; + bool fetchRemoteRevision() const; + bool ignoreErrors() const; + bool fetchFlags() const; + bool fetchRemoteId() const; + bool fetchGID() const; + bool fetchTags() const; + bool fetchRelations() const; + bool fetchVirtualReferences() const; + + void addSubscriber(); + void removeSubscriber(); + +private: + AggregatedItemFetchScopePrivate *const d_ptr; + Q_DECLARE_PRIVATE(AggregatedItemFetchScope) +}; + +class AggregatedTagFetchScopePrivate; +class AggregatedTagFetchScope +{ +public: + explicit AggregatedTagFetchScope(); + ~AggregatedTagFetchScope(); + + void apply(const Protocol::TagFetchScope &oldScope, const Protocol::TagFetchScope &newScope); + Protocol::TagFetchScope toFetchScope() const; + + QSet attributes() const; + + void addSubscriber(); + void removeSubscriber(); + + bool fetchIdOnly() const; + bool fetchRemoteId() const; + bool fetchAllAttributes() const; + +private: + AggregatedTagFetchScopePrivate *const d_ptr; + Q_DECLARE_PRIVATE(AggregatedTagFetchScope) +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/aklocalserver.cpp b/src/server/aklocalserver.cpp new file mode 100644 index 0000000..08635c4 --- /dev/null +++ b/src/server/aklocalserver.cpp @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "aklocalserver.h" + +using namespace Akonadi::Server; + +AkLocalServer::AkLocalServer(QObject *parent) + : QLocalServer(parent) +{ +} + +void AkLocalServer::incomingConnection(quintptr socketDescriptor) +{ + Q_EMIT newConnection(socketDescriptor); +} diff --git a/src/server/aklocalserver.h b/src/server/aklocalserver.h new file mode 100644 index 0000000..30b6e17 --- /dev/null +++ b/src/server/aklocalserver.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2016 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +class AkLocalServer : public QLocalServer +{ + Q_OBJECT +public: + explicit AkLocalServer(QObject *parent = nullptr); + +Q_SIGNALS: + void newConnection(quintptr socketDescriptor); + +protected: + void incomingConnection(quintptr socketDescriptor) override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/akonadi.cpp b/src/server/akonadi.cpp new file mode 100644 index 0000000..d61677f --- /dev/null +++ b/src/server/akonadi.cpp @@ -0,0 +1,455 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "akonadi.h" +#include "akonadiserver_debug.h" +#include "connection.h" +#include "handler.h" +#include "serveradaptor.h" + +#include "aklocalserver.h" +#include "cachecleaner.h" +#include "debuginterface.h" +#include "intervalcheck.h" +#include "notificationmanager.h" +#include "preprocessormanager.h" +#include "resourcemanager.h" +#include "search/searchmanager.h" +#include "search/searchtaskmanager.h" +#include "storage/collectionstatistics.h" +#include "storage/datastore.h" +#include "storage/dbconfig.h" +#include "storage/itemretrievalmanager.h" +#include "storagejanitor.h" +#include "tracer.h" +#include "utils.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +namespace +{ +class AkonadiDataStore : public DataStore +{ + Q_OBJECT +public: + explicit AkonadiDataStore(AkonadiServer &server) + : DataStore(server) + { + } +}; + +class AkonadiDataStoreFactory : public DataStoreFactory +{ +public: + explicit AkonadiDataStoreFactory(AkonadiServer &akonadi) + : m_akonadi(akonadi) + { + } + + DataStore *createStore() override + { + return new AkonadiDataStore(m_akonadi); + } + +private: + AkonadiServer &m_akonadi; +}; + +} // namespace + +AkonadiServer::AkonadiServer() +{ + // Register bunch of useful types + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType("quintptr"); + + DataStore::setFactory(std::make_unique(*this)); +} + +bool AkonadiServer::init() +{ + qCInfo(AKONADISERVER_LOG) << "Starting up the Akonadi Server..."; + + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + // Restrict permission to 600, as the file might contain database password in plaintext + QFile::setPermissions(serverConfigFile, QFile::ReadOwner | QFile::WriteOwner); + + const QString connectionSettingsFile = StandardDirs::connectionConfigFile(StandardDirs::WriteOnly); + QSettings connectionSettings(connectionSettingsFile, QSettings::IniFormat); + + const QByteArray dbusAddress = qgetenv("DBUS_SESSION_BUS_ADDRESS"); + if (!dbusAddress.isEmpty()) { + connectionSettings.setValue(QStringLiteral("DBUS/Address"), QLatin1String(dbusAddress)); + } + + // Setup database + if (!setupDatabase()) { + quit(); + return false; + } + + // Create local servers and start listening + if (!createServers(settings, connectionSettings)) { + quit(); + return false; + } + + const auto searchManagers = settings.value(QStringLiteral("Search/Manager"), QStringList{QStringLiteral("Agent")}).toStringList(); + + mTracer = std::make_unique(); + mCollectionStats = std::make_unique(); + mCacheCleaner = std::make_unique(); + mItemRetrieval = std::make_unique(); + mAgentSearchManager = std::make_unique(); + + mDebugInterface = std::make_unique(*mTracer); + mResourceManager = std::make_unique(*mTracer); + mPreprocessorManager = std::make_unique(*mTracer); + mIntervalCheck = std::make_unique(*mItemRetrieval); + mSearchManager = std::make_unique(searchManagers, *mAgentSearchManager); + mStorageJanitor = std::make_unique(*this); + + if (settings.value(QStringLiteral("General/DisablePreprocessing"), false).toBool()) { + mPreprocessorManager->setEnabled(false); + } + + new ServerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Server"), this); + + mControlWatcher = + std::make_unique(DBus::serviceName(DBus::Control), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration); + connect(mControlWatcher.get(), &QDBusServiceWatcher::serviceUnregistered, this, [this]() { + qCCritical(AKONADISERVER_LOG) << "Control process died, committing suicide!"; + quit(); + }); + + // Unhide all the items that are actually hidden. + // The hidden flag was probably left out after an (abrupt) + // server quit. We don't attempt to resume preprocessing + // for the items as we don't actually know at which stage the + // operation was interrupted... + DataStore::self()->unhideAllPimItems(); + + // We are ready, now register org.freedesktop.Akonadi service to DBus and + // the fun can begin + if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::Server))) { + qCCritical(AKONADISERVER_LOG) << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + quit(); + return false; + } + + return true; +} + +AkonadiServer::~AkonadiServer() = default; + +bool AkonadiServer::quit() +{ + if (mAlreadyShutdown) { + return true; + } + mAlreadyShutdown = true; + + qCDebug(AKONADISERVER_LOG) << "terminating connection threads"; + mConnections.clear(); + + qCDebug(AKONADISERVER_LOG) << "terminating service threads"; + // Keep this order in sync (reversed) with the order of initialization + mStorageJanitor.reset(); + mSearchManager.reset(); + mIntervalCheck.reset(); + mPreprocessorManager.reset(); + mResourceManager.reset(); + mDebugInterface.reset(); + + mAgentSearchManager.reset(); + mItemRetrieval.reset(); + mCacheCleaner.reset(); + mCollectionStats.reset(); + mTracer.reset(); + + if (DbConfig::isConfigured()) { + if (DataStore::hasDataStore()) { + DataStore::self()->close(); + } + qCDebug(AKONADISERVER_LOG) << "stopping db process"; + stopDatabaseProcess(); + } + + // QSettings settings(StandardDirs::serverConfigFile(), QSettings::IniFormat); + const QString connectionSettingsFile = StandardDirs::connectionConfigFile(StandardDirs::WriteOnly); + + if (!QDir::home().remove(connectionSettingsFile)) { + qCCritical(AKONADISERVER_LOG) << "Failed to remove runtime connection config file"; + } + + QTimer::singleShot(0, this, &AkonadiServer::doQuit); + + return true; +} + +void AkonadiServer::doQuit() +{ + QCoreApplication::exit(); +} + +void AkonadiServer::newCmdConnection(quintptr socketDescriptor) +{ + if (mAlreadyShutdown) { + return; + } + + auto connection = std::make_unique(socketDescriptor, *this); + connect(connection.get(), &Connection::disconnected, this, &AkonadiServer::connectionDisconnected); + mConnections.push_back(std::move(connection)); +} + +void AkonadiServer::connectionDisconnected() +{ + auto it = std::find_if(mConnections.begin(), mConnections.end(), [this](const auto &ptr) { + return ptr.get() == sender(); + }); + Q_ASSERT(it != mConnections.end()); + mConnections.erase(it); +} + +bool AkonadiServer::setupDatabase() +{ + if (!DbConfig::configuredDatabase()) { + return false; + } + + if (DbConfig::configuredDatabase()->useInternalServer()) { + if (!startDatabaseProcess()) { + return false; + } + } else { + if (!createDatabase()) { + return false; + } + } + + DbConfig::configuredDatabase()->setup(); + + // initialize the database + DataStore *db = DataStore::self(); + if (!db->database().isOpen()) { + qCCritical(AKONADISERVER_LOG) << "Unable to open database" << db->database().lastError().text(); + return false; + } + if (!db->init()) { + qCCritical(AKONADISERVER_LOG) << "Unable to initialize database."; + return false; + } + + return true; +} + +bool AkonadiServer::startDatabaseProcess() +{ + if (!DbConfig::configuredDatabase()->useInternalServer()) { + qCCritical(AKONADISERVER_LOG) << "Trying to start external database!"; + } + + // create the database directories if they don't exists + StandardDirs::saveDir("data"); + StandardDirs::saveDir("data", QStringLiteral("file_db_data")); + + return DbConfig::configuredDatabase()->startInternalServer(); +} + +bool AkonadiServer::createDatabase() +{ + bool success = true; + const QLatin1String initCon("initConnection"); + QSqlDatabase db = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), initCon); + DbConfig::configuredDatabase()->apply(db); + db.setDatabaseName(DbConfig::configuredDatabase()->databaseName()); + if (!db.isValid()) { + qCCritical(AKONADISERVER_LOG) << "Invalid database object during initial database connection"; + return false; + } + + if (db.open()) { + db.close(); + } else { + qCCritical(AKONADISERVER_LOG) << "Failed to use database" << DbConfig::configuredDatabase()->databaseName(); + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + qCDebug(AKONADISERVER_LOG) << "Trying to create database now..."; + + db.close(); + db.setDatabaseName(QString()); + if (db.open()) { + { + QSqlQuery query(db); + if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(DbConfig::configuredDatabase()->databaseName()))) { + qCCritical(AKONADISERVER_LOG) << "Failed to create database"; + qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + success = false; + } + } // make sure query is destroyed before we close the db + db.close(); + } else { + qCCritical(AKONADISERVER_LOG) << "Failed to connect to database!"; + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + success = false; + } + } + return success; +} + +void AkonadiServer::stopDatabaseProcess() +{ + if (!DbConfig::configuredDatabase()->useInternalServer()) { + // closing initConnection this late to work around QTBUG-63108 + QSqlDatabase::removeDatabase(QStringLiteral("initConnection")); + return; + } + + DbConfig::configuredDatabase()->stopInternalServer(); +} + +bool AkonadiServer::createServers(QSettings &settings, QSettings &connectionSettings) +{ + mCmdServer = std::make_unique(this); + connect(mCmdServer.get(), QOverload::of(&AkLocalServer::newConnection), this, &AkonadiServer::newCmdConnection); + + mNotificationManager = std::make_unique(); + mNtfServer = std::make_unique(this); + // Note: this is a queued connection, as NotificationManager lives in its + // own thread + connect(mNtfServer.get(), QOverload::of(&AkLocalServer::newConnection), mNotificationManager.get(), &NotificationManager::registerConnection); + + // TODO: share socket setup with client +#ifdef Q_OS_WIN + // use the installation prefix as uid + QString suffix; + if (Instance::hasIdentifier()) { + suffix = QStringLiteral("%1-").arg(Instance::identifier()); + } + suffix += QString::fromUtf8(QUrl::toPercentEncoding(qApp->applicationDirPath())); + const QString defaultCmdPipe = QStringLiteral("Akonadi-Cmd-") % suffix; + const QString cmdPipe = settings.value(QStringLiteral("Connection/NamedPipe"), defaultCmdPipe).toString(); + if (!mCmdServer->listen(cmdPipe)) { + qCCritical(AKONADISERVER_LOG) << "Unable to listen on Named Pipe" << cmdPipe << ":" << mCmdServer->errorString(); + return false; + } + + const QString defaultNtfPipe = QStringLiteral("Akonadi-Ntf-") % suffix; + const QString ntfPipe = settings.value(QStringLiteral("Connection/NtfNamedPipe"), defaultNtfPipe).toString(); + if (!mNtfServer->listen(ntfPipe)) { + qCCritical(AKONADISERVER_LOG) << "Unable to listen on Named Pipe" << ntfPipe << ":" << mNtfServer->errorString(); + return false; + } + + connectionSettings.setValue(QStringLiteral("Data/Method"), QStringLiteral("NamedPipe")); + connectionSettings.setValue(QStringLiteral("Data/NamedPipe"), cmdPipe); + connectionSettings.setValue(QStringLiteral("Notifications/Method"), QStringLiteral("NamedPipe")); + connectionSettings.setValue(QStringLiteral("Notifications/NamedPipe"), ntfPipe); +#else + Q_UNUSED(settings) + + const QString cmdSocketName = QStringLiteral("akonadiserver-cmd.socket"); + const QString ntfSocketName = QStringLiteral("akonadiserver-ntf.socket"); + const QString socketDir = Utils::preferredSocketDirectory(StandardDirs::saveDir("data"), qMax(cmdSocketName.length(), ntfSocketName.length())); + const QString cmdSocketFile = socketDir % QLatin1Char('/') % cmdSocketName; + QFile::remove(cmdSocketFile); + if (!mCmdServer->listen(cmdSocketFile)) { + qCCritical(AKONADISERVER_LOG) << "Unable to listen on Unix socket" << cmdSocketFile << ":" << mCmdServer->errorString(); + return false; + } + + const QString ntfSocketFile = socketDir % QLatin1Char('/') % ntfSocketName; + QFile::remove(ntfSocketFile); + if (!mNtfServer->listen(ntfSocketFile)) { + qCCritical(AKONADISERVER_LOG) << "Unable to listen on Unix socket" << ntfSocketFile << ":" << mNtfServer->errorString(); + return false; + } + + connectionSettings.setValue(QStringLiteral("Data/Method"), QStringLiteral("UnixPath")); + connectionSettings.setValue(QStringLiteral("Data/UnixPath"), cmdSocketFile); + connectionSettings.setValue(QStringLiteral("Notifications/Method"), QStringLiteral("UnixPath")); + connectionSettings.setValue(QStringLiteral("Notifications/UnixPath"), ntfSocketFile); +#endif + + return true; +} + +CacheCleaner *AkonadiServer::cacheCleaner() +{ + return mCacheCleaner.get(); +} + +IntervalCheck &AkonadiServer::intervalChecker() +{ + return *mIntervalCheck; +} + +ResourceManager &AkonadiServer::resourceManager() +{ + return *mResourceManager; +} + +NotificationManager *AkonadiServer::notificationManager() +{ + return mNotificationManager.get(); +} + +CollectionStatistics &AkonadiServer::collectionStatistics() +{ + return *mCollectionStats; +} + +PreprocessorManager &AkonadiServer::preprocessorManager() +{ + return *mPreprocessorManager; +} + +SearchTaskManager &AkonadiServer::agentSearchManager() +{ + return *mAgentSearchManager; +} + +SearchManager &AkonadiServer::searchManager() +{ + return *mSearchManager; +} + +ItemRetrievalManager &AkonadiServer::itemRetrievalManager() +{ + return *mItemRetrieval; +} + +Tracer &AkonadiServer::tracer() +{ + return *mTracer; +} + +QString AkonadiServer::serverPath() const +{ + return StandardDirs::saveDir("config"); +} + +#include "akonadi.moc" diff --git a/src/server/akonadi.h b/src/server/akonadi.h new file mode 100644 index 0000000..e8b9435 --- /dev/null +++ b/src/server/akonadi.h @@ -0,0 +1,124 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +#include + +class QDBusServiceWatcher; +class QSettings; + +namespace Akonadi +{ +namespace Server +{ +class Connection; +class ItemRetrievalManager; +class SearchTaskManager; +class SearchManager; +class StorageJanitor; +class CacheCleaner; +class IntervalCheck; +class AkLocalServer; +class NotificationManager; +class ResourceManager; +class CollectionStatistics; +class PreprocessorManager; +class Tracer; +class DebugInterface; + +class AkonadiServer : public QObject +{ + Q_OBJECT + +public: + explicit AkonadiServer(); + ~AkonadiServer(); + + /** + * Can return a nullptr + */ + CacheCleaner *cacheCleaner(); + + /** + * Returns the IntervalCheck instance. Never nullptr. + */ + IntervalCheck &intervalChecker(); + + ResourceManager &resourceManager(); + + CollectionStatistics &collectionStatistics(); + + PreprocessorManager &preprocessorManager(); + + SearchTaskManager &agentSearchManager(); + + SearchManager &searchManager(); + + ItemRetrievalManager &itemRetrievalManager(); + + Tracer &tracer(); + + /** + * Instance-aware server .config directory + */ + QString serverPath() const; + + /** + * Can return a nullptr + */ + NotificationManager *notificationManager(); + +public Q_SLOTS: + /** + * Triggers a clean server shutdown. + */ + virtual bool quit(); + + virtual bool init(); + +protected Q_SLOTS: + virtual void newCmdConnection(quintptr socketDescriptor); + +private Q_SLOTS: + void doQuit(); + void connectionDisconnected(); + +private: + bool startDatabaseProcess(); + bool createDatabase(); + void stopDatabaseProcess(); + bool createServers(QSettings &settings, QSettings &connectionSettings); + bool setupDatabase(); + +protected: + std::unique_ptr mControlWatcher; + + std::unique_ptr mCmdServer; + std::unique_ptr mNtfServer; + + std::unique_ptr mResourceManager; + std::unique_ptr mDebugInterface; + std::unique_ptr mCollectionStats; + std::unique_ptr mPreprocessorManager; + std::unique_ptr mNotificationManager; + std::unique_ptr mCacheCleaner; + std::unique_ptr mIntervalCheck; + std::unique_ptr mStorageJanitor; + std::unique_ptr mItemRetrieval; + std::unique_ptr mAgentSearchManager; + std::unique_ptr mSearchManager; + std::unique_ptr mTracer; + + std::vector> mConnections; + bool mAlreadyShutdown = false; +}; + +} // namespace Server +} // namespace Akonadi diff --git a/src/server/akthread.cpp b/src/server/akthread.cpp new file mode 100644 index 0000000..76ea4b6 --- /dev/null +++ b/src/server/akthread.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akthread.h" +#include "akonadiserver_debug.h" +#include "storage/datastore.h" + +using namespace Akonadi::Server; + +AkThread::AkThread(const QString &objectName, StartMode startMode, QThread::Priority priority, QObject *parent) + : QObject(parent) + , m_startMode(startMode) +{ + setObjectName(objectName); + if (startMode != NoThread) { + auto thread = new QThread(); + thread->setObjectName(objectName + QStringLiteral("-Thread")); + moveToThread(thread); + thread->start(priority); + } + + if (startMode == AutoStart) { + startThread(); + } +} + +AkThread::AkThread(const QString &objectName, QThread::Priority priority, QObject *parent) + : AkThread(objectName, AutoStart, priority, parent) +{ +} + +AkThread::~AkThread() = default; + +void AkThread::startThread() +{ + Q_ASSERT(m_startMode != NoThread); + const bool init = QMetaObject::invokeMethod(this, &AkThread::init, Qt::QueuedConnection); + Q_ASSERT(init); + Q_UNUSED(init) +} + +void AkThread::quitThread() +{ + if (m_startMode == NoThread) { + return; + } + qCDebug(AKONADISERVER_LOG) << "Shutting down" << objectName() << "..."; + const bool invoke = QMetaObject::invokeMethod(this, &AkThread::quit, Qt::QueuedConnection); + + Q_ASSERT(invoke); + Q_UNUSED(invoke) + if (!thread()->wait(10 * 1000)) { + thread()->terminate(); + thread()->wait(); + } + delete thread(); +} + +void AkThread::init() +{ + Q_ASSERT(thread() == QThread::currentThread()); +} + +void AkThread::quit() +{ + Q_ASSERT(thread() == QThread::currentThread()); + + if (DataStore::hasDataStore()) { + DataStore::self()->close(); + } + + if (m_startMode != NoThread) { + thread()->quit(); + } +} diff --git a/src/server/akthread.h b/src/server/akthread.h new file mode 100644 index 0000000..2027f66 --- /dev/null +++ b/src/server/akthread.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class AkThread : public QObject +{ + Q_OBJECT +public: + enum StartMode { + AutoStart, + ManualStart, + NoThread // for unit-tests + }; + + explicit AkThread(const QString &objectName, QThread::Priority priority = QThread::InheritPriority, QObject *parent = nullptr); + explicit AkThread(const QString &objectName, StartMode startMode, QThread::Priority priority = QThread::InheritPriority, QObject *parent = nullptr); + ~AkThread() override; + +protected: + void quitThread(); + void startThread(); + +protected Q_SLOTS: + virtual void init(); + virtual void quit(); + +private: + StartMode m_startMode = AutoStart; +}; + +} +} + diff --git a/src/server/cachecleaner.cpp b/src/server/cachecleaner.cpp new file mode 100644 index 0000000..e3c276b --- /dev/null +++ b/src/server/cachecleaner.cpp @@ -0,0 +1,139 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "cachecleaner.h" +#include "akonadi.h" +#include "akonadiserver_debug.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/parthelper.h" +#include "storage/selectquerybuilder.h" + +#include + +#include + +using namespace Akonadi::Server; + +QMutex CacheCleanerInhibitor::sLock; +int CacheCleanerInhibitor::sInhibitCount = 0; + +CacheCleanerInhibitor::CacheCleanerInhibitor(AkonadiServer &akonadi, bool doInhibit) + : mCleaner(akonadi.cacheCleaner()) +{ + if (doInhibit) { + inhibit(); + } +} + +CacheCleanerInhibitor::~CacheCleanerInhibitor() +{ + if (mInhibited) { + uninhibit(); + } +} + +void CacheCleanerInhibitor::inhibit() +{ + if (mInhibited) { + qCCritical(AKONADISERVER_LOG) << "Cannot recursively inhibit an inhibitor"; + return; + } + + sLock.lock(); + if (++sInhibitCount == 1) { + if (mCleaner) { + mCleaner->inhibit(true); + } + } + sLock.unlock(); + mInhibited = true; +} + +void CacheCleanerInhibitor::uninhibit() +{ + if (!mInhibited) { + qCCritical(AKONADISERVER_LOG) << "Cannot uninhibit an uninhibited inhibitor"; // aaaw yeah + return; + } + mInhibited = false; + + sLock.lock(); + Q_ASSERT(sInhibitCount > 0); + if (--sInhibitCount == 0) { + if (mCleaner) { + mCleaner->inhibit(false); + } + } + sLock.unlock(); +} + +CacheCleaner::CacheCleaner(QObject *parent) + : CollectionScheduler(QStringLiteral("CacheCleaner"), QThread::IdlePriority, parent) +{ + setMinimumInterval(5); +} + +CacheCleaner::~CacheCleaner() +{ + quitThread(); +} + +int CacheCleaner::collectionScheduleInterval(const Collection &collection) +{ + return collection.cachePolicyCacheTimeout(); +} + +bool CacheCleaner::hasChanged(const Collection &collection, const Collection &changed) +{ + return collection.cachePolicyLocalParts() != changed.cachePolicyLocalParts() || collection.cachePolicyCacheTimeout() != changed.cachePolicyCacheTimeout() + || collection.cachePolicyInherit() != changed.cachePolicyInherit(); +} + +bool CacheCleaner::shouldScheduleCollection(const Collection &collection) +{ + return collection.cachePolicyLocalParts() != QLatin1String("ALL") && collection.cachePolicyCacheTimeout() >= 0 + && (collection.enabled() || (collection.displayPref() == Collection::True) || (collection.syncPref() == Collection::True) + || (collection.indexPref() == Collection::True)) + && collection.resourceId() > 0; +} + +void CacheCleaner::collectionExpired(const Collection &collection) +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdColumn(), PimItem::idFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, collection.id()); + qb.addValueCondition(PimItem::atimeFullColumnName(), Query::Less, QDateTime::currentDateTimeUtc().addSecs(-60 * collection.cachePolicyCacheTimeout())); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); + qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); + + const QStringList partNames = collection.cachePolicyLocalParts().split(QLatin1Char(' ')); + for (QString partName : partNames) { + if (partName.startsWith(QLatin1String(AKONADI_PARAM_PLD))) { + partName.remove(0, 4); + } + qb.addValueCondition(PartType::nameFullColumnName(), Query::NotEquals, partName); + } + if (qb.exec()) { + const Part::List parts = qb.result(); + if (!parts.isEmpty()) { + qCInfo(AKONADISERVER_LOG) << "CacheCleaner found" << parts.count() << "item parts to expire in collection" << collection.name(); + // clear data field + for (Part part : parts) { + try { + if (!PartHelper::truncate(part)) { + qCWarning(AKONADISERVER_LOG) << "CacheCleaner failed to expire item part" << part.id(); + } + } catch (const PartHelperException &e) { + qCCritical(AKONADISERVER_LOG) << e.type() << e.what(); + } + } + } + } +} diff --git a/src/server/cachecleaner.h b/src/server/cachecleaner.h new file mode 100644 index 0000000..1d8f068 --- /dev/null +++ b/src/server/cachecleaner.h @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collectionscheduler.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +class Collection; +class CacheCleaner; +class AkonadiServer; + +/** + * A RAII helper class to temporarily stop the CacheCleaner. This allows long-lasting + * operations to safely retrieve all data from resource and perform an operation on them + * (like move or copy) without risking that the cache will be cleaned in the meanwhile + * + * The inhibitor is recursive, so it's possible to create multiple instances of the + * CacheCleanerInhibitor and the CacheCleaner will be inhibited until all instances + * are destroyed again. However it's not possible to inhibit a single inhibitor + * multiple times. + */ +class CacheCleanerInhibitor +{ +public: + explicit CacheCleanerInhibitor(AkonadiServer &akonadi, bool inhibit = true); + ~CacheCleanerInhibitor(); + + void inhibit(); + void uninhibit(); + +private: + Q_DISABLE_COPY(CacheCleanerInhibitor) + static QMutex sLock; + static int sInhibitCount; + + CacheCleaner *mCleaner = nullptr; + bool mInhibited = false; +}; + +/** + Cache cleaner. + */ +class CacheCleaner : public CollectionScheduler +{ + Q_OBJECT + +public: + /** + Creates a new cache cleaner thread. + @param parent The parent object. + */ + explicit CacheCleaner(QObject *parent = nullptr); + ~CacheCleaner() override; + +protected: + void collectionExpired(const Collection &collection) override; + int collectionScheduleInterval(const Collection &collection) override; + bool hasChanged(const Collection &collection, const Collection &changed) override; + bool shouldScheduleCollection(const Collection &collection) override; + +private: + friend class CacheCleanerInhibitor; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/collectionscheduler.cpp b/src/server/collectionscheduler.cpp new file mode 100644 index 0000000..49c7b26 --- /dev/null +++ b/src/server/collectionscheduler.cpp @@ -0,0 +1,341 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionscheduler.h" +#include "akonadiserver_debug.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" + +#include +#include + +#include +#include + +using namespace std::literals::chrono_literals; + +namespace Akonadi +{ +namespace Server +{ +/** + * @warning: QTimer's methods are not virtual, so it's necessary to always call + * methods on pointer to PauseableTimer! + */ +class PauseableTimer : public QTimer +{ + Q_OBJECT + +public: + explicit PauseableTimer(QObject *parent = nullptr) + : QTimer(parent) + { + } + + void start(std::chrono::milliseconds interval) + { + mStarted = QDateTime::currentDateTimeUtc(); + mPaused = QDateTime(); + setInterval(interval); + QTimer::start(interval); + } + + void start() + { + start(std::chrono::milliseconds{interval()}); + } + + void stop() + { + mStarted = QDateTime(); + mPaused = QDateTime(); + QTimer::stop(); + } + + Q_INVOKABLE void pause() + { + if (!isActive() || isPaused()) { + return; + } + + mPaused = QDateTime::currentDateTimeUtc(); + QTimer::stop(); + } + + Q_INVOKABLE void resume() + { + if (!isPaused()) { + return; + } + + const auto remainder = std::chrono::milliseconds{interval()} - std::chrono::seconds{mStarted.secsTo(mPaused)}; + start(qMax(std::chrono::milliseconds{0}, remainder)); + mPaused = QDateTime(); + // Update mStarted so that pause() can be called repeatedly + mStarted = QDateTime::currentDateTimeUtc(); + } + + bool isPaused() const + { + return mPaused.isValid(); + } + +private: + QDateTime mStarted; + QDateTime mPaused; +}; + +} // namespace Server +} // namespace Akonadi + +using namespace Akonadi::Server; + +CollectionScheduler::CollectionScheduler(const QString &threadName, QThread::Priority priority, QObject *parent) + : AkThread(threadName, priority, parent) +{ +} + +CollectionScheduler::~CollectionScheduler() +{ +} + +// Called in secondary thread +void CollectionScheduler::quit() +{ + delete mScheduler; + mScheduler = nullptr; + + AkThread::quit(); +} + +void CollectionScheduler::inhibit(bool inhibit) +{ + if (inhibit) { + const bool success = QMetaObject::invokeMethod(mScheduler, &PauseableTimer::pause, Qt::QueuedConnection); + Q_ASSERT(success); + Q_UNUSED(success) + } else { + const bool success = QMetaObject::invokeMethod(mScheduler, &PauseableTimer::resume, Qt::QueuedConnection); + Q_ASSERT(success); + Q_UNUSED(success) + } +} + +int CollectionScheduler::minimumInterval() const +{ + return mMinInterval; +} + +CollectionScheduler::TimePoint CollectionScheduler::nextScheduledTime(qint64 collectionId) const +{ + QMutexLocker locker(&mScheduleLock); + const auto i = constFind(collectionId); + if (i != mSchedule.cend()) { + return i.key(); + } + return {}; +} + +std::chrono::milliseconds CollectionScheduler::currentTimerInterval() const +{ + return std::chrono::milliseconds(mScheduler->isActive() ? mScheduler->interval() : 0); +} + +void CollectionScheduler::setMinimumInterval(int intervalMinutes) +{ + // No mutex -- you can only call this before starting the thread + mMinInterval = intervalMinutes; +} + +void CollectionScheduler::collectionAdded(qint64 collectionId) +{ + Collection collection = Collection::retrieveById(collectionId); + DataStore::self()->activeCachePolicy(collection); + if (shouldScheduleCollection(collection)) { + QMetaObject::invokeMethod( + this, + [this, collection]() { + scheduleCollection(collection); + }, + Qt::QueuedConnection); + } +} + +void CollectionScheduler::collectionChanged(qint64 collectionId) +{ + QMutexLocker locker(&mScheduleLock); + const auto it = constFind(collectionId); + if (it != mSchedule.cend()) { + const Collection &oldCollection = it.value(); + Collection changed = Collection::retrieveById(collectionId); + DataStore::self()->activeCachePolicy(changed); + if (hasChanged(oldCollection, changed)) { + if (shouldScheduleCollection(changed)) { + locker.unlock(); + // Scheduling the changed collection will automatically remove the old one + QMetaObject::invokeMethod( + this, + [this, changed]() { + scheduleCollection(changed); + }, + Qt::QueuedConnection); + } else { + locker.unlock(); + // If the collection should no longer be scheduled then remove it + collectionRemoved(collectionId); + } + } + } else { + // We don't know the collection yet, but maybe now it can be scheduled + collectionAdded(collectionId); + } +} + +void CollectionScheduler::collectionRemoved(qint64 collectionId) +{ + QMutexLocker locker(&mScheduleLock); + auto it = find(collectionId); + if (it != mSchedule.end()) { + const bool reschedule = it == mSchedule.begin(); + mSchedule.erase(it); + + // If we just remove currently scheduled collection, schedule the next one + if (reschedule) { + QMetaObject::invokeMethod(this, &CollectionScheduler::startScheduler, Qt::QueuedConnection); + } + } +} + +// Called in secondary thread +void CollectionScheduler::startScheduler() +{ + QMutexLocker locker(&mScheduleLock); + // Don't restart timer if we are paused. + if (mScheduler->isPaused()) { + return; + } + + if (mSchedule.isEmpty()) { + // Stop the timer. It will be started again once some collection is scheduled + mScheduler->stop(); + return; + } + + // Get next collection to expire and start the timer + const auto next = mSchedule.constBegin().key(); + // TimePoint uses a signed representation internally (int64_t), so we get negative result when next is in the past + const auto delayUntilNext = std::chrono::duration_cast(next - std::chrono::steady_clock::now()); + mScheduler->start(qMax(std::chrono::milliseconds{0}, delayUntilNext)); +} + +// Called in secondary thread +void CollectionScheduler::scheduleCollection(Collection collection, bool shouldStartScheduler) +{ + DataStore::self()->activeCachePolicy(collection); + + QMutexLocker locker(&mScheduleLock); + auto i = find(collection.id()); + if (i != mSchedule.end()) { + mSchedule.erase(i); + } + + if (!shouldScheduleCollection(collection)) { + return; + } + + const int expireMinutes = qMax(mMinInterval, collectionScheduleInterval(collection)); + TimePoint nextCheck(std::chrono::steady_clock::now() + std::chrono::minutes(expireMinutes)); + + // Check whether there's another check scheduled within a minute after this one. + // If yes, then delay this check so that it's scheduled together with the others + // This is a minor optimization to reduce wakeups and SQL queries + auto it = constLowerBound(nextCheck); + if (it != mSchedule.cend() && it.key() - nextCheck < 1min) { + nextCheck = it.key(); + + // Also check whether there's another checked scheduled within a minute before + // this one. + } else if (it != mSchedule.cbegin()) { + --it; + if (nextCheck - it.key() < 1min) { + nextCheck = it.key(); + } + } + + mSchedule.insert(nextCheck, collection); + if (shouldStartScheduler && !mScheduler->isActive()) { + locker.unlock(); + startScheduler(); + } +} + +CollectionScheduler::ScheduleMap::const_iterator CollectionScheduler::constFind(qint64 collectionId) const +{ + return std::find_if(mSchedule.cbegin(), mSchedule.cend(), [collectionId](const Collection &c) { + return c.id() == collectionId; + }); +} + +CollectionScheduler::ScheduleMap::iterator CollectionScheduler::find(qint64 collectionId) +{ + return std::find_if(mSchedule.begin(), mSchedule.end(), [collectionId](const Collection &c) { + return c.id() == collectionId; + }); +} + +// separate method so we call the const version of QMap::lowerBound +CollectionScheduler::ScheduleMap::const_iterator CollectionScheduler::constLowerBound(TimePoint timestamp) const +{ + return mSchedule.lowerBound(timestamp); +} + +// Called in secondary thread +void CollectionScheduler::init() +{ + AkThread::init(); + + mScheduler = new PauseableTimer(); + mScheduler->setSingleShot(true); + connect(mScheduler, &QTimer::timeout, this, &CollectionScheduler::schedulerTimeout); + + // Only retrieve enabled collections and referenced collections, we don't care + // about anything else + SelectQueryBuilder qb; + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to query initial collections for scheduler!"; + qCWarning(AKONADISERVER_LOG) << "Not a fatal error, no collections will be scheduled for sync or cache expiration!"; + } + + const Collection::List collections = qb.result(); + for (const Collection &collection : collections) { + scheduleCollection(collection); + } + + startScheduler(); +} + +// Called in secondary thread +void CollectionScheduler::schedulerTimeout() +{ + QMutexLocker locker(&mScheduleLock); + + // Call stop() explicitly to reset the timer + mScheduler->stop(); + + const auto timestamp = mSchedule.constBegin().key(); + const QList collections = mSchedule.values(timestamp); + mSchedule.remove(timestamp); + locker.unlock(); + + for (const Collection &collection : collections) { + collectionExpired(collection); + scheduleCollection(collection, false); + } + + startScheduler(); +} + +#include "collectionscheduler.moc" diff --git a/src/server/collectionscheduler.h b/src/server/collectionscheduler.h new file mode 100644 index 0000000..785bea5 --- /dev/null +++ b/src/server/collectionscheduler.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include "akthread.h" +#include "entities.h" + +namespace Akonadi +{ +namespace Server +{ +class Collection; +class PauseableTimer; + +class CollectionScheduler : public AkThread +{ + Q_OBJECT + +public: + explicit CollectionScheduler(const QString &threadName, QThread::Priority priority, QObject *parent = nullptr); + ~CollectionScheduler() override; + + void collectionChanged(qint64 collectionId); + void collectionRemoved(qint64 collectionId); + void collectionAdded(qint64 collectionId); + + /** + * Sets the minimum timeout interval. + * + * Default value is 5. + * + * @p intervalMinutes Minimum timeout interval in minutes. + */ + void setMinimumInterval(int intervalMinutes); + Q_REQUIRED_RESULT int minimumInterval() const; + + using TimePoint = std::chrono::steady_clock::time_point; + + /** + * @return the timestamp (in seconds since epoch) when collectionExpired + * will next be called on the given collection, or 0 if we don't know about the collection. + * Only used by the unittest. + */ + TimePoint nextScheduledTime(qint64 collectionId) const; + + /** + * @return the next timeout + */ + std::chrono::milliseconds currentTimerInterval() const; + +protected: + void init() override; + void quit() override; + + virtual bool shouldScheduleCollection(const Collection &collection) = 0; + virtual bool hasChanged(const Collection &collection, const Collection &changed) = 0; + /** + * @return Return cache timeout in minutes + */ + virtual int collectionScheduleInterval(const Collection &collection) = 0; + /** + * Called when it's time to do something on that collection. + * Notice: this method is called in the secondary thread + */ + virtual void collectionExpired(const Collection &collection) = 0; + + void inhibit(bool inhibit = true); + +private Q_SLOTS: + void schedulerTimeout(); + void startScheduler(); + void scheduleCollection(/*sic!*/ Akonadi::Server::Collection collection, bool shouldStartScheduler = true); + +private: + using ScheduleMap = QMultiMap; + ScheduleMap::const_iterator constFind(qint64 collectionId) const; + ScheduleMap::iterator find(qint64 collectionId); + ScheduleMap::const_iterator constLowerBound(TimePoint timestamp) const; + + mutable QMutex mScheduleLock; + ScheduleMap mSchedule; + PauseableTimer *mScheduler = nullptr; + int mMinInterval = 5; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/commandcontext.cpp b/src/server/commandcontext.cpp new file mode 100644 index 0000000..ff90d15 --- /dev/null +++ b/src/server/commandcontext.cpp @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "commandcontext.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void CommandContext::setResource(const Resource &resource) +{ + mResource = resource; +} + +Resource CommandContext::resource() const +{ + return mResource; +} + +bool CommandContext::setScopeContext(const Protocol::ScopeContext &scopeContext) +{ + if (scopeContext.hasContextId(Protocol::ScopeContext::Collection)) { + mCollection = Collection::retrieveById(scopeContext.contextId(Protocol::ScopeContext::Collection)); + } else if (scopeContext.hasContextRID(Protocol::ScopeContext::Collection)) { + if (mResource.isValid()) { + SelectQueryBuilder qb; + qb.addValueCondition(Collection::remoteIdColumn(), Query::Equals, scopeContext.contextRID(Protocol::ScopeContext::Collection)); + qb.addValueCondition(Collection::resourceIdColumn(), Query::Equals, mResource.id()); + qb.exec(); + Collection::List cols = qb.result(); + if (cols.isEmpty()) { + // error + return false; + } + mCollection = cols.at(0); + } else { + return false; + } + } + + if (scopeContext.hasContextId(Protocol::ScopeContext::Tag)) { + mTagId = scopeContext.contextId(Protocol::ScopeContext::Tag); + } + + return true; +} + +void CommandContext::setCollection(const Collection &collection) +{ + mCollection = collection; +} + +qint64 CommandContext::collectionId() const +{ + return mCollection.id(); +} + +Collection CommandContext::collection() const +{ + return mCollection; +} + +void CommandContext::setTag(std::optional tagId) +{ + mTagId = tagId; +} + +std::optional CommandContext::tagId() const +{ + return mTagId; +} + +Tag CommandContext::tag() const +{ + return mTagId.has_value() ? Tag::retrieveById(*mTagId) : Tag(); +} + +bool CommandContext::isEmpty() const +{ + return !mCollection.isValid() && !mTagId.has_value(); +} diff --git a/src/server/commandcontext.h b/src/server/commandcontext.h new file mode 100644 index 0000000..7e69f4d --- /dev/null +++ b/src/server/commandcontext.h @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include "entities.h" + +#include + +namespace Akonadi +{ +namespace Protocol +{ +class ScopeContext; +} + +namespace Server +{ +class CommandContext +{ +public: + void setResource(const Resource &resource); + Resource resource() const; + + bool setScopeContext(const Protocol::ScopeContext &scopeContext); + + void setCollection(const Collection &collection); + qint64 collectionId() const; + Collection collection() const; + + void setTag(std::optional tagId); + std::optional tagId() const; + Tag tag() const; + + bool isEmpty() const; + +private: + Resource mResource; + Collection mCollection; + std::optional mTagId; +}; + +} + +} + diff --git a/src/server/connection.cpp b/src/server/connection.cpp new file mode 100644 index 0000000..2eba5ec --- /dev/null +++ b/src/server/connection.cpp @@ -0,0 +1,483 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#include "connection.h" +#include "akonadiserver_debug.h" + +#include +#include +#include + +#include "handler.h" +#include "notificationmanager.h" +#include "storage/datastore.h" +#include "storage/dbdeadlockcatcher.h" + +#include + +#ifndef Q_OS_WIN +#include +#endif + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define IDLE_TIMER_TIMEOUT 180000 // 3 min + +static QString connectionIdentifier(Connection *c) +{ + const QString id = QString::asprintf("%p", static_cast(c)); + return id; +} + +Connection::Connection(AkonadiServer &akonadi) + : AkThread(connectionIdentifier(this), QThread::InheritPriority) + , m_akonadi(akonadi) +{ +} + +Connection::Connection(quintptr socketDescriptor, AkonadiServer &akonadi) + : AkThread(connectionIdentifier(this), QThread::InheritPriority) + , m_akonadi(akonadi) +{ + m_socketDescriptor = socketDescriptor; + m_identifier = connectionIdentifier(this); // same as objectName() + + const QSettings settings(Akonadi::StandardDirs::serverConfigFile(), QSettings::IniFormat); + m_verifyCacheOnRetrieval = settings.value(QStringLiteral("Cache/VerifyOnRetrieval"), m_verifyCacheOnRetrieval).toBool(); +} + +void Connection::init() +{ + AkThread::init(); + + auto socket = std::make_unique(); + if (!socket->setSocketDescriptor(m_socketDescriptor)) { + qCWarning(AKONADISERVER_LOG) << "Connection(" << m_identifier << ")::run: failed to set socket descriptor: " << socket->error() << "(" + << socket->errorString() << ")"; + return; + } + + m_socket = std::move(socket); + connect(m_socket.get(), &QLocalSocket::disconnected, this, &Connection::slotSocketDisconnected); + + m_idleTimer = std::make_unique(); + connect(m_idleTimer.get(), &QTimer::timeout, this, &Connection::slotConnectionIdle); + + storageBackend()->notificationCollector()->setConnection(this); + + if (m_socket->state() == QLocalSocket::ConnectedState) { + QTimer::singleShot(0, this, &Connection::handleIncomingData); + } else { + connect(m_socket.get(), &QLocalSocket::connected, this, &Connection::handleIncomingData, Qt::QueuedConnection); + } + + try { + slotSendHello(); + } catch (const ProtocolException &e) { + qCWarning(AKONADISERVER_LOG) << "Protocol Exception sending \"hello\" on connection" << m_identifier << ":" << e.what(); + m_socket->disconnectFromServer(); + } +} + +void Connection::quit() +{ + if (QThread::currentThread()->loopLevel() > 1) { + m_connectionClosing = true; + Q_EMIT connectionClosing(); + return; + } + + m_akonadi.tracer().endConnection(m_identifier, QString()); + + m_socket.reset(); + m_idleTimer.reset(); + + AkThread::quit(); +} + +void Connection::slotSendHello() +{ + SchemaVersion version = SchemaVersion::retrieveAll().at(0); + + Protocol::HelloResponse hello; + hello.setServerName(QStringLiteral("Akonadi")); + hello.setMessage(QStringLiteral("Not Really IMAP server")); + hello.setProtocolVersion(Protocol::version()); + hello.setGeneration(version.generation()); + sendResponse(0, std::move(hello)); +} + +DataStore *Connection::storageBackend() +{ + if (!m_backend) { + m_backend = DataStore::self(); + } + return m_backend; +} + +Connection::~Connection() +{ + quitThread(); + + if (m_reportTime) { + reportTime(); + } +} + +void Connection::slotConnectionIdle() +{ + Q_ASSERT(m_currentHandler == nullptr); + if (m_backend && m_backend->isOpened()) { + if (m_backend->inTransaction()) { + // This is a programming error, the timer should not have fired. + // But it is safer to abort and leave the connection open, until + // a later operation causes the idle timer to fire (than crash + // the akonadi server). + qCInfo(AKONADISERVER_LOG) << m_sessionId << "NOT Closing idle db connection; we are in transaction"; + return; + } + m_backend->close(); + } +} + +void Connection::slotSocketDisconnected() +{ + // If we have active handler, wait for it to finish, then we emit the signal + // from slotNewDate() + if (m_currentHandler) { + return; + } + + Q_EMIT disconnected(); +} + +void Connection::parseStream(const Protocol::CommandPtr &cmd) +{ + if (!m_currentHandler->parseStream()) { + try { + m_currentHandler->failureResponse("Error while handling a command"); + } catch (...) { + m_connectionClosing = true; + } + qCWarning(AKONADISERVER_LOG) << "Error while handling command" << cmd->type() << "on connection" << m_identifier; + } +} + +void Connection::handleIncomingData() +{ + Q_FOREVER { + if (m_connectionClosing || !m_socket || m_socket->state() != QLocalSocket::ConnectedState) { + break; + } + + // Blocks with event loop until some data arrive, allows us to still use QTimers + // and similar while waiting for some data to arrive + if (m_socket->bytesAvailable() < int(sizeof(qint64))) { + QEventLoop loop; + connect(m_socket.get(), &QLocalSocket::readyRead, &loop, &QEventLoop::quit); + connect(m_socket.get(), &QLocalSocket::stateChanged, &loop, &QEventLoop::quit); + connect(this, &Connection::connectionClosing, &loop, &QEventLoop::quit); + loop.exec(); + } + + if (m_connectionClosing || !m_socket || m_socket->state() != QLocalSocket::ConnectedState) { + break; + } + + m_idleTimer->stop(); + + // will only open() a previously idle backend. + // Otherwise, a new backend could lazily be constructed by later calls. + if (!storageBackend()->isOpened()) { + m_backend->open(); + } + + QString currentCommand; + while (m_socket->bytesAvailable() >= int(sizeof(qint64))) { + Protocol::DataStream stream(m_socket.get()); + qint64 tag = -1; + stream >> tag; + // TODO: Check tag is incremental sequence + + Protocol::CommandPtr cmd; + try { + cmd = Protocol::deserialize(m_socket.get()); + } catch (const Akonadi::ProtocolException &e) { + qCWarning(AKONADISERVER_LOG) << "ProtocolException while deserializing incoming data on connection" << m_identifier << ":" << e.what(); + setState(Server::LoggingOut); + return; + } catch (const std::exception &e) { + qCWarning(AKONADISERVER_LOG) << "Unknown exception while deserializing incoming data on connection" << m_identifier << ":" << e.what(); + setState(Server::LoggingOut); + return; + } + if (cmd->type() == Protocol::Command::Invalid) { + qCWarning(AKONADISERVER_LOG) << "Received an invalid command on connection" << m_identifier << ": resetting connection"; + setState(Server::LoggingOut); + return; + } + + // Tag context and collection context is not persistent. + m_context.setTag(std::nullopt); + m_context.setCollection({}); + if (m_akonadi.tracer().currentTracer() != QLatin1String("null")) { + m_akonadi.tracer().connectionInput(m_identifier, tag, cmd); + } + + m_currentHandler = findHandlerForCommand(cmd->type()); + if (!m_currentHandler) { + qCWarning(AKONADISERVER_LOG) << "Invalid command: no such handler for" << cmd->type() << "on connection" << m_identifier; + setState(Server::LoggingOut); + return; + } + if (m_reportTime) { + startTime(); + } + + m_currentHandler->setConnection(this); + m_currentHandler->setTag(tag); + m_currentHandler->setCommand(cmd); + try { + DbDeadlockCatcher catcher([this, &cmd]() { + parseStream(cmd); + }); + } catch (const Akonadi::Server::HandlerException &e) { + if (m_currentHandler) { + try { + m_currentHandler->failureResponse(e.what()); + } catch (...) { + m_connectionClosing = true; + } + qCWarning(AKONADISERVER_LOG) << "Handler exception when handling command" << cmd->type() << "on connection" << m_identifier << ":" + << e.what(); + } + } catch (const Akonadi::Server::Exception &e) { + if (m_currentHandler) { + try { + m_currentHandler->failureResponse(QString::fromUtf8(e.type()) + QLatin1String(": ") + QString::fromUtf8(e.what())); + } catch (...) { + m_connectionClosing = true; + } + qCWarning(AKONADISERVER_LOG) << "General exception when handling command" << cmd->type() << "on connection" << m_identifier << ":" + << e.what(); + } + } catch (const Akonadi::ProtocolException &e) { + // No point trying to send anything back to client, the connection is + // already messed up + qCWarning(AKONADISERVER_LOG) << "Protocol exception when handling command" << cmd->type() << "on connection" << m_identifier << ":" << e.what(); + m_connectionClosing = true; +#if defined(Q_OS_LINUX) && !defined(_LIBCPP_VERSION) + } catch (abi::__forced_unwind &) { + // HACK: NPTL throws __forced_unwind during thread cancellation and + // we *must* rethrow it otherwise the program aborts. Due to the issue + // described in #376385 we might end up destroying (cancelling) the + // thread from a nested loop executed inside parseStream() above, + // so the exception raised in there gets caught by this try..catch + // statement and it must be rethrown at all cost. Remove this hack + // once the root problem is fixed. + throw; +#endif + } catch (...) { + qCCritical(AKONADISERVER_LOG) << "Unknown exception while handling command" << cmd->type() << "on connection" << m_identifier; + if (m_currentHandler) { + try { + m_currentHandler->failureResponse("Unknown exception caught"); + } catch (...) { + m_connectionClosing = true; + } + } + } + if (m_reportTime) { + stopTime(currentCommand); + } + m_currentHandler.reset(); + + if (!m_socket || m_socket->state() != QLocalSocket::ConnectedState) { + Q_EMIT disconnected(); + return; + } + + if (m_connectionClosing) { + break; + } + } + + // reset, arm the timer + m_idleTimer->start(IDLE_TIMER_TIMEOUT); + + if (m_connectionClosing) { + break; + } + } + + if (m_connectionClosing) { + m_socket->disconnect(this); + m_socket->close(); + QTimer::singleShot(0, this, &Connection::quit); + } +} + +const CommandContext &Connection::context() const +{ + return m_context; +} + +void Connection::setContext(const CommandContext &context) +{ + m_context = context; +} + +std::unique_ptr Connection::findHandlerForCommand(Protocol::Command::Type command) +{ + auto handler = Handler::findHandlerForCommandAlwaysAllowed(command, m_akonadi); + if (handler) { + return handler; + } + + switch (m_connectionState) { + case NonAuthenticated: + handler = Handler::findHandlerForCommandNonAuthenticated(command, m_akonadi); + break; + case Authenticated: + handler = Handler::findHandlerForCommandAuthenticated(command, m_akonadi); + break; + case LoggingOut: + break; + } + + return handler; +} + +qint64 Connection::currentTag() const +{ + return m_currentHandler->tag(); +} + +void Connection::setState(ConnectionState state) +{ + if (state == m_connectionState) { + return; + } + m_connectionState = state; + switch (m_connectionState) { + case NonAuthenticated: + assert(0); // can't happen, it's only the initial state, we can't go back to it + break; + case Authenticated: + break; + case LoggingOut: + m_socket->disconnectFromServer(); + break; + } +} + +void Connection::setSessionId(const QByteArray &id) +{ + m_identifier = QString::asprintf("%s (%p)", id.data(), static_cast(this)); + m_akonadi.tracer().beginConnection(m_identifier, QString()); + // m_streamParser->setTracerIdentifier(m_identifier); + + m_sessionId = id; + setObjectName(QString::fromLatin1(id)); + // this races with the use of objectName() in QThreadPrivate::start + // thread()->setObjectName(objectName() + QStringLiteral("-Thread")); + storageBackend()->setSessionId(id); +} + +QByteArray Connection::sessionId() const +{ + return m_sessionId; +} + +bool Connection::isOwnerResource(const PimItem &item) const +{ + if (context().resource().isValid() && item.collection().resourceId() == context().resource().id()) { + return true; + } + // fallback for older resources + if (sessionId() == item.collection().resource().name().toUtf8()) { + return true; + } + return false; +} + +bool Connection::isOwnerResource(const Collection &collection) const +{ + if (context().resource().isValid() && collection.resourceId() == context().resource().id()) { + return true; + } + if (sessionId() == collection.resource().name().toUtf8()) { + return true; + } + return false; +} + +bool Connection::verifyCacheOnRetrieval() const +{ + return m_verifyCacheOnRetrieval; +} + +void Connection::startTime() +{ + m_time.start(); +} + +void Connection::stopTime(const QString &identifier) +{ + int elapsed = m_time.elapsed(); + m_totalTime += elapsed; + m_totalTimeByHandler[identifier] += elapsed; + m_executionsByHandler[identifier]++; + qCDebug(AKONADISERVER_LOG) << identifier << " time : " << elapsed << " total: " << m_totalTime; +} + +void Connection::reportTime() const +{ + qCDebug(AKONADISERVER_LOG) << "===== Time report for " << m_identifier << " ====="; + qCDebug(AKONADISERVER_LOG) << " total: " << m_totalTime; + for (auto it = m_totalTimeByHandler.cbegin(), end = m_totalTimeByHandler.cend(); it != end; ++it) { + const QString &handler = it.key(); + qCDebug(AKONADISERVER_LOG) << "handler : " << handler << " time: " << m_totalTimeByHandler.value(handler) << " executions " + << m_executionsByHandler.value(handler) + << " avg: " << m_totalTimeByHandler.value(handler) / m_executionsByHandler.value(handler); + } +} + +void Connection::sendResponse(qint64 tag, const Protocol::CommandPtr &response) +{ + if (m_akonadi.tracer().currentTracer() != QLatin1String("null")) { + m_akonadi.tracer().connectionOutput(m_identifier, tag, response); + } + Protocol::DataStream stream(m_socket.get()); + stream << tag; + Protocol::serialize(stream, response); + stream.flush(); + if (!m_socket->waitForBytesWritten()) { + if (m_socket->state() == QLocalSocket::ConnectedState) { + throw ProtocolException("Server write timeout"); + } else { + // The client has disconnected before we managed to send our response, + // which is not an error + } + } +} + +Protocol::CommandPtr Connection::readCommand() +{ + while (m_socket->bytesAvailable() < static_cast(sizeof(qint64))) { + Protocol::DataStream::waitForData(m_socket.get(), 30000); // 30 seconds, just in case client is busy + } + + Protocol::DataStream stream(m_socket.get()); + qint64 tag; + stream >> tag; + + // TODO: compare tag with m_currentHandler->tag() ? + return Protocol::deserialize(m_socket.get()); +} diff --git a/src/server/connection.h b/src/server/connection.h new file mode 100644 index 0000000..86647b0 --- /dev/null +++ b/src/server/connection.h @@ -0,0 +1,156 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#include "akonadi.h" +#include "akthread.h" +#include "commandcontext.h" +#include "entities.h" +#include "global.h" +#include "tracer.h" + +#include +#include + +#include + +namespace Akonadi +{ +namespace Server +{ +class Handler; +class Response; +class DataStore; +class Collection; + +/** + An Connection represents one connection of a client to the server. +*/ +class Connection : public AkThread +{ + Q_OBJECT +public: + explicit Connection(quintptr socketDescriptor, AkonadiServer &akonadi); + ~Connection() override; + + virtual DataStore *storageBackend(); + + const CommandContext &context() const; + void setContext(const CommandContext &context); + + AkonadiServer &akonadi() const + { + return m_akonadi; + } + + /** + Returns @c true if this connection belongs to the owning resource of @p item. + */ + bool isOwnerResource(const PimItem &item) const; + bool isOwnerResource(const Collection &collection) const; + + void setSessionId(const QByteArray &id); + QByteArray sessionId() const; + + /** Returns @c true if permanent cache verification is enabled. */ + bool verifyCacheOnRetrieval() const; + + Protocol::CommandPtr readCommand(); + + void setState(ConnectionState state); + + template inline typename std::enable_if::value>::type sendResponse(T &&response); + + void sendResponse(qint64 tag, const Protocol::CommandPtr &response); + +Q_SIGNALS: + void disconnected(); + void connectionClosing(); + +protected Q_SLOTS: + void handleIncomingData(); + + void slotConnectionIdle(); + void slotSocketDisconnected(); + void slotSendHello(); + +protected: + Connection(AkonadiServer &akonadi); // used for testing + + void init() override; + void quit() override; + + std::unique_ptr findHandlerForCommand(Protocol::Command::Type cmd); + + qint64 currentTag() const; + +protected: + quintptr m_socketDescriptor = {}; + AkonadiServer &m_akonadi; + std::unique_ptr m_socket; + std::unique_ptr m_currentHandler; + std::unique_ptr m_idleTimer; + + ConnectionState m_connectionState = NonAuthenticated; + + mutable DataStore *m_backend = nullptr; + QList m_statusMessageQueue; + QString m_identifier; + QByteArray m_sessionId; + bool m_verifyCacheOnRetrieval = false; + CommandContext m_context; + + QElapsedTimer m_time; + qint64 m_totalTime = 0; + QHash m_totalTimeByHandler; + QHash m_executionsByHandler; + + bool m_connectionClosing = false; + +private: + void parseStream(const Protocol::CommandPtr &cmd); + template inline typename std::enable_if::value>::type sendResponse(qint64 tag, T &&response); + + /** For debugging */ + void startTime(); + void stopTime(const QString &identifier); + void reportTime() const; + bool m_reportTime = false; +}; + +template inline typename std::enable_if::value>::type Connection::sendResponse(T &&response) +{ + sendResponse(currentTag(), std::move(response)); +} + +template inline typename std::enable_if::value>::type Connection::sendResponse(qint64 tag, T &&response) +{ + if (m_akonadi.tracer().currentTracer() != QLatin1String("null")) { + m_akonadi.tracer().connectionOutput(m_identifier, tag, response); + } + Protocol::DataStream stream(m_socket.get()); + stream << tag; + stream << std::move(response); + stream.flush(); + if (!m_socket->waitForBytesWritten()) { + if (m_socket->state() == QLocalSocket::ConnectedState) { + throw ProtocolException("Server write timeout"); + } else { + // The client has disconnected before we managed to send our response, + // which is not an error + } + } +} + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/dbustracer.cpp b/src/server/dbustracer.cpp new file mode 100644 index 0000000..2fba799 --- /dev/null +++ b/src/server/dbustracer.cpp @@ -0,0 +1,55 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "dbustracer.h" +#include "tracernotificationadaptor.h" + +using namespace Akonadi::Server; + +DBusTracer::DBusTracer() + : QObject(nullptr) +{ + new TracerNotificationAdaptor(this); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/tracing/notifications"), this, QDBusConnection::ExportAdaptors); +} + +DBusTracer::~DBusTracer() = default; + +void DBusTracer::beginConnection(const QString &identifier, const QString &msg) +{ + Q_EMIT connectionStarted(identifier, msg); +} + +void DBusTracer::endConnection(const QString &identifier, const QString &msg) +{ + Q_EMIT connectionEnded(identifier, msg); +} + +void DBusTracer::connectionInput(const QString &identifier, const QByteArray &msg) +{ + Q_EMIT connectionDataInput(identifier, QString::fromUtf8(msg)); +} + +void DBusTracer::connectionOutput(const QString &identifier, const QByteArray &msg) +{ + Q_EMIT connectionDataOutput(identifier, QString::fromUtf8(msg)); +} + +void DBusTracer::signal(const QString &signalName, const QString &msg) +{ + Q_EMIT signalEmitted(signalName, msg); +} + +void DBusTracer::warning(const QString &componentName, const QString &msg) +{ + Q_EMIT warningEmitted(componentName, msg); +} + +void DBusTracer::error(const QString &componentName, const QString &msg) +{ + Q_EMIT errorEmitted(componentName, msg); +} diff --git a/src/server/dbustracer.h b/src/server/dbustracer.h new file mode 100644 index 0000000..80001e8 --- /dev/null +++ b/src/server/dbustracer.h @@ -0,0 +1,53 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include + +#include "tracerinterface.h" + +namespace Akonadi +{ +namespace Server +{ +/** + * A tracer which forwards all tracing information as dbus signals. + */ +class DBusTracer : public QObject, public TracerInterface +{ + Q_OBJECT + +public: + DBusTracer(); + ~DBusTracer() override; + + void beginConnection(const QString &identifier, const QString &msg) override; + void endConnection(const QString &identifier, const QString &msg) override; + void connectionInput(const QString &identifier, const QByteArray &msg) override; + void connectionOutput(const QString &identifier, const QByteArray &msg) override; + void signal(const QString &signalName, const QString &msg) override; + void warning(const QString &componentName, const QString &msg) override; + void error(const QString &componentName, const QString &msg) override; + + TracerInterface::ConnectionFormat connectionFormat() const override + { + return TracerInterface::Json; + } + +Q_SIGNALS: + void connectionStarted(const QString &identifier, const QString &msg); + void connectionEnded(const QString &identifier, const QString &msg); + void connectionDataInput(const QString &identifier, const QString &msg); + void connectionDataOutput(const QString &identifier, const QString &msg); + void signalEmitted(const QString &signalName, const QString &msg); + void warningEmitted(const QString &componentName, const QString &msg); + void errorEmitted(const QString &componentName, const QString &msg); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/debuginterface.cpp b/src/server/debuginterface.cpp new file mode 100644 index 0000000..80ac89e --- /dev/null +++ b/src/server/debuginterface.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "debuginterface.h" +#include "debuginterfaceadaptor.h" +#include "tracer.h" + +#include + +using namespace Akonadi::Server; + +DebugInterface::DebugInterface(Tracer &tracer) + : m_tracer(tracer) +{ + new DebugInterfaceAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/debug"), this, QDBusConnection::ExportAdaptors); +} + +QString DebugInterface::tracer() const +{ + return m_tracer.currentTracer(); +} + +void DebugInterface::setTracer(const QString &tracer) +{ + m_tracer.activateTracer(tracer); +} diff --git a/src/server/debuginterface.h b/src/server/debuginterface.h new file mode 100644 index 0000000..01b5f0d --- /dev/null +++ b/src/server/debuginterface.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +class Tracer; + +/** + * Interface to configure and query debugging options. + */ +class DebugInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.DebugInterface") + +public: + explicit DebugInterface(Tracer &tracer); + +public Q_SLOTS: + Q_SCRIPTABLE QString tracer() const; + Q_SCRIPTABLE void setTracer(const QString &tracer); + +private: + Tracer &m_tracer; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/exception.h b/src/server/exception.h new file mode 100644 index 0000000..82d41d2 --- /dev/null +++ b/src/server/exception.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace Akonadi +{ +namespace Server +{ +/** + Base class for exception used internally by the Akonadi server. +*/ +class Exception : public std::exception +{ +public: + explicit Exception(const char *what) throw() + : mWhat(what) + { + } + + explicit Exception(const QByteArray &what) throw() + : mWhat(what) + { + } + + explicit Exception(const QString &what) throw() + : mWhat(what.toUtf8()) + { + } + + Exception(const Exception &) = delete; + Exception &operator=(const Exception &) = delete; + + ~Exception() throw() override = default; + + const char *what() const throw() override + { + return mWhat.constData(); + } + + virtual const char *type() const throw() + { + return "General Exception"; + } + +protected: + QByteArray mWhat; +}; + +#define AKONADI_EXCEPTION_MAKE_INSTANCE(classname) \ + class classname : public Akonadi::Server::Exception \ + { \ + public: \ + classname(const char *what) throw() \ + : Akonadi::Server::Exception(what) \ + { \ + } \ + classname(const QByteArray &what) throw() \ + : Akonadi::Server::Exception(what) \ + { \ + } \ + classname(const QString &what) throw() \ + : Akonadi::Server::Exception(what) \ + { \ + } \ + const char *type() const throw() override \ + { \ + return "" #classname; \ + } \ + } + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/filetracer.cpp b/src/server/filetracer.cpp new file mode 100644 index 0000000..5e94507 --- /dev/null +++ b/src/server/filetracer.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#include "filetracer.h" + +#include + +using namespace Akonadi::Server; + +FileTracer::FileTracer(const QString &fileName) + : m_file(fileName) +{ + m_file.open(QIODevice::WriteOnly | QIODevice::Unbuffered); +} + +FileTracer::~FileTracer() = default; + +void FileTracer::beginConnection(const QString &identifier, const QString &msg) +{ + output(identifier, QStringLiteral("begin_connection: %1").arg(msg)); +} + +void FileTracer::endConnection(const QString &identifier, const QString &msg) +{ + output(identifier, QStringLiteral("end_connection: %1").arg(msg)); +} + +void FileTracer::connectionInput(const QString &identifier, const QByteArray &msg) +{ + output(identifier, QStringLiteral("input: %1").arg(QString::fromUtf8(msg))); +} + +void FileTracer::connectionOutput(const QString &identifier, const QByteArray &msg) +{ + output(identifier, QStringLiteral("output: %1").arg(QString::fromUtf8(msg))); +} + +void FileTracer::signal(const QString &signalName, const QString &msg) +{ + output(QStringLiteral("signal"), QStringLiteral("<%1> %2").arg(signalName, msg)); +} + +void FileTracer::warning(const QString &componentName, const QString &msg) +{ + output(QStringLiteral("warning"), QStringLiteral("<%1> %2").arg(componentName, msg)); +} + +void FileTracer::error(const QString &componentName, const QString &msg) +{ + output(QStringLiteral("error"), QStringLiteral("<%1> %2").arg(componentName, msg)); +} + +void FileTracer::output(const QString &id, const QString &msg) +{ + QString output = + QStringLiteral("%1: %2: %3\r\n").arg(QTime::currentTime().toString(QStringLiteral("HH:mm:ss.zzz")), id, msg.left(msg.indexOf(QLatin1Char('\n')))); + m_file.write(output.toUtf8()); +} diff --git a/src/server/filetracer.h b/src/server/filetracer.h new file mode 100644 index 0000000..74d3023 --- /dev/null +++ b/src/server/filetracer.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "tracerinterface.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +/** + * A tracer which forwards all tracing information to a + * log file. + */ +class FileTracer : public TracerInterface +{ +public: + explicit FileTracer(const QString &fileName); + ~FileTracer() override; + + void beginConnection(const QString &identifier, const QString &msg) override; + void endConnection(const QString &identifier, const QString &msg) override; + void connectionInput(const QString &identifier, const QByteArray &msg) override; + void connectionOutput(const QString &identifier, const QByteArray &msg) override; + void signal(const QString &signalName, const QString &msg) override; + void warning(const QString &componentName, const QString &msg) override; + void error(const QString &componentName, const QString &msg) override; + +private: + void output(const QString &id, const QString &msg); + + QFile m_file; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/global.h b/src/server/global.h new file mode 100644 index 0000000..4dd73b8 --- /dev/null +++ b/src/server/global.h @@ -0,0 +1,23 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +namespace Akonadi +{ +namespace Server +{ +// rfc1730 section 3 +/** The state of the client + */ +enum ConnectionState { + NonAuthenticated, ///< Not yet authenticated + Authenticated, ///< The client is authenticated + LoggingOut +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler.cpp b/src/server/handler.cpp new file mode 100644 index 0000000..40e2336 --- /dev/null +++ b/src/server/handler.cpp @@ -0,0 +1,249 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#include "handler.h" + +#include + +#include "handler/collectioncopyhandler.h" +#include "handler/collectioncreatehandler.h" +#include "handler/collectiondeletehandler.h" +#include "handler/collectionfetchhandler.h" +#include "handler/collectionmodifyhandler.h" +#include "handler/collectionmovehandler.h" +#include "handler/collectionstatsfetchhandler.h" +#include "handler/itemcopyhandler.h" +#include "handler/itemcreatehandler.h" +#include "handler/itemdeletehandler.h" +#include "handler/itemfetchhandler.h" +#include "handler/itemlinkhandler.h" +#include "handler/itemmodifyhandler.h" +#include "handler/itemmovehandler.h" +#include "handler/loginhandler.h" +#include "handler/logouthandler.h" +#include "handler/relationfetchhandler.h" +#include "handler/relationmodifyhandler.h" +#include "handler/relationremovehandler.h" +#include "handler/resourceselecthandler.h" +#include "handler/searchcreatehandler.h" +#include "handler/searchhandler.h" +#include "handler/searchresulthandler.h" +#include "handler/tagcreatehandler.h" +#include "handler/tagdeletehandler.h" +#include "handler/tagfetchhandler.h" +#include "handler/tagmodifyhandler.h" +#include "handler/transactionhandler.h" +#include "storage/querybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +std::unique_ptr Handler::findHandlerForCommandNonAuthenticated(Protocol::Command::Type cmd, AkonadiServer &akonadi) +{ + // allowed are LOGIN + if (cmd == Protocol::Command::Login) { + return std::make_unique(akonadi); + } + + return {}; +} + +std::unique_ptr Handler::findHandlerForCommandAlwaysAllowed(Protocol::Command::Type cmd, AkonadiServer &akonadi) +{ + // allowed is LOGOUT + if (cmd == Protocol::Command::Logout) { + return std::make_unique(akonadi); + } + return nullptr; +} + +std::unique_ptr Handler::findHandlerForCommandAuthenticated(Protocol::Command::Type cmd, AkonadiServer &akonadi) +{ + switch (cmd) { + case Protocol::Command::Invalid: + Q_ASSERT_X(cmd != Protocol::Command::Invalid, __FUNCTION__, "Invalid command is not allowed"); + return {}; + case Protocol::Command::Hello: + Q_ASSERT_X(cmd != Protocol::Command::Hello, __FUNCTION__, "Hello command is not allowed in this context"); + return {}; + case Protocol::Command::Login: + case Protocol::Command::Logout: + return {}; + case Protocol::Command::_ResponseBit: + Q_ASSERT_X(cmd != Protocol::Command::_ResponseBit, __FUNCTION__, "ResponseBit is not a valid command type"); + return {}; + + case Protocol::Command::Transaction: + return std::make_unique(akonadi); + + case Protocol::Command::CreateItem: + return std::make_unique(akonadi); + case Protocol::Command::CopyItems: + return std::make_unique(akonadi); + case Protocol::Command::DeleteItems: + return std::make_unique(akonadi); + case Protocol::Command::FetchItems: + return std::make_unique(akonadi); + case Protocol::Command::LinkItems: + return std::make_unique(akonadi); + case Protocol::Command::ModifyItems: + return std::make_unique(akonadi); + case Protocol::Command::MoveItems: + return std::make_unique(akonadi); + + case Protocol::Command::CreateCollection: + return std::make_unique(akonadi); + case Protocol::Command::CopyCollection: + return std::make_unique(akonadi); + case Protocol::Command::DeleteCollection: + return std::make_unique(akonadi); + case Protocol::Command::FetchCollections: + return std::make_unique(akonadi); + case Protocol::Command::FetchCollectionStats: + return std::make_unique(akonadi); + case Protocol::Command::ModifyCollection: + return std::make_unique(akonadi); + case Protocol::Command::MoveCollection: + return std::make_unique(akonadi); + + case Protocol::Command::Search: + return std::make_unique(akonadi); + case Protocol::Command::SearchResult: + return std::make_unique(akonadi); + case Protocol::Command::StoreSearch: + return std::make_unique(akonadi); + + case Protocol::Command::CreateTag: + return std::make_unique(akonadi); + case Protocol::Command::DeleteTag: + return std::make_unique(akonadi); + case Protocol::Command::FetchTags: + return std::make_unique(akonadi); + case Protocol::Command::ModifyTag: + return std::make_unique(akonadi); + + case Protocol::Command::FetchRelations: + return std::make_unique(akonadi); + case Protocol::Command::ModifyRelation: + return std::make_unique(akonadi); + case Protocol::Command::RemoveRelations: + return std::make_unique(akonadi); + + case Protocol::Command::SelectResource: + return std::make_unique(akonadi); + + case Protocol::Command::StreamPayload: + Q_ASSERT_X(cmd != Protocol::Command::StreamPayload, __FUNCTION__, "StreamPayload command is not allowed in this context"); + return {}; + + case Protocol::Command::ItemChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::ItemChangeNotification, __FUNCTION__, "ItemChangeNotification command is not allowed on this connection"); + return {}; + case Protocol::Command::CollectionChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::CollectionChangeNotification, + __FUNCTION__, + "CollectionChangeNotification command is not allowed on this connection"); + return {}; + case Protocol::Command::TagChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::TagChangeNotification, __FUNCTION__, "TagChangeNotification command is not allowed on this connection"); + return {}; + case Protocol::Command::RelationChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::RelationChangeNotification, __FUNCTION__, "RelationChangeNotification command is not allowed on this connection"); + return {}; + case Protocol::Command::SubscriptionChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::SubscriptionChangeNotification, + __FUNCTION__, + "SubscriptionChangeNotification command is not allowed on this connection"); + return {}; + case Protocol::Command::DebugChangeNotification: + Q_ASSERT_X(cmd != Protocol::Command::DebugChangeNotification, __FUNCTION__, "DebugChangeNotification command is not allowed on this connection"); + return {}; + case Protocol::Command::ModifySubscription: + Q_ASSERT_X(cmd != Protocol::Command::ModifySubscription, __FUNCTION__, "ModifySubscription command is not allowed on this connection"); + return {}; + case Protocol::Command::CreateSubscription: + Q_ASSERT_X(cmd != Protocol::Command::CreateSubscription, __FUNCTION__, "CreateSubscription command is not allowed on this connection"); + return {}; + } + + return {}; +} + +Handler::Handler(AkonadiServer &akonadi) + : m_akonadi(akonadi) +{ +} + +void Handler::setTag(quint64 tag) +{ + m_tag = tag; +} + +quint64 Handler::tag() const +{ + return m_tag; +} + +void Handler::setCommand(const Protocol::CommandPtr &cmd) +{ + m_command = cmd; +} + +Protocol::CommandPtr Handler::command() const +{ + return m_command; +} + +void Handler::setConnection(Connection *connection) +{ + m_connection = connection; +} + +Connection *Handler::connection() const +{ + return m_connection; +} + +DataStore *Handler::storageBackend() const +{ + return m_connection->storageBackend(); +} + +AkonadiServer &Handler::akonadi() const +{ + return m_akonadi; +} + +bool Handler::failureResponse(const QByteArray &failureMessage) +{ + return failureResponse(QString::fromUtf8(failureMessage)); +} + +bool Handler::failureResponse(const char *failureMessage) +{ + return failureResponse(QString::fromUtf8(failureMessage)); +} + +bool Handler::failureResponse(const QString &failureMessage) +{ + // Prevent sending multiple error responses from a single handler (or from + // a handler and then from Connection, since clients only expect a single + // error response + if (!m_sentFailureResponse) { + m_sentFailureResponse = true; + Protocol::ResponsePtr r = Protocol::Factory::response(m_command->type()); + // FIXME: Error enums? + r->setError(1, failureMessage); + + m_connection->sendResponse(m_tag, r); + } + + return false; +} + +bool Handler::checkScopeConstraints(const Akonadi::Scope &scope, int permittedScopes) +{ + return scope.scope() & permittedScopes; +} diff --git a/src/server/handler.h b/src/server/handler.h new file mode 100644 index 0000000..0b199b1 --- /dev/null +++ b/src/server/handler.h @@ -0,0 +1,142 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +#include "connection.h" +#include "exception.h" +#include "global.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +class AkonadiServer; +class Response; + +AKONADI_EXCEPTION_MAKE_INSTANCE(HandlerException); + +/** + \defgroup akonadi_server_handler Command handlers + + All commands supported by the Akonadi server are implemented as sub-classes of Akonadi::Handler. +*/ + +/** +The handler interfaces describes an entity capable of handling an AkonadiIMAP command.*/ +class Handler +{ +public: + Handler() = delete; + Handler(const Handler &) = delete; + Handler(Handler &&) noexcept = delete; + Handler &operator=(const Handler &) = delete; + Handler &operator=(Handler &&) noexcept = delete; + + virtual ~Handler() = default; + + /** + * Set the tag of the command to be processed, and thus of the response + * generated by this handler. + * @param tag The command tag, an alphanumerical string, normally. + */ + void setTag(quint64 tag); + + /** + * The tag of the command associated with this handler. + */ + quint64 tag() const; + + void setCommand(const Protocol::CommandPtr &cmd); + Protocol::CommandPtr command() const; + + /** + * Find a handler for a command that is always allowed, like LOGOUT. + * @param cmd the command string + * @return an instance to the handler. The handler is deleted after @see handelLine is executed. The caller needs to delete the handler in case an exception + * is thrown from handelLine. + */ + static std::unique_ptr findHandlerForCommandAlwaysAllowed(Protocol::Command::Type cmd, AkonadiServer &akonadi); + + /** + * Find a handler for a command that is allowed when the client is not yet authenticated, like LOGIN. + * @param cmd the command string + * @return an instance to the handler. The handler is deleted after @see handelLine is executed. The caller needs to delete the handler in case an exception + * is thrown from handelLine. + */ + static std::unique_ptr findHandlerForCommandNonAuthenticated(Protocol::Command::Type cmd, AkonadiServer &akonadi); + + /** + * Find a handler for a command that is allowed when the client is authenticated, like LIST, FETCH, etc. + * @param cmd the command string + * @return an instance to the handler. The handler is deleted after @see handelLine is executed. The caller needs to delete the handler in case an exception + * is thrown from handelLine. + */ + static std::unique_ptr findHandlerForCommandAuthenticated(Protocol::Command::Type cmd, AkonadiServer &akonadi); + + void setConnection(Connection *connection); + Connection *connection() const; + DataStore *storageBackend() const; + + AkonadiServer &akonadi() const; + + bool failureResponse(const char *response); + bool failureResponse(const QByteArray &response); + bool failureResponse(const QString &response); + + template inline bool successResponse(); + template inline bool successResponse(T &&response); + + template inline void sendResponse(T &&response); + template inline void sendResponse(); + + /** + * Parse and handle the IMAP message using the streaming parser. The implementation MUST leave the trailing newline character(s) in the stream! + * @return true if parsed successfully, false in case of parse failure + */ + virtual bool parseStream() = 0; + + bool checkScopeConstraints(const Scope &scope, int permittedScopes); + +protected: + Handler(AkonadiServer &akonadi); + +private: + AkonadiServer &m_akonadi; + quint64 m_tag = 0; + Connection *m_connection = nullptr; + bool m_sentFailureResponse = false; + +protected: + Protocol::CommandPtr m_command; +}; + +template inline bool Handler::successResponse() +{ + sendResponse(T{}); + return true; +} + +template inline bool Handler::successResponse(T &&response) +{ + sendResponse(std::move(response)); + return true; +} + +template inline void Handler::sendResponse() +{ + m_connection->sendResponse(T{}); +} + +template inline void Handler::sendResponse(T &&response) +{ + m_connection->sendResponse(std::move(response)); +} + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectioncopyhandler.cpp b/src/server/handler/collectioncopyhandler.cpp new file mode 100644 index 0000000..2b9b232 --- /dev/null +++ b/src/server/handler/collectioncopyhandler.cpp @@ -0,0 +1,110 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectioncopyhandler.h" + +#include "akonadi.h" +#include "cachecleaner.h" +#include "connection.h" +#include "handlerhelper.h" +#include "protocol_p.h" +#include "shared/akranges.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" +#include "storage/transaction.h" + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +CollectionCopyHandler::CollectionCopyHandler(AkonadiServer &akonadi) + : ItemCopyHandler(akonadi) +{ +} + +bool CollectionCopyHandler::copyCollection(const Collection &source, const Collection &target) +{ + if (!CollectionQueryHelper::canBeMovedTo(source, target)) { + // We don't accept source==target, or source being an ancestor of target. + return false; + } + + // copy the source collection + Collection col = source; + col.setParentId(target.id()); + col.setResourceId(target.resourceId()); + // clear remote id and revision on inter-resource copies + if (source.resourceId() != target.resourceId()) { + col.setRemoteId(QString()); + col.setRemoteRevision(QString()); + } + + const auto mimeTypes = source.mimeTypes() | Views::transform(&MimeType::name) | Actions::toQList; + const auto attributes = source.attributes() | Views::transform([](const auto &attr) { + return std::make_pair(attr.type(), attr.value()); + }) + | Actions::toQMap; + + if (!storageBackend()->appendCollection(col, mimeTypes, attributes)) { + return false; + } + + // copy sub-collections + const Collection::List lstCols = source.children(); + for (const Collection &child : lstCols) { + if (!copyCollection(child, col)) { + return false; + } + } + + // copy items + const auto items = source.items(); + for (const auto &item : items) { + if (!copyItem(item, col)) { + return false; + } + } + + return true; +} + +bool CollectionCopyHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + const Collection source = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context()); + if (!source.isValid()) { + return failureResponse(QStringLiteral("No valid source specified")); + } + + const Collection target = HandlerHelper::collectionFromScope(cmd.destination(), connection()->context()); + if (!target.isValid()) { + return failureResponse(QStringLiteral("No valid target specified")); + } + + CacheCleanerInhibitor inhibitor(akonadi()); + + // retrieve all not yet cached items of the source + ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), connection()->context()); + retriever.setCollection(source, true); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + Transaction transaction(storageBackend(), QStringLiteral("CollectionCopyHandler")); + + if (!copyCollection(source, target)) { + return failureResponse(QStringLiteral("Failed to copy collection")); + } + + if (!transaction.commit()) { + return failureResponse(QStringLiteral("Cannot commit transaction.")); + } + + return successResponse(); +} diff --git a/src/server/handler/collectioncopyhandler.h b/src/server/handler/collectioncopyhandler.h new file mode 100644 index 0000000..02b504f --- /dev/null +++ b/src/server/handler/collectioncopyhandler.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" +#include "handler/itemcopyhandler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the CollectionCopyHandler command. + + This command is used to copy a single collection into another collection, including + all sub-collections and their content. + + The copied items differ in the following points from the originals: + - new unique id + - empty remote id + - possible located in a different collection (and thus resource) + + The copied collections differ in the following points from the originals: + - new unique id + - empty remote id + - owning resource is the same as the one of the target collection + */ +class CollectionCopyHandler : public ItemCopyHandler +{ +public: + CollectionCopyHandler(AkonadiServer &akonadi); + ~CollectionCopyHandler() override = default; + + bool parseStream() override; + +private: + bool copyCollection(const Collection &source, const Collection &target); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectioncreatehandler.cpp b/src/server/handler/collectioncreatehandler.cpp new file mode 100644 index 0000000..d517014 --- /dev/null +++ b/src/server/handler/collectioncreatehandler.cpp @@ -0,0 +1,118 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Ingo Kloecker * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#include "collectioncreatehandler.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "shared/akranges.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +CollectionCreateHandler::CollectionCreateHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool CollectionCreateHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (cmd.name().isEmpty()) { + return failureResponse(QStringLiteral("Invalid collection name")); + } + + Collection parent; + qint64 resourceId = 0; + bool forceVirtual = false; + MimeType::List parentContentTypes; + + // Invalid or empty scope means we refer to root collection + if (cmd.parent().scope() != Scope::Invalid && !cmd.parent().isEmpty()) { + parent = HandlerHelper::collectionFromScope(cmd.parent(), connection()->context()); + if (!parent.isValid()) { + return failureResponse(QStringLiteral("Invalid parent collection")); + } + + // check if parent can contain a sub-folder + parentContentTypes = parent.mimeTypes(); + const auto hasMimeType = [](const QString &mimeType) { + return [mimeType](const MimeType &mt) { + return mt.name() == mimeType; + }; + }; + const bool canContainCollections = parentContentTypes | Actions::any(hasMimeType(CollectionMimeType)); + const bool canContainVirtualCollections = parentContentTypes | Actions::any(hasMimeType(VirtualCollectionMimeType)); + + if (!canContainCollections && !canContainVirtualCollections) { + return failureResponse(QStringLiteral("Parent collection can not contain sub-collections")); + } + + // If only virtual collections are supported, force every new collection to + // be virtual. Otherwise depend on VIRTUAL attribute in the command + if (canContainVirtualCollections && !canContainCollections) { + forceVirtual = true; + } + + // inherit resource + resourceId = parent.resourceId(); + } else { + const QString sessionId = QString::fromUtf8(connection()->sessionId()); + Resource res = Resource::retrieveByName(sessionId); + if (!res.isValid()) { + return failureResponse(QStringLiteral("Cannot create top-level collection")); + } + resourceId = res.id(); + } + + Collection collection; + if (parent.isValid()) { + collection.setParentId(parent.id()); + } + collection.setName(cmd.name()); + collection.setResourceId(resourceId); + collection.setRemoteId(cmd.remoteId()); + collection.setRemoteRevision(cmd.remoteRevision()); + collection.setIsVirtual(cmd.isVirtual() || forceVirtual); + collection.setEnabled(cmd.enabled()); + collection.setSyncPref(static_cast(cmd.syncPref())); + collection.setDisplayPref(static_cast(cmd.displayPref())); + collection.setIndexPref(static_cast(cmd.indexPref())); + const Protocol::CachePolicy &cp = cmd.cachePolicy(); + collection.setCachePolicyCacheTimeout(cp.cacheTimeout()); + collection.setCachePolicyCheckInterval(cp.checkInterval()); + collection.setCachePolicyInherit(cp.inherit()); + collection.setCachePolicyLocalParts(cp.localParts().join(QLatin1Char(' '))); + collection.setCachePolicySyncOnDemand(cp.syncOnDemand()); + + DataStore *db = connection()->storageBackend(); + Transaction transaction(db, QStringLiteral("CREATE")); + + QStringList effectiveMimeTypes = cmd.mimeTypes(); + if (effectiveMimeTypes.isEmpty()) { + effectiveMimeTypes = parentContentTypes | Views::transform(&MimeType::name) | Actions::toQList; + } + + if (!db->appendCollection(collection, effectiveMimeTypes, cmd.attributes())) { + return failureResponse(QStringLiteral("Could not create collection %1, resourceId %2").arg(cmd.name()).arg(resourceId)); + } + + if (!transaction.commit()) { + return failureResponse(QStringLiteral("Unable to commit transaction.")); + } + + db->activeCachePolicy(collection); + + sendResponse(HandlerHelper::fetchCollectionsResponse(akonadi(), collection)); + + return successResponse(); +} diff --git a/src/server/handler/collectioncreatehandler.h b/src/server/handler/collectioncreatehandler.h new file mode 100644 index 0000000..76625bc --- /dev/null +++ b/src/server/handler/collectioncreatehandler.h @@ -0,0 +1,28 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Ingo Kloecker * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + */ +class CollectionCreateHandler : public Handler +{ +public: + CollectionCreateHandler(AkonadiServer &akonadi); + ~CollectionCreateHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectiondeletehandler.cpp b/src/server/handler/collectiondeletehandler.cpp new file mode 100644 index 0000000..ffa5e8c --- /dev/null +++ b/src/server/handler/collectiondeletehandler.cpp @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiondeletehandler.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "search/searchmanager.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +CollectionDeleteHandler::CollectionDeleteHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool CollectionDeleteHandler::deleteRecursive(Collection &col) +{ + Collection::List children = col.children(); + for (Collection &child : children) { + if (!deleteRecursive(child)) { + return false; + } + } + + DataStore *db = connection()->storageBackend(); + return db->cleanupCollection(col); +} + +bool CollectionDeleteHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + Collection collection = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context()); + if (!collection.isValid()) { + return failureResponse(QStringLiteral("No such collection.")); + } + + // handle virtual folders + if (collection.resource().name() == QLatin1String(AKONADI_SEARCH_RESOURCE)) { + // don't delete virtual root + if (collection.parentId() == 0) { + return failureResponse(QStringLiteral("Cannot delete virtual root collection")); + } + } + + Transaction transaction(storageBackend(), QStringLiteral("DELETE")); + + if (!deleteRecursive(collection)) { + return failureResponse(QStringLiteral("Unable to delete collection")); + } + + if (!transaction.commit()) { + return failureResponse(QStringLiteral("Unable to commit transaction")); + } + + return successResponse(); +} diff --git a/src/server/handler/collectiondeletehandler.h b/src/server/handler/collectiondeletehandler.h new file mode 100644 index 0000000..5cd839a --- /dev/null +++ b/src/server/handler/collectiondeletehandler.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +class Collection; + +/** + @ingroup akonadi_server_handler + + Handler for the collection deletion command. + + This commands deletes the selected collections including all their content + and that of any child collection. +*/ +class CollectionDeleteHandler : public Handler +{ +public: + CollectionDeleteHandler(AkonadiServer &akonadi); + ~CollectionDeleteHandler() override = default; + + bool parseStream() override; + +private: + bool deleteRecursive(Collection &col); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectionfetchhandler.cpp b/src/server/handler/collectionfetchhandler.cpp new file mode 100644 index 0000000..60f4ee3 --- /dev/null +++ b/src/server/handler/collectionfetchhandler.cpp @@ -0,0 +1,555 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionfetchhandler.h" +#include "akonadiserver_debug.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +template static bool intersect(const QVector &l1, const QVector &l2) +{ + for (const T &e2 : l2) { + if (l1.contains(e2.id())) { + return true; + } + } + return false; +} + +CollectionFetchHandler::CollectionFetchHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +QStack CollectionFetchHandler::ancestorsForCollection(const Collection &col) +{ + if (mAncestorDepth <= 0) { + return QStack(); + } + QStack ancestors; + Collection parent = col; + for (int i = 0; i < mAncestorDepth; ++i) { + if (parent.parentId() == 0) { + break; + } + if (mAncestors.contains(parent.parentId())) { + parent = mAncestors.value(parent.parentId()); + } else { + parent = mCollections.value(parent.parentId()); + } + if (!parent.isValid()) { + qCWarning(AKONADISERVER_LOG) << "Found an invalid parent in ancestors of Collection" << col.name() << "(ID:" << col.id() << ")"; + throw HandlerException("Found invalid parent in ancestors"); + } + ancestors.prepend(parent); + } + return ancestors; +} + +CollectionAttribute::List CollectionFetchHandler::getAttributes(const Collection &col, const QSet &filter) +{ + CollectionAttribute::List attributes; + auto it = mCollectionAttributes.find(col.id()); + while (it != mCollectionAttributes.end() && it.key() == col.id()) { + if (filter.isEmpty() || filter.contains(it.value().type())) { + attributes << it.value(); + } + ++it; + } + + { + CollectionAttribute attr; + attr.setType(AKONADI_PARAM_ENABLED); + attr.setValue(col.enabled() ? "TRUE" : "FALSE"); + attributes << attr; + } + + return attributes; +} + +void CollectionFetchHandler::listCollection(const Collection &root, + const QStack &ancestors, + const QStringList &mimeTypes, + const CollectionAttribute::List &attributes) +{ + QStack ancestorAttributes; + // backwards compatibility, collectionToByteArray will automatically fall-back to id + remoteid + if (!mAncestorAttributes.isEmpty()) { + ancestorAttributes.reserve(ancestors.size()); + for (const Collection &col : ancestors) { + ancestorAttributes.push(getAttributes(col, mAncestorAttributes)); + } + } + + // write out collection details + Collection dummy = root; + storageBackend()->activeCachePolicy(dummy); + + sendResponse( + HandlerHelper::fetchCollectionsResponse(akonadi(), dummy, attributes, mIncludeStatistics, mAncestorDepth, ancestors, ancestorAttributes, mimeTypes)); +} + +static Query::Condition filterCondition(const QString &column) +{ + Query::Condition orCondition(Query::Or); + orCondition.addValueCondition(column, Query::Equals, static_cast(Collection::True)); + Query::Condition andCondition(Query::And); + andCondition.addValueCondition(column, Query::Equals, static_cast(Collection::Undefined)); + andCondition.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true); + orCondition.addCondition(andCondition); + return orCondition; +} + +bool CollectionFetchHandler::checkFilterCondition(const Collection &col) const +{ + // Don't include the collection when only looking for enabled collections + if (mEnabledCollections && !col.enabled()) { + return false; + } + // Don't include the collection when only looking for collections to display/index/sync + if (mCollectionsToDisplay && (((col.displayPref() == Collection::Undefined) && !col.enabled()) || (col.displayPref() == Collection::False))) { + return false; + } + if (mCollectionsToIndex && (((col.indexPref() == Collection::Undefined) && !col.enabled()) || (col.indexPref() == Collection::False))) { + return false; + } + // Single collection sync will still work since that is using a base fetch + if (mCollectionsToSynchronize && (((col.syncPref() == Collection::Undefined) && !col.enabled()) || (col.syncPref() == Collection::False))) { + return false; + } + return true; +} + +static QSqlQuery getAttributeQuery(const QVariantList &ids, const QSet &requestedAttributes) +{ + QueryBuilder qb(CollectionAttribute::tableName()); + + qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::In, ids); + + qb.addColumn(CollectionAttribute::collectionIdFullColumnName()); + qb.addColumn(CollectionAttribute::typeFullColumnName()); + qb.addColumn(CollectionAttribute::valueFullColumnName()); + + if (!requestedAttributes.isEmpty()) { + QVariantList attributes; + attributes.reserve(requestedAttributes.size()); + for (const QByteArray &type : requestedAttributes) { + attributes << type; + } + qb.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::In, attributes); + } + + qb.addSortColumn(CollectionAttribute::collectionIdFullColumnName(), Query::Ascending); + + if (!qb.exec()) { + throw HandlerException("Unable to retrieve attributes for listing"); + } + return qb.query(); +} + +void CollectionFetchHandler::retrieveAttributes(const QVariantList &collectionIds) +{ + // We are querying for the attributes in batches because something can't handle WHERE IN queries with sets larger than 999 + int start = 0; + const int size = 999; + while (start < collectionIds.size()) { + const QVariantList ids = collectionIds.mid(start, size); + QSqlQuery attributeQuery = getAttributeQuery(ids, mAncestorAttributes); + while (attributeQuery.next()) { + CollectionAttribute attr; + attr.setType(attributeQuery.value(1).toByteArray()); + attr.setValue(attributeQuery.value(2).toByteArray()); + // qCDebug(AKONADISERVER_LOG) << "found attribute " << attr.type() << attr.value(); + mCollectionAttributes.insert(attributeQuery.value(0).toLongLong(), attr); + } + attributeQuery.finish(); + start += size; + } +} + +static QSqlQuery getMimeTypeQuery(const QVariantList &ids) +{ + QueryBuilder qb(CollectionMimeTypeRelation::tableName()); + + qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), MimeType::idFullColumnName(), CollectionMimeTypeRelation::rightFullColumnName()); + qb.addValueCondition(CollectionMimeTypeRelation::leftFullColumnName(), Query::In, ids); + + qb.addColumn(CollectionMimeTypeRelation::leftFullColumnName()); + qb.addColumn(CollectionMimeTypeRelation::rightFullColumnName()); + qb.addColumn(MimeType::nameFullColumnName()); + qb.addSortColumn(CollectionMimeTypeRelation::leftFullColumnName(), Query::Ascending); + + if (!qb.exec()) { + throw HandlerException("Unable to retrieve mimetypes for listing"); + } + return qb.query(); +} + +void CollectionFetchHandler::retrieveCollections(const Collection &topParent, int depth) +{ + /* + * Retrieval of collections: + * The aim is to reduce the amount of queries as much as possible, as this has the largest performance impact for large queries. + * * First all collections that match the given criteria are queried + * * We then filter the false positives: + * ** all collections out that are not part of the tree we asked for are filtered + * * Finally we complete the tree by adding missing collections + * + * Mimetypes and attributes are also retrieved in single queries to avoid spawning two queries per collection (the N+1 problem). + * Note that we're not querying attributes and mimetypes for the collections that are only included to complete the tree, + * this results in no items being queried for those collections. + */ + + const qint64 parentId = topParent.isValid() ? topParent.id() : 0; + { + SelectQueryBuilder qb; + + if (depth == 0) { + qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, parentId); + } else if (depth == 1) { + if (topParent.isValid()) { + qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Equals, parentId); + } else { + qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is, QVariant()); + } + } else { + if (topParent.isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, topParent.resourceId()); + } else { + // Gimme gimme gimme...everything! + } + } + + // Base listings should succeed always + if (depth != 0) { + if (mCollectionsToSynchronize) { + qb.addCondition(filterCondition(Collection::syncPrefFullColumnName())); + } else if (mCollectionsToDisplay) { + qb.addCondition(filterCondition(Collection::displayPrefFullColumnName())); + } else if (mCollectionsToIndex) { + qb.addCondition(filterCondition(Collection::indexPrefFullColumnName())); + } else if (mEnabledCollections) { + qb.addValueCondition(Collection::enabledFullColumnName(), Query::Equals, true); + } + if (mResource.isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, mResource.id()); + } + + if (!mMimeTypes.isEmpty()) { + qb.addJoin(QueryBuilder::LeftJoin, + CollectionMimeTypeRelation::tableName(), + CollectionMimeTypeRelation::leftColumn(), + Collection::idFullColumnName()); + QVariantList mimeTypeFilter; + mimeTypeFilter.reserve(mMimeTypes.size()); + for (MimeType::Id mtId : std::as_const(mMimeTypes)) { + mimeTypeFilter << mtId; + } + qb.addValueCondition(CollectionMimeTypeRelation::rightColumn(), Query::In, mimeTypeFilter); + qb.addGroupColumn(Collection::idFullColumnName()); + } + } + + if (!qb.exec()) { + throw HandlerException("Unable to retrieve collection for listing"); + } + Q_FOREACH (const Collection &col, qb.result()) { + mCollections.insert(col.id(), col); + } + } + + // Post filtering that we couldn't do as part of the sql query + if (depth > 0) { + auto it = mCollections.begin(); + while (it != mCollections.end()) { + if (topParent.isValid()) { + // Check that each collection is linked to the root collection + bool foundParent = false; + // We iterate over parents to link it to topParent if possible + Collection::Id id = it->parentId(); + while (id > 0) { + if (id == parentId) { + foundParent = true; + break; + } + Collection col = mCollections.value(id); + if (!col.isValid()) { + col = Collection::retrieveById(id); + } + id = col.parentId(); + } + if (!foundParent) { + it = mCollections.erase(it); + continue; + } + } + ++it; + } + } + + QVariantList mimeTypeIds; + QVariantList attributeIds; + QVariantList ancestorIds; + const int collectionSize{mCollections.size()}; + mimeTypeIds.reserve(collectionSize); + attributeIds.reserve(collectionSize); + // We'd only require the non-leaf collections, but we don't know which those are, so we take all. + ancestorIds.reserve(collectionSize); + for (auto it = mCollections.cbegin(), end = mCollections.cend(); it != end; ++it) { + mimeTypeIds << it.key(); + attributeIds << it.key(); + ancestorIds << it.key(); + } + + if (mAncestorDepth > 0 && topParent.isValid()) { + // unless depth is 0 the base collection is not part of the listing + mAncestors.insert(topParent.id(), topParent); + ancestorIds << topParent.id(); + // We need to retrieve additional ancestors to what we already have in the tree + Collection parent = topParent; + for (int i = 0; i < mAncestorDepth; ++i) { + if (parent.parentId() == 0) { + break; + } + parent = parent.parent(); + mAncestors.insert(parent.id(), parent); + // We also require the attributes + ancestorIds << parent.id(); + } + } + + QSet missingCollections; + if (depth > 0) { + for (const Collection &col : std::as_const(mCollections)) { + if (col.parentId() != parentId && !mCollections.contains(col.parentId())) { + missingCollections.insert(col.parentId()); + } + } + } + + /* + QSet knownIds; + for (const Collection &col : mCollections) { + knownIds.insert(col.id()); + } + qCDebug(AKONADISERVER_LOG) << "HAS:" << knownIds; + qCDebug(AKONADISERVER_LOG) << "MISSING:" << missingCollections; + */ + + // Fetch missing collections that are part of the tree + while (!missingCollections.isEmpty()) { + SelectQueryBuilder qb; + QVariantList ids; + ids.reserve(missingCollections.size()); + for (qint64 id : std::as_const(missingCollections)) { + ids << id; + } + qb.addValueCondition(Collection::idFullColumnName(), Query::In, ids); + if (!qb.exec()) { + throw HandlerException("Unable to retrieve collections for listing"); + } + + missingCollections.clear(); + Q_FOREACH (const Collection &missingCol, qb.result()) { + mCollections.insert(missingCol.id(), missingCol); + ancestorIds << missingCol.id(); + attributeIds << missingCol.id(); + mimeTypeIds << missingCol.id(); + // We have to do another round if the parents parent is missing + if (missingCol.parentId() != parentId && !mCollections.contains(missingCol.parentId())) { + missingCollections.insert(missingCol.parentId()); + } + } + } + + // Since we don't know when we'll need the ancestor attributes, we have to fetch them all together. + // The alternative would be to query for each collection which would reintroduce the N+1 query performance problem. + if (!mAncestorAttributes.isEmpty()) { + retrieveAttributes(ancestorIds); + } + + // We are querying in batches because something can't handle WHERE IN queries with sets larger than 999 + const int querySizeLimit = 999; + int mimetypeQueryStart = 0; + int attributeQueryStart = 0; + QSqlQuery mimeTypeQuery(storageBackend()->database()); + QSqlQuery attributeQuery(storageBackend()->database()); + auto it = mCollections.begin(); + while (it != mCollections.end()) { + const Collection col = it.value(); + + QStringList mimeTypes; + { + // Get new query if necessary + if (!mimeTypeQuery.isValid() && mimetypeQueryStart < mimeTypeIds.size()) { + const QVariantList ids = mimeTypeIds.mid(mimetypeQueryStart, querySizeLimit); + mimetypeQueryStart += querySizeLimit; + mimeTypeQuery = getMimeTypeQuery(ids); + mimeTypeQuery.next(); // place at first record + } + + while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() < col.id()) { + if (!mimeTypeQuery.next()) { + break; + } + } + // Advance query while a mimetype for this collection is returned + while (mimeTypeQuery.isValid() && mimeTypeQuery.value(0).toLongLong() == col.id()) { + mimeTypes << mimeTypeQuery.value(2).toString(); + if (!mimeTypeQuery.next()) { + break; + } + } + } + + CollectionAttribute::List attributes; + { + // Get new query if necessary + if (!attributeQuery.isValid() && attributeQueryStart < attributeIds.size()) { + const QVariantList ids = attributeIds.mid(attributeQueryStart, querySizeLimit); + attributeQueryStart += querySizeLimit; + attributeQuery = getAttributeQuery(ids, QSet()); + attributeQuery.next(); // place at first record + } + + while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() < col.id()) { + if (!attributeQuery.next()) { + break; + } + } + // Advance query while a mimetype for this collection is returned + while (attributeQuery.isValid() && attributeQuery.value(0).toLongLong() == col.id()) { + CollectionAttribute attr; + attr.setType(attributeQuery.value(1).toByteArray()); + attr.setValue(attributeQuery.value(2).toByteArray()); + attributes << attr; + + if (!attributeQuery.next()) { + break; + } + } + } + + listCollection(col, ancestorsForCollection(col), mimeTypes, attributes); + it++; + } + attributeQuery.finish(); + mimeTypeQuery.finish(); +} + +bool CollectionFetchHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (!cmd.resource().isEmpty()) { + mResource = Resource::retrieveByName(cmd.resource()); + if (!mResource.isValid()) { + return failureResponse("Unknown resource"); + } + } + const QStringList lstMimeTypes = cmd.mimeTypes(); + for (const QString &mtName : lstMimeTypes) { + const MimeType mt = MimeType::retrieveByNameOrCreate(mtName); + if (!mt.isValid()) { + return failureResponse("Failed to create mimetype record"); + } + mMimeTypes.append(mt.id()); + } + + mEnabledCollections = cmd.enabled(); + mCollectionsToSynchronize = cmd.syncPref(); + mCollectionsToDisplay = cmd.displayPref(); + mCollectionsToIndex = cmd.indexPref(); + mIncludeStatistics = cmd.fetchStats(); + + int depth = 0; + switch (cmd.depth()) { + case Protocol::FetchCollectionsCommand::BaseCollection: + depth = 0; + break; + case Protocol::FetchCollectionsCommand::ParentCollection: + depth = 1; + break; + case Protocol::FetchCollectionsCommand::AllCollections: + depth = INT_MAX; + break; + } + + switch (cmd.ancestorsDepth()) { + case Protocol::Ancestor::NoAncestor: + mAncestorDepth = 0; + break; + case Protocol::Ancestor::ParentAncestor: + mAncestorDepth = 1; + break; + case Protocol::Ancestor::AllAncestors: + mAncestorDepth = INT_MAX; + break; + } + mAncestorAttributes = cmd.ancestorsAttributes(); + + Scope scope = cmd.collections(); + if (!scope.isEmpty()) { // not root + Collection col; + if (scope.scope() == Scope::Uid) { + col = Collection::retrieveById(scope.uid()); + } else if (scope.scope() == Scope::Rid) { + SelectQueryBuilder qb; + qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, scope.rid()); + qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); + if (mCollectionsToSynchronize) { + qb.addCondition(filterCondition(Collection::syncPrefFullColumnName())); + } else if (mCollectionsToDisplay) { + qb.addCondition(filterCondition(Collection::displayPrefFullColumnName())); + } else if (mCollectionsToIndex) { + qb.addCondition(filterCondition(Collection::indexPrefFullColumnName())); + } + if (mResource.isValid()) { + qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, mResource.id()); + } else if (connection()->context().resource().isValid()) { + qb.addValueCondition(Resource::idFullColumnName(), Query::Equals, connection()->context().resource().id()); + } else { + return failureResponse("Cannot retrieve collection based on remote identifier without a resource context"); + } + if (!qb.exec()) { + return failureResponse("Unable to retrieve collection for listing"); + } + Collection::List results = qb.result(); + if (results.count() != 1) { + return failureResponse(QString::number(results.count()) + QStringLiteral(" collections found")); + } + col = results.first(); + } else if (scope.scope() == Scope::HierarchicalRid) { + if (!connection()->context().resource().isValid()) { + return failureResponse("Cannot retrieve collection based on hierarchical remote identifier without a resource context"); + } + col = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), connection()->context().resource().id()); + } else { + return failureResponse("Unexpected error"); + } + + if (!col.isValid()) { + return failureResponse("Collection does not exist"); + } + + retrieveCollections(col, depth); + } else { // Root folder listing + if (depth != 0) { + retrieveCollections(Collection(), depth); + } + } + + return successResponse(); +} diff --git a/src/server/handler/collectionfetchhandler.h b/src/server/handler/collectionfetchhandler.h new file mode 100644 index 0000000..509ee95 --- /dev/null +++ b/src/server/handler/collectionfetchhandler.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" +#include "handler.h" + +template class QStack; + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the LIST command. + + This command is used to get a (limited) listing of the available collections. + It is different from the LIST command and is more similar to FETCH. + + The @c RID command prefix indicates that @c collection-id is a remote identifier + instead of a unique identifier. In this case a resource context has to be specified + previously using the @c RESSELECT command. + + @c depths chooses between recursive (@c INF), flat (1) and local (0, ie. just the + base collection) listing, 0 indicates the root collection. + + The @c filter-list is used to restrict the listing to collection of a specific + resource or content type. + + The @c option-list allows to specify the response content to some extend: + - @c STATISTICS (boolean) allows to include the collection statistics (see Status) + - @c ANCESTORDEPTH (numeric) allows you to specify the number of ancestor nodes that + should be included additionally to the @c parent-id included anyway. + Possible values are @c 0 (the default), @c 1 for the direct parent node and @c INF for all, + terminating with the root collection. + + The name is encoded as an quoted UTF-8 string. There is no order defined for the + single responses. + + The ancestors property is encoded as a list of UID/RID pairs. +*/ +class CollectionFetchHandler : public Handler +{ +public: + CollectionFetchHandler(AkonadiServer &akonadi); + ~CollectionFetchHandler() override = default; + + bool parseStream() override; + +private: + void listCollection(const Collection &root, const QStack &ancestors, const QStringList &mimeTypes, const CollectionAttribute::List &attributes); + QStack ancestorsForCollection(const Collection &col); + void retrieveCollections(const Collection &topParent, int depth); + bool checkFilterCondition(const Collection &col) const; + CollectionAttribute::List getAttributes(const Collection &colId, const QSet &filter = QSet()); + void retrieveAttributes(const QVariantList &collectionIds); + + Resource mResource; + QVector mMimeTypes; + int mAncestorDepth = 0; + bool mIncludeStatistics = false; + bool mEnabledCollections = false; + bool mCollectionsToDisplay = false; + bool mCollectionsToSynchronize = false; + bool mCollectionsToIndex = false; + QSet mAncestorAttributes; + QMap mCollections; + QHash mAncestors; + QMultiHash mCollectionAttributes; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectionmodifyhandler.cpp b/src/server/handler/collectionmodifyhandler.cpp new file mode 100644 index 0000000..6246064 --- /dev/null +++ b/src/server/handler/collectionmodifyhandler.cpp @@ -0,0 +1,285 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionmodifyhandler.h" + +#include "akonadi.h" +#include "akonadiserver_debug.h" +#include "cachecleaner.h" +#include "connection.h" +#include "handlerhelper.h" +#include "intervalcheck.h" +#include "search/searchmanager.h" +#include "shared/akranges.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +CollectionModifyHandler::CollectionModifyHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool CollectionModifyHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + Collection collection = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context()); + if (!collection.isValid()) { + return failureResponse("No such collection"); + } + + CacheCleanerInhibitor inhibitor(akonadi(), false); + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) { + const Collection newParent = Collection::retrieveById(cmd.parentId()); + if (newParent.isValid() && collection.parentId() != newParent.id() && collection.resourceId() != newParent.resourceId()) { + inhibitor.inhibit(); + ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), connection()->context()); + retriever.setCollection(collection, true); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + throw HandlerException(retriever.lastError()); + } + } + } + + DataStore *db = connection()->storageBackend(); + Transaction transaction(db, QStringLiteral("MODIFY")); + QList changes; + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) { + QStringList mts = cmd.mimeTypes(); + const MimeType::List currentMts = collection.mimeTypes(); + bool equal = true; + for (const MimeType ¤tMt : currentMts) { + const int removeMts = mts.removeAll(currentMt.name()); + if (removeMts > 0) { + continue; + } + equal = false; + if (!collection.removeMimeType(currentMt)) { + return failureResponse("Unable to remove collection mimetype"); + } + } + if (!db->appendMimeTypeForCollection(collection.id(), mts)) { + return failureResponse("Unable to add collection mimetypes"); + } + if (!equal || !mts.isEmpty()) { + changes.append(AKONADI_PARAM_MIMETYPE); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::CachePolicy) { + bool changed = false; + const Protocol::CachePolicy newCp = cmd.cachePolicy(); + if (collection.cachePolicyCacheTimeout() != newCp.cacheTimeout()) { + collection.setCachePolicyCacheTimeout(newCp.cacheTimeout()); + changed = true; + } + if (collection.cachePolicyCheckInterval() != newCp.checkInterval()) { + collection.setCachePolicyCheckInterval(newCp.checkInterval()); + changed = true; + } + if (collection.cachePolicyInherit() != newCp.inherit()) { + collection.setCachePolicyInherit(newCp.inherit()); + changed = true; + } + + QStringList parts = newCp.localParts(); + std::sort(parts.begin(), parts.end()); + const QString localParts = parts.join(QLatin1Char(' ')); + if (collection.cachePolicyLocalParts() != localParts) { + collection.setCachePolicyLocalParts(localParts); + changed = true; + } + if (collection.cachePolicySyncOnDemand() != newCp.syncOnDemand()) { + collection.setCachePolicySyncOnDemand(newCp.syncOnDemand()); + changed = true; + } + + if (changed) { + changes.append(AKONADI_PARAM_CACHEPOLICY); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Name) { + if (cmd.name() != collection.name()) { + if (!CollectionQueryHelper::hasAllowedName(collection, cmd.name(), collection.parentId())) { + return failureResponse("Collection with the same name exists already"); + } + collection.setName(cmd.name()); + changes.append(AKONADI_PARAM_NAME); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ParentID) { + if (collection.parentId() != cmd.parentId()) { + if (!db->moveCollection(collection, Collection::retrieveById(cmd.parentId()))) { + return failureResponse("Unable to reparent collection"); + } + changes.append(AKONADI_PARAM_PARENT); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteID) { + if (cmd.remoteId() != collection.remoteId() && !cmd.remoteId().isEmpty()) { + if (!connection()->isOwnerResource(collection)) { + qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the collection remoteID from" << collection.remoteId() << "to" << cmd.remoteId(); + return failureResponse("Only resources can modify remote identifiers"); + } + collection.setRemoteId(cmd.remoteId()); + changes.append(AKONADI_PARAM_REMOTEID); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemoteRevision) { + if (cmd.remoteRevision() != collection.remoteRevision()) { + collection.setRemoteRevision(cmd.remoteRevision()); + changes.append(AKONADI_PARAM_REMOTEREVISION); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::PersistentSearch) { + bool changed = false; + if (cmd.persistentSearchQuery() != collection.queryString()) { + collection.setQueryString(cmd.persistentSearchQuery()); + changed = true; + } + + QList queryAttributes = collection.queryAttributes().toUtf8().split(' '); + if (cmd.persistentSearchRemote() != queryAttributes.contains(AKONADI_PARAM_REMOTE)) { + if (cmd.persistentSearchRemote()) { + queryAttributes.append(AKONADI_PARAM_REMOTE); + } else { + queryAttributes.removeOne(AKONADI_PARAM_REMOTE); + } + changed = true; + } + if (cmd.persistentSearchRecursive() != queryAttributes.contains(AKONADI_PARAM_RECURSIVE)) { + if (cmd.persistentSearchRecursive()) { + queryAttributes.append(AKONADI_PARAM_RECURSIVE); + } else { + queryAttributes.removeOne(AKONADI_PARAM_RECURSIVE); + } + changed = true; + } + if (changed) { + collection.setQueryAttributes(QString::fromLatin1(queryAttributes.join(' '))); + } + + QVector inCols = cmd.persistentSearchCollections(); + std::sort(inCols.begin(), inCols.end()); + const auto cols = inCols | Views::transform([](const auto col) { + return QString::number(col); + }) + | Actions::toQList; + const QString colStr = cols.join(QLatin1Char(' ')); + if (colStr != collection.queryCollections()) { + collection.setQueryCollections(colStr); + changed = true; + } + + if (changed || cmd.modifiedParts() & Protocol::ModifyCollectionCommand::MimeTypes) { + changes.append(AKONADI_PARAM_PERSISTENTSEARCH); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::ListPreferences) { + if (cmd.enabled() != collection.enabled()) { + collection.setEnabled(cmd.enabled()); + changes.append(AKONADI_PARAM_ENABLED); + } + if (cmd.syncPref() != static_cast(collection.syncPref())) { + collection.setSyncPref(static_cast(cmd.syncPref())); + changes.append(AKONADI_PARAM_SYNC); + } + if (cmd.displayPref() != static_cast(collection.displayPref())) { + collection.setDisplayPref(static_cast(cmd.displayPref())); + changes.append(AKONADI_PARAM_DISPLAY); + } + if (cmd.indexPref() != static_cast(collection.indexPref())) { + collection.setIndexPref(static_cast(cmd.indexPref())); + changes.append(AKONADI_PARAM_INDEX); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::RemovedAttributes) { + Q_FOREACH (const QByteArray &attr, cmd.removedAttributes()) { + if (db->removeCollectionAttribute(collection, attr)) { + changes.append(attr); + } + } + } + + if (cmd.modifiedParts() & Protocol::ModifyCollectionCommand::Attributes) { + const QMap attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + SelectQueryBuilder qb; + qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, collection.id()); + qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, iter.key()); + if (!qb.exec()) { + return failureResponse("Unable to retrieve collection attribute"); + } + + const CollectionAttribute::List attrs = qb.result(); + if (attrs.isEmpty()) { + CollectionAttribute newAttr; + newAttr.setCollectionId(collection.id()); + newAttr.setType(iter.key()); + newAttr.setValue(iter.value()); + if (!newAttr.insert()) { + return failureResponse("Unable to add collection attribute"); + } + changes.append(iter.key()); + } else if (attrs.size() == 1) { + CollectionAttribute currAttr = attrs.first(); + if (currAttr.value() == iter.value()) { + continue; + } + currAttr.setValue(iter.value()); + if (!currAttr.update()) { + return failureResponse("Unable to update collection attribute"); + } + changes.append(iter.key()); + } else { + return failureResponse("WTF: more than one attribute with the same name"); + } + } + } + + if (!changes.isEmpty()) { + if (collection.hasPendingChanges() && !collection.update()) { + return failureResponse("Unable to update collection"); + } + db->notificationCollector()->collectionChanged(collection, changes); + // For backwards compatibility. Must be after the changed notification (otherwise the compression removes it). + if (changes.contains(AKONADI_PARAM_ENABLED)) { + if (collection.enabled()) { + db->notificationCollector()->collectionSubscribed(collection); + } else { + db->notificationCollector()->collectionUnsubscribed(collection); + } + } + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + + // Only request Search update AFTER committing the transaction to avoid + // transaction deadlock with SQLite + if (changes.contains(AKONADI_PARAM_PERSISTENTSEARCH)) { + akonadi().searchManager().updateSearch(collection); + } + } + + return successResponse(); +} diff --git a/src/server/handler/collectionmodifyhandler.h b/src/server/handler/collectionmodifyhandler.h new file mode 100644 index 0000000..4e72362 --- /dev/null +++ b/src/server/handler/collectionmodifyhandler.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + This command is used to modify collections. Its syntax is similar to the STORE + command. +*/ +class CollectionModifyHandler : public Handler +{ +public: + CollectionModifyHandler(AkonadiServer &akonadi); + ~CollectionModifyHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectionmovehandler.cpp b/src/server/handler/collectionmovehandler.cpp new file mode 100644 index 0000000..2df4035 --- /dev/null +++ b/src/server/handler/collectionmovehandler.cpp @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionmovehandler.h" + +#include "akonadi.h" +#include "cachecleaner.h" +#include "connection.h" +#include "handlerhelper.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" +#include "storage/transaction.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +CollectionMoveHandler::CollectionMoveHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool CollectionMoveHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + Collection source = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context()); + if (!source.isValid()) { + return failureResponse(QStringLiteral("Invalid collection to move")); + } + + Collection target; + if (cmd.destination().isEmpty()) { + target.setId(0); + } else { + target = HandlerHelper::collectionFromScope(cmd.destination(), connection()->context()); + if (!target.isValid()) { + return failureResponse(QStringLiteral("Invalid destination collection")); + } + } + + if (source.parentId() == target.id()) { + return successResponse(); + } + + CacheCleanerInhibitor inhibitor(akonadi()); + + // retrieve all not yet cached items of the source + ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), connection()->context()); + retriever.setCollection(source, true); + retriever.setRetrieveFullPayload(true); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store, QStringLiteral("CollectionMoveHandler")); + + if (!store->moveCollection(source, target)) { + return failureResponse(QStringLiteral("Unable to reparent collection")); + } + + if (!transaction.commit()) { + return failureResponse(QStringLiteral("Cannot commit transaction.")); + } + + return successResponse(); +} diff --git a/src/server/handler/collectionmovehandler.h b/src/server/handler/collectionmovehandler.h new file mode 100644 index 0000000..5c9cf50 --- /dev/null +++ b/src/server/handler/collectionmovehandler.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the MoveCollection command + + This command is used to move a set of collections into another collection, including + all sub-collections and their content. +*/ +class CollectionMoveHandler : public Handler +{ +public: + CollectionMoveHandler(AkonadiServer &akonadi); + ~CollectionMoveHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/collectionstatsfetchhandler.cpp b/src/server/handler/collectionstatsfetchhandler.cpp new file mode 100644 index 0000000..d202a86 --- /dev/null +++ b/src/server/handler/collectionstatsfetchhandler.cpp @@ -0,0 +1,45 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Ingo Kloecker * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "collectionstatsfetchhandler.h" + +#include "akonadi.h" +#include "connection.h" +#include "global.h" +#include "handlerhelper.h" +#include "storage/collectionstatistics.h" +#include "storage/datastore.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +CollectionStatsFetchHandler::CollectionStatsFetchHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool CollectionStatsFetchHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + const Collection col = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context()); + if (!col.isValid()) { + return failureResponse(QStringLiteral("No status for this folder")); + } + + const auto stats = akonadi().collectionStatistics().statistics(col); + if (stats.count == -1) { + return failureResponse(QStringLiteral("Failed to query statistics.")); + } + + Protocol::FetchCollectionStatsResponse resp; + resp.setCount(stats.count); + resp.setUnseen(stats.count - stats.read); + resp.setSize(stats.size); + return successResponse(std::move(resp)); +} diff --git a/src/server/handler/collectionstatsfetchhandler.h b/src/server/handler/collectionstatsfetchhandler.h new file mode 100644 index 0000000..c2042f1 --- /dev/null +++ b/src/server/handler/collectionstatsfetchhandler.h @@ -0,0 +1,30 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Ingo Kloecker * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the STATUS command. + */ +class CollectionStatsFetchHandler : public Handler +{ +public: + CollectionStatsFetchHandler(AkonadiServer &akonadi); + ~CollectionStatsFetchHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemcopyhandler.cpp b/src/server/handler/itemcopyhandler.cpp new file mode 100644 index 0000000..f675c18 --- /dev/null +++ b/src/server/handler/itemcopyhandler.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemcopyhandler.h" + +#include "akonadi.h" +#include "cachecleaner.h" +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretriever.h" +#include "storage/parthelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +ItemCopyHandler::ItemCopyHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ItemCopyHandler::copyItem(const PimItem &item, const Collection &target) +{ + PimItem newItem = item; + newItem.setId(-1); + newItem.setRev(0); + newItem.setDatetime(QDateTime::currentDateTimeUtc()); + newItem.setAtime(QDateTime::currentDateTimeUtc()); + newItem.setRemoteId(QString()); + newItem.setRemoteRevision(QString()); + newItem.setCollectionId(target.id()); + Part::List parts; + parts.reserve(item.parts().count()); + Q_FOREACH (const Part &part, item.parts()) { + Part newPart(part); + newPart.setData(PartHelper::translateData(newPart.data(), part.storage())); + newPart.setPimItemId(-1); + newPart.setStorage(Part::Internal); + parts << newPart; + } + + DataStore *store = connection()->storageBackend(); + return store->appendPimItem(parts, item.flags(), item.mimeType(), target, QDateTime::currentDateTimeUtc(), QString(), QString(), item.gid(), newItem); +} + +void ItemCopyHandler::processItems(const QVector &ids) +{ + SelectQueryBuilder qb; + ItemQueryHelper::itemSetToQuery(ImapSet(ids), qb); + if (!qb.exec()) { + failureResponse(QStringLiteral("Unable to retrieve items")); + return; + } + const PimItem::List items = qb.result(); + qb.query().finish(); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store, QStringLiteral("COPY")); + + for (const PimItem &item : items) { + if (!copyItem(item, mTargetCollection)) { + failureResponse(QStringLiteral("Unable to copy item")); + return; + } + } + + if (!transaction.commit()) { + failureResponse(QStringLiteral("Cannot commit transaction.")); + return; + } +} + +bool ItemCopyHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (!checkScopeConstraints(cmd.items(), Scope::Uid)) { + return failureResponse(QStringLiteral("Only UID copy is allowed")); + } + + if (cmd.items().isEmpty()) { + return failureResponse(QStringLiteral("No items specified")); + } + + mTargetCollection = HandlerHelper::collectionFromScope(cmd.destination(), connection()->context()); + if (!mTargetCollection.isValid()) { + return failureResponse(QStringLiteral("No valid target specified")); + } + if (mTargetCollection.isVirtual()) { + return failureResponse(QStringLiteral("Copying items into virtual collections is not allowed")); + } + + CacheCleanerInhibitor inhibitor(akonadi()); + + ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), connection()->context()); + retriever.setItemSet(cmd.items().uidSet()); + retriever.setRetrieveFullPayload(true); + QObject::connect(&retriever, &ItemRetriever::itemsRetrieved, [this](const QVector &ids) { + processItems(ids); + }); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + return successResponse(); +} diff --git a/src/server/handler/itemcopyhandler.h b/src/server/handler/itemcopyhandler.h new file mode 100644 index 0000000..ccf85da --- /dev/null +++ b/src/server/handler/itemcopyhandler.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the COPY command. + + This command is used to copy a set of items into the specific collection. It + is syntactically identical to the IMAP COPY command. + + The copied items differ in the following points from the originals: + - new unique id + - empty remote id + - possible located in a different collection (and thus resource) + + There is only the usual status response indicating success or failure of the + COPY command + */ +class ItemCopyHandler : public Handler +{ +public: + ItemCopyHandler(AkonadiServer &akonadi); + ~ItemCopyHandler() override = default; + + bool parseStream() override; + +protected: + /** + Copy the given item and all its parts into the @p target. + The changes mentioned above are applied. + */ + bool copyItem(const PimItem &item, const Collection &target); + void processItems(const QVector &ids); + +private: + Collection mTargetCollection; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemcreatehandler.cpp b/src/server/handler/itemcreatehandler.cpp new file mode 100644 index 0000000..201e30d --- /dev/null +++ b/src/server/handler/itemcreatehandler.cpp @@ -0,0 +1,496 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2007 Robert Zwerus * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "itemcreatehandler.h" + +#include "akonadi.h" +#include "connection.h" +#include "handlerhelper.h" +#include "itemfetchhelper.h" +#include "preprocessormanager.h" +#include "storage/datastore.h" +#include "storage/dbconfig.h" +#include "storage/itemretrievalmanager.h" +#include "storage/parthelper.h" +#include "storage/partstreamer.h" +#include "storage/parttypehelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" +#include + +#include "shared/akranges.h" +#include "shared/akscopeguard.h" + +#include //std::accumulate + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +ItemCreateHandler::ItemCreateHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ItemCreateHandler::buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &item, Collection &parentCol) +{ + parentCol = HandlerHelper::collectionFromScope(cmd.collection(), connection()->context()); + if (!parentCol.isValid()) { + return failureResponse(QStringLiteral("Invalid parent collection")); + } + if (parentCol.isVirtual()) { + return failureResponse(QStringLiteral("Cannot append item into virtual collection")); + } + + MimeType mimeType = MimeType::retrieveByNameOrCreate(cmd.mimeType()); + if (!mimeType.isValid()) { + return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'.")); + } + + item.setRev(0); + item.setSize(cmd.itemSize()); + item.setMimeTypeId(mimeType.id()); + item.setCollectionId(parentCol.id()); + item.setDatetime(cmd.dateTime()); + if (cmd.remoteId().isEmpty()) { + // from application + item.setDirty(true); + } else { + // from resource + item.setRemoteId(cmd.remoteId()); + item.setDirty(false); + } + item.setRemoteRevision(cmd.remoteRevision()); + item.setGid(cmd.gid()); + item.setAtime(QDateTime::currentDateTimeUtc()); + + return true; +} + +bool ItemCreateHandler::insertItem(const Protocol::CreateItemCommand &cmd, PimItem &item, const Collection &parentCol) +{ + if (!item.datetime().isValid()) { + item.setDatetime(QDateTime::currentDateTimeUtc()); + } + + if (!item.insert()) { + return failureResponse(QStringLiteral("Failed to append item")); + } + + // set message flags + const QSet flags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.flags() : cmd.addedFlags(); + if (!flags.isEmpty()) { + // This will hit an entry in cache inserted there in buildPimItem() + const Flag::List flagList = HandlerHelper::resolveFlags(flags); + bool flagsChanged = false; + if (!storageBackend()->appendItemsFlags({item}, flagList, &flagsChanged, false, parentCol, true)) { + return failureResponse("Unable to append item flags."); + } + } + + const Scope tags = cmd.mergeModes() == Protocol::CreateItemCommand::None ? cmd.tags() : cmd.addedTags(); + if (!tags.isEmpty()) { + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context()); + bool tagsChanged = false; + if (!storageBackend()->appendItemsTags({item}, tagList, &tagsChanged, false, parentCol, true)) { + return failureResponse(QStringLiteral("Unable to append item tags.")); + } + } + + // Handle individual parts + qint64 partSizes = 0; + PartStreamer streamer(connection(), item); + const auto parts = cmd.parts(); + for (const QByteArray &partName : parts) { + qint64 partSize = 0; + try { + streamer.stream(true, partName, partSize); + } catch (const PartStreamerException &e) { + return failureResponse(e.what()); + } + partSizes += partSize; + } + const Protocol::Attributes attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + try { + streamer.streamAttribute(true, iter.key(), iter.value()); + } catch (const PartStreamerException &e) { + return failureResponse(e.what()); + } + } + + // TODO: Try to avoid this addition query + if (partSizes > item.size()) { + item.setSize(partSizes); + item.update(); + } + + // Preprocessing + if (akonadi().preprocessorManager().isActive()) { + Part hiddenAttribute; + hiddenAttribute.setPimItemId(item.id()); + hiddenAttribute.setPartType(PartTypeHelper::fromFqName(QStringLiteral(AKONADI_ATTRIBUTE_HIDDEN))); + hiddenAttribute.setData(QByteArray()); + hiddenAttribute.setDatasize(0); + // TODO: Handle errors? Technically, this is not a critical issue as no data are lost + PartHelper::insert(&hiddenAttribute); + } + + const bool seen = flags.contains(AKONADI_FLAG_SEEN) || flags.contains(AKONADI_FLAG_IGNORED); + notify(item, seen, item.collection()); + sendResponse(item, Protocol::CreateItemCommand::None); + + return true; +} + +bool ItemCreateHandler::mergeItem(const Protocol::CreateItemCommand &cmd, PimItem &newItem, PimItem ¤tItem, const Collection &parentCol) +{ + bool needsUpdate = false; + QSet changedParts; + + if (!newItem.remoteId().isEmpty() && currentItem.remoteId() != newItem.remoteId()) { + currentItem.setRemoteId(newItem.remoteId()); + changedParts.insert(AKONADI_PARAM_REMOTEID); + needsUpdate = true; + } + if (!newItem.remoteRevision().isEmpty() && currentItem.remoteRevision() != newItem.remoteRevision()) { + currentItem.setRemoteRevision(newItem.remoteRevision()); + changedParts.insert(AKONADI_PARAM_REMOTEREVISION); + needsUpdate = true; + } + if (!newItem.gid().isEmpty() && currentItem.gid() != newItem.gid()) { + currentItem.setGid(newItem.gid()); + changedParts.insert(AKONADI_PARAM_GID); + needsUpdate = true; + } + if (newItem.datetime().isValid() && newItem.datetime() != currentItem.datetime()) { + currentItem.setDatetime(newItem.datetime()); + needsUpdate = true; + } + + if (newItem.size() > 0 && newItem.size() != currentItem.size()) { + currentItem.setSize(newItem.size()); + needsUpdate = true; + } + + const Collection col = Collection::retrieveById(parentCol.id()); + if (cmd.flags().isEmpty() && !cmd.flagsOverwritten()) { + bool flagsAdded = false; + bool flagsRemoved = false; + if (!cmd.addedFlags().isEmpty()) { + const auto addedFlags = HandlerHelper::resolveFlags(cmd.addedFlags()); + storageBackend()->appendItemsFlags({currentItem}, addedFlags, &flagsAdded, true, col, true); + } + if (!cmd.removedFlags().isEmpty()) { + const auto removedFlags = HandlerHelper::resolveFlags(cmd.removedFlags()); + storageBackend()->removeItemsFlags({currentItem}, removedFlags, &flagsRemoved, col, true); + } + if (flagsAdded || flagsRemoved) { + changedParts.insert(AKONADI_PARAM_FLAGS); + needsUpdate = true; + } + } else { + bool flagsChanged = false; + QSet flagNames = cmd.flags(); + + static QVector localFlagsToPreserve = {"$ATTACHMENT", "$INVITATION", "$ENCRYPTED", "$SIGNED", "$WATCHED"}; + + // Make sure we don't overwrite some local-only flags that can't come + // through from Resource during ItemSync, like $ATTACHMENT, because the + // resource is not aware of them (they are usually assigned by client + // upon inspecting the payload) + const Flag::List currentFlags = currentItem.flags(); + for (const Flag ¤tFlag : currentFlags) { + const QByteArray currentFlagName = currentFlag.name().toLatin1(); + if (localFlagsToPreserve.contains(currentFlagName)) { + flagNames.insert(currentFlagName); + } + } + const auto flags = HandlerHelper::resolveFlags(flagNames); + storageBackend()->setItemsFlags({currentItem}, ¤tFlags, flags, &flagsChanged, col, true); + if (flagsChanged) { + changedParts.insert(AKONADI_PARAM_FLAGS); + needsUpdate = true; + } + } + + if (cmd.tags().isEmpty()) { + bool tagsAdded = false; + bool tagsRemoved = false; + if (!cmd.addedTags().isEmpty()) { + const auto addedTags = HandlerHelper::tagsFromScope(cmd.addedTags(), connection()->context()); + storageBackend()->appendItemsTags({currentItem}, addedTags, &tagsAdded, true, col, true); + } + if (!cmd.removedTags().isEmpty()) { + const Tag::List removedTags = HandlerHelper::tagsFromScope(cmd.removedTags(), connection()->context()); + storageBackend()->removeItemsTags({currentItem}, removedTags, &tagsRemoved, true); + } + + if (tagsAdded || tagsRemoved) { + changedParts.insert(AKONADI_PARAM_TAGS); + needsUpdate = true; + } + } else { + bool tagsChanged = false; + const auto tags = HandlerHelper::tagsFromScope(cmd.tags(), connection()->context()); + storageBackend()->setItemsTags({currentItem}, tags, &tagsChanged, true); + if (tagsChanged) { + changedParts.insert(AKONADI_PARAM_TAGS); + needsUpdate = true; + } + } + + const Part::List existingParts = Part::retrieveFiltered(Part::pimItemIdColumn(), currentItem.id()); + QMap partsSizes; + for (const Part &part : existingParts) { + partsSizes.insert(PartTypeHelper::fullName(part.partType()).toLatin1(), part.datasize()); + } + + PartStreamer streamer(connection(), currentItem); + Q_FOREACH (const QByteArray &partName, cmd.parts()) { + bool changed = false; + qint64 partSize = 0; + try { + streamer.stream(true, partName, partSize, &changed); + } catch (const PartStreamerException &e) { + return failureResponse(e.what()); + } + + if (changed) { + changedParts.insert(partName); + partsSizes.insert(partName, partSize); + needsUpdate = true; + } + } + + const qint64 size = std::accumulate(partsSizes.begin(), partsSizes.end(), 0LL); + if (size > currentItem.size()) { + currentItem.setSize(size); + needsUpdate = true; + } + + if (needsUpdate) { + currentItem.setRev(qMax(newItem.rev(), currentItem.rev()) + 1); + currentItem.setAtime(QDateTime::currentDateTimeUtc()); + // Only mark dirty when merged from application + currentItem.setDirty(!connection()->context().resource().isValid()); + + // Store all changes + if (!currentItem.update()) { + return failureResponse("Failed to store merged item"); + } + + notify(currentItem, currentItem.collection(), changedParts); + } + + sendResponse(currentItem, cmd.mergeModes()); + + return true; +} + +bool ItemCreateHandler::sendResponse(const PimItem &item, Protocol::CreateItemCommand::MergeModes mergeModes) +{ + if (mergeModes & Protocol::CreateItemCommand::Silent || mergeModes & Protocol::CreateItemCommand::None) { + Protocol::FetchItemsResponse resp; + resp.setId(item.id()); + resp.setMTime(item.datetime()); + Handler::sendResponse(std::move(resp)); + return true; + } + + Protocol::ItemFetchScope fetchScope; + fetchScope.setAncestorDepth(Protocol::ItemFetchScope::ParentAncestor); + fetchScope.setFetch(Protocol::ItemFetchScope::AllAttributes | Protocol::ItemFetchScope::FullPayload | Protocol::ItemFetchScope::CacheOnly + | Protocol::ItemFetchScope::Flags | Protocol::ItemFetchScope::GID | Protocol::ItemFetchScope::MTime | Protocol::ItemFetchScope::RemoteID + | Protocol::ItemFetchScope::RemoteRevision | Protocol::ItemFetchScope::Size | Protocol::ItemFetchScope::Tags); + ImapSet set; + set.add(QVector() << item.id()); + Scope scope; + scope.setUidSet(set); + + ItemFetchHelper fetchHelper(connection(), scope, fetchScope, Protocol::TagFetchScope{}, akonadi()); + if (!fetchHelper.fetchItems()) { + return failureResponse("Failed to retrieve item"); + } + + return true; +} + +bool ItemCreateHandler::notify(const PimItem &item, bool seen, const Collection &collection) +{ + storageBackend()->notificationCollector()->itemAdded(item, seen, collection); + + if (akonadi().preprocessorManager().isActive()) { + // enqueue the item for preprocessing + akonadi().preprocessorManager().beginHandleItem(item, storageBackend()); + } + return true; +} + +bool ItemCreateHandler::notify(const PimItem &item, const Collection &collection, const QSet &changedParts) +{ + if (!changedParts.isEmpty()) { + storageBackend()->notificationCollector()->itemChanged(item, changedParts, collection); + } + return true; +} + +void ItemCreateHandler::recoverFromMultipleMergeCandidates(const PimItem::List &items, const Collection &collection) +{ + // HACK HACK HACK: When this happens within ItemSync, we are running inside a client-side + // transaction, so just calling commit here won't have any effect, since this handler will + // ultimately fail and the client will rollback the transaction. To circumvent this, we + // will forcibly commit the transaction, do our changes here within a new transaction and + // then we open a new transaction so that the client won't notice. + + int transactionDepth = 0; + while (storageBackend()->inTransaction()) { + ++transactionDepth; + storageBackend()->commitTransaction(); + } + const AkScopeGuard restoreTransaction([&]() { + for (int i = 0; i < transactionDepth; ++i) { + storageBackend()->beginTransaction(QStringLiteral("RestoredTransactionAfterMMCRecovery")); + } + }); + + Transaction transaction(storageBackend(), QStringLiteral("MMC Recovery Transaction")); + + // If any of the conflicting items is dirty or does not have a remote ID, we don't want to remove + // them as it would cause data loss. There's a chance next changeReplay will fix this, so + // next time the ItemSync hits this multiple merge candidates, all changes will be committed + // and this check will succeed + if (items | Actions::any([](const auto &item) { + return item.dirty() || item.remoteId().isEmpty(); + })) { + qCWarning(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery failed: at least one of the candidates has uncommitted changes!"; + return; + } + + // This cannot happen with ItemSync, but in theory could happen during individual GID merge. + if (items | Actions::any([collection](const auto &item) { + return item.collectionId() != collection.id(); + })) { + qCWarning(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery failed: all candidates do not belong to the same collection."; + return; + } + + storageBackend()->cleanupPimItems(items, DataStore::Silent); + if (!transaction.commit()) { + qCWarning(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery failed: failed to commit database transaction."; + return; + } + + // Schedule a new sync of the collection, one that will succeed + akonadi().itemRetrievalManager().triggerCollectionSync(collection.resource().name(), collection.id()); + + qCInfo(AKONADISERVER_LOG) << "Automatic multiple merge candidates recovery successful: conflicting items" + << (items | Views::transform([](const auto &i) { + return i.id(); + }) + | Actions::toQVector) + << "in collection" << collection.name() << "(ID:" << collection.id() + << ") were removed and a new sync was scheduled in the resource" << collection.resource().name(); +} + +bool ItemCreateHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + // FIXME: The streaming/reading of all item parts can hold the transaction for + // unnecessary long time -> should we wrap the PimItem into one transaction + // and try to insert Parts independently? In case we fail to insert a part, + // it's not a problem as it can be re-fetched at any time, except for attributes. + Transaction transaction(storageBackend(), QStringLiteral("ItemCreateHandler")); + ExternalPartStorageTransaction storageTrx; + + PimItem item; + Collection parentCol; + if (!buildPimItem(cmd, item, parentCol)) { + return false; + } + + if (cmd.mergeModes() == Protocol::CreateItemCommand::None) { + if (!insertItem(cmd, item, parentCol)) { + return false; + } + if (!transaction.commit()) { + return failureResponse(QStringLiteral("Failed to commit transaction")); + } + storageTrx.commit(); + } else { + // Merging is always restricted to the same collection + SelectQueryBuilder qb; + qb.setForUpdate(); + qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, parentCol.id()); + Query::Condition rootCondition(Query::Or); + + Query::Condition mergeCondition(Query::And); + if (cmd.mergeModes() & Protocol::CreateItemCommand::GID) { + mergeCondition.addValueCondition(PimItem::gidColumn(), Query::Equals, item.gid()); + } + if (cmd.mergeModes() & Protocol::CreateItemCommand::RemoteID) { + mergeCondition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); + } + rootCondition.addCondition(mergeCondition); + + // If an Item with matching RID but empty GID exists during GID merge, + // merge into this item instead of creating a new one + if (cmd.mergeModes() & Protocol::CreateItemCommand::GID && !item.remoteId().isEmpty()) { + mergeCondition = Query::Condition(Query::And); + mergeCondition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, item.remoteId()); + mergeCondition.addValueCondition(PimItem::gidColumn(), Query::Equals, QLatin1String("")); + rootCondition.addCondition(mergeCondition); + } + qb.addCondition(rootCondition); + + if (!qb.exec()) { + return failureResponse("Failed to query database for item"); + } + + const QVector result = qb.result(); + if (result.isEmpty()) { + // No item with such GID/RID exists, so call ItemCreateHandler::insert() and behave + // like if this was a new item + if (!insertItem(cmd, item, parentCol)) { + return false; + } + if (!transaction.commit()) { + return failureResponse("Failed to commit transaction"); + } + storageTrx.commit(); + + } else if (result.count() == 1) { + // Item with matching GID/RID combination exists, so merge this item into it + // and send itemChanged() + PimItem existingItem = result.at(0); + + if (!mergeItem(cmd, item, existingItem, parentCol)) { + return false; + } + if (!transaction.commit()) { + return failureResponse("Failed to commit transaction"); + } + storageTrx.commit(); + } else { + qCWarning(AKONADISERVER_LOG) << "Multiple merge candidates, will attempt to recover:"; + for (const PimItem &item : result) { + qCWarning(AKONADISERVER_LOG) << "\tID:" << item.id() << ", RID:" << item.remoteId() << ", GID:" << item.gid() + << ", Collection:" << item.collection().name() << "(" << item.collectionId() << ")" + << ", Resource:" << item.collection().resource().name() << "(" << item.collection().resourceId() << ")"; + } + + transaction.commit(); // commit the current transaction, before we attempt MMC recovery + recoverFromMultipleMergeCandidates(result, parentCol); + + // Even if the recovery was successful, indicate error to force the client to abort the + // sync, since we've interfered with the overall state. + return failureResponse(QStringLiteral("Multiple merge candidates in collection '%1', aborting").arg(item.collection().name())); + } + } + + return successResponse(); +} diff --git a/src/server/handler/itemcreatehandler.h b/src/server/handler/itemcreatehandler.h new file mode 100644 index 0000000..6d7f14e --- /dev/null +++ b/src/server/handler/itemcreatehandler.h @@ -0,0 +1,49 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2007 Robert Zwerus * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "entities.h" +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the X-AKAPPEND command. + + This command is used to append an item with multiple parts. + + */ +class ItemCreateHandler : public Handler +{ +public: + ItemCreateHandler(AkonadiServer &akonadi); + ~ItemCreateHandler() override = default; + + bool parseStream() override; + +private: + bool buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &item, Collection &parentCollection); + + bool insertItem(const Protocol::CreateItemCommand &cmd, PimItem &item, const Collection &parentCollection); + + bool mergeItem(const Protocol::CreateItemCommand &cmd, PimItem &newItem, PimItem ¤tItem, const Collection &parentCollection); + + bool sendResponse(const PimItem &item, Protocol::CreateItemCommand::MergeModes mergeModes); + + bool notify(const PimItem &item, bool seen, const Collection &collection); + bool notify(const PimItem &item, const Collection &collection, const QSet &changedParts); + + void recoverFromMultipleMergeCandidates(const PimItem::List &items, const Collection &collection); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemdeletehandler.cpp b/src/server/handler/itemdeletehandler.cpp new file mode 100644 index 0000000..0fc063c --- /dev/null +++ b/src/server/handler/itemdeletehandler.cpp @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemdeletehandler.h" + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +ItemDeleteHandler::ItemDeleteHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ItemDeleteHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + CommandContext context = connection()->context(); + if (!context.setScopeContext(cmd.scopeContext())) { + return failureResponse(QStringLiteral("Invalid scope context")); + } + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery(cmd.items(), context, qb); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store, QStringLiteral("REMOVE")); + + if (!qb.exec()) { + return failureResponse("Unable to execute query"); + } + + const QVector items = qb.result(); + if (items.isEmpty()) { + return failureResponse("No items found"); + } + if (!store->cleanupPimItems(items)) { + return failureResponse("Deletion failed"); + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + + return successResponse(); +} diff --git a/src/server/handler/itemdeletehandler.h b/src/server/handler/itemdeletehandler.h new file mode 100644 index 0000000..ae69ba7 --- /dev/null +++ b/src/server/handler/itemdeletehandler.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the item deletion command. + +

Semantics

+ Removes the selected items. Item selection can happen within the usual three scopes: + - based on a uid set relative to the currently selected collection + - based on a global uid set (UID) + - based on a remote identifier within the currently selected collection (RID) +*/ +class ItemDeleteHandler : public Handler +{ +public: + ItemDeleteHandler(AkonadiServer &akonadi); + ~ItemDeleteHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemfetchhandler.cpp b/src/server/handler/itemfetchhandler.cpp new file mode 100644 index 0000000..e56d422 --- /dev/null +++ b/src/server/handler/itemfetchhandler.cpp @@ -0,0 +1,43 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "itemfetchhandler.h" + +#include "cachecleaner.h" +#include "connection.h" +#include "itemfetchhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +ItemFetchHandler::ItemFetchHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ItemFetchHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + CommandContext context = connection()->context(); + if (!context.setScopeContext(cmd.scopeContext())) { + return failureResponse(QStringLiteral("Invalid scope context")); + } + + // We require context in case we do RID fetch + if (context.isEmpty() && cmd.scope().scope() == Scope::Rid) { + return failureResponse(QStringLiteral("No FETCH context specified")); + } + + CacheCleanerInhibitor inhibitor(akonadi()); + + ItemFetchHelper fetchHelper(connection(), context, cmd.scope(), cmd.itemFetchScope(), cmd.tagFetchScope(), akonadi()); + if (!fetchHelper.fetchItems()) { + return failureResponse(QStringLiteral("Failed to fetch items")); + } + + return successResponse(); +} diff --git a/src/server/handler/itemfetchhandler.h b/src/server/handler/itemfetchhandler.h new file mode 100644 index 0000000..f1ab32f --- /dev/null +++ b/src/server/handler/itemfetchhandler.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the fetch command. +*/ +class ItemFetchHandler : public Handler +{ +public: + ItemFetchHandler(AkonadiServer &akonadi); + ~ItemFetchHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemfetchhelper.cpp b/src/server/handler/itemfetchhelper.cpp new file mode 100644 index 0000000..9ce4db5 --- /dev/null +++ b/src/server/handler/itemfetchhelper.cpp @@ -0,0 +1,747 @@ + +/*************************************************************************** + * SPDX-FileCopyrightText: 2006-2009 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "itemfetchhelper.h" + +#include "akonadi.h" +#include "connection.h" +#include "handler.h" +#include "handlerhelper.h" +#include "shared/akranges.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretrievalmanager.h" +#include "storage/itemretrievalrequest.h" +#include "storage/parthelper.h" +#include "storage/parttypehelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include "agentmanagerinterface.h" +#include "akonadiserver_debug.h" +#include "intervalcheck.h" +#include "relationfetchhandler.h" +#include "tagfetchhelper.h" +#include "utils.h" + +#include + +#include +#include +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +#define ENABLE_FETCH_PROFILING 0 +#if ENABLE_FETCH_PROFILING +#define BEGIN_TIMER(name) \ + QElapsedTimer name##Timer; \ + name##Timer.start(); + +#define END_TIMER(name) const double name##Elapsed = name##Timer.nsecsElapsed() / 1000000.0; +#define PROF_INC(name) ++name; +#else +#define BEGIN_TIMER(name) +#define END_TIMER(name) +#define PROF_INC(name) +#endif + +ItemFetchHelper::ItemFetchHelper(Connection *connection, + const Scope &scope, + const Protocol::ItemFetchScope &itemFetchScope, + const Protocol::TagFetchScope &tagFetchScope, + AkonadiServer &akonadi) + : ItemFetchHelper(connection, connection->context(), scope, itemFetchScope, tagFetchScope, akonadi) +{ +} + +ItemFetchHelper::ItemFetchHelper(Connection *connection, + const CommandContext &context, + const Scope &scope, + const Protocol::ItemFetchScope &itemFetchScope, + const Protocol::TagFetchScope &tagFetchScope, + AkonadiServer &akonadi) + : mConnection(connection) + , mContext(context) + , mScope(scope) + , mItemFetchScope(itemFetchScope) + , mTagFetchScope(tagFetchScope) + , mAkonadi(akonadi) +{ + std::fill(mItemQueryColumnMap, mItemQueryColumnMap + ItemQueryColumnCount, -1); +} + +void ItemFetchHelper::disableATimeUpdates() +{ + mUpdateATimeEnabled = false; +} + +enum PartQueryColumns { + PartQueryPimIdColumn, + PartQueryTypeIdColumn, + PartQueryDataColumn, + PartQueryStorageColumn, + PartQueryVersionColumn, + PartQueryDataSizeColumn +}; + +QSqlQuery ItemFetchHelper::buildPartQuery(const QVector &partList, bool allPayload, bool allAttrs) +{ + /// TODO: merge with ItemQuery + QueryBuilder partQuery(PimItem::tableName()); + + if (!partList.isEmpty() || allPayload || allAttrs) { + partQuery.addJoin(QueryBuilder::InnerJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); + partQuery.addColumn(PimItem::idFullColumnName()); + partQuery.addColumn(Part::partTypeIdFullColumnName()); + partQuery.addColumn(Part::dataFullColumnName()); + partQuery.addColumn(Part::storageFullColumnName()); + partQuery.addColumn(Part::versionFullColumnName()); + partQuery.addColumn(Part::datasizeFullColumnName()); + + partQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!partList.isEmpty() || allPayload || allAttrs) { + Query::Condition cond(Query::Or); + for (const QByteArray &b : std::as_const(partList)) { + if (b.startsWith("PLD") || b.startsWith("ATR")) { + cond.addValueCondition(Part::partTypeIdFullColumnName(), Query::Equals, PartTypeHelper::fromFqName(b).id()); + } + } + if (allPayload || allAttrs) { + partQuery.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + if (allPayload) { + cond.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("PLD")); + } + if (allAttrs) { + cond.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("ATR")); + } + } + + partQuery.addCondition(cond); + } + + ItemQueryHelper::scopeToQuery(mScope, mContext, partQuery); + + if (!partQuery.exec()) { + throw HandlerException("Unable to list item parts"); + } + partQuery.query().next(); + } + + return partQuery.query(); +} + +QSqlQuery ItemFetchHelper::buildItemQuery() +{ + QueryBuilder itemQuery(PimItem::tableName()); + + int column = 0; +#define ADD_COLUMN(colName, colId) \ + { \ + itemQuery.addColumn(colName); \ + mItemQueryColumnMap[colId] = column++; \ + } + ADD_COLUMN(PimItem::idFullColumnName(), ItemQueryPimItemIdColumn); + if (mItemFetchScope.fetchRemoteId()) { + ADD_COLUMN(PimItem::remoteIdFullColumnName(), ItemQueryPimItemRidColumn) + } + ADD_COLUMN(PimItem::mimeTypeIdFullColumnName(), ItemQueryMimeTypeIdColumn) + ADD_COLUMN(PimItem::revFullColumnName(), ItemQueryRevColumn) + if (mItemFetchScope.fetchRemoteRevision()) { + ADD_COLUMN(PimItem::remoteRevisionFullColumnName(), ItemQueryRemoteRevisionColumn) + } + if (mItemFetchScope.fetchSize()) { + ADD_COLUMN(PimItem::sizeFullColumnName(), ItemQuerySizeColumn) + } + if (mItemFetchScope.fetchMTime()) { + ADD_COLUMN(PimItem::datetimeFullColumnName(), ItemQueryDatetimeColumn) + } + ADD_COLUMN(PimItem::collectionIdFullColumnName(), ItemQueryCollectionIdColumn) + if (mItemFetchScope.fetchGID()) { + ADD_COLUMN(PimItem::gidFullColumnName(), ItemQueryPimItemGidColumn) + } +#undef ADD_COLUMN + + itemQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + ItemQueryHelper::scopeToQuery(mScope, mContext, itemQuery); + + if (mItemFetchScope.changedSince().isValid()) { + itemQuery.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mItemFetchScope.changedSince().toUTC()); + } + + if (!itemQuery.exec()) { + throw HandlerException("Unable to list items"); + } + + itemQuery.query().next(); + + return itemQuery.query(); +} + +enum FlagQueryColumns { + FlagQueryPimItemIdColumn, + FlagQueryFlagIdColumn, +}; + +QSqlQuery ItemFetchHelper::buildFlagQuery() +{ + QueryBuilder flagQuery(PimItem::tableName()); + flagQuery.addJoin(QueryBuilder::InnerJoin, PimItemFlagRelation::tableName(), PimItem::idFullColumnName(), PimItemFlagRelation::leftFullColumnName()); + flagQuery.addColumn(PimItem::idFullColumnName()); + flagQuery.addColumn(PimItemFlagRelation::rightFullColumnName()); + + ItemQueryHelper::scopeToQuery(mScope, mContext, flagQuery); + flagQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!flagQuery.exec()) { + throw HandlerException("Unable to retrieve item flags"); + } + + flagQuery.query().next(); + + return flagQuery.query(); +} + +enum TagQueryColumns { + TagQueryItemIdColumn, + TagQueryTagIdColumn, +}; + +QSqlQuery ItemFetchHelper::buildTagQuery() +{ + QueryBuilder tagQuery(PimItem::tableName()); + tagQuery.addJoin(QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName()); + tagQuery.addJoin(QueryBuilder::InnerJoin, Tag::tableName(), Tag::idFullColumnName(), PimItemTagRelation::rightFullColumnName()); + tagQuery.addColumn(PimItem::idFullColumnName()); + tagQuery.addColumn(Tag::idFullColumnName()); + + ItemQueryHelper::scopeToQuery(mScope, mContext, tagQuery); + tagQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!tagQuery.exec()) { + throw HandlerException("Unable to retrieve item tags"); + } + + tagQuery.query().next(); + + return tagQuery.query(); +} + +enum VRefQueryColumns { + VRefQueryCollectionIdColumn, + VRefQueryItemIdColumn, +}; + +QSqlQuery ItemFetchHelper::buildVRefQuery() +{ + QueryBuilder vRefQuery(PimItem::tableName()); + vRefQuery.addJoin(QueryBuilder::LeftJoin, + CollectionPimItemRelation::tableName(), + CollectionPimItemRelation::rightFullColumnName(), + PimItem::idFullColumnName()); + vRefQuery.addColumn(CollectionPimItemRelation::leftFullColumnName()); + vRefQuery.addColumn(CollectionPimItemRelation::rightFullColumnName()); + ItemQueryHelper::scopeToQuery(mScope, mContext, vRefQuery); + vRefQuery.addSortColumn(PimItem::idFullColumnName(), Query::Descending); + + if (!vRefQuery.exec()) { + throw HandlerException("Unable to retrieve virtual references"); + } + + vRefQuery.query().next(); + + return vRefQuery.query(); +} + +bool ItemFetchHelper::isScopeLocal(const Scope &scope) +{ + // The only agent allowed to override local scope is the Baloo Indexer + if (!mConnection->sessionId().startsWith("akonadi_indexing_agent")) { + return false; + } + + // Get list of all resources that own all items in the scope + QueryBuilder qb(PimItem::tableName(), QueryBuilder::Select); + qb.setDistinct(true); + qb.addColumn(Resource::nameFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); + ItemQueryHelper::scopeToQuery(scope, mContext, qb); + if (mContext.resource().isValid()) { + qb.addValueCondition(Resource::nameFullColumnName(), Query::NotEquals, mContext.resource().name()); + } + + if (!qb.exec()) { + throw HandlerException("Failed to query database"); + return false; + } + + // If there is more than one resource, i.e. this is a fetch from multiple + // collections, then don't bother and just return FALSE. This case is aimed + // specifically on Baloo, which fetches items from each collection independently, + // so it will pass this check. + QSqlQuery query = qb.query(); + if (query.size() != 1) { + return false; + } + + query.next(); + const QString resourceName = query.value(0).toString(); + query.finish(); + + org::freedesktop::Akonadi::AgentManager manager(DBus::serviceName(DBus::Control), QStringLiteral("/AgentManager"), QDBusConnection::sessionBus()); + const QString typeIdentifier = manager.agentInstanceType(resourceName); + const QVariantMap properties = manager.agentCustomProperties(typeIdentifier); + return properties.value(QStringLiteral("HasLocalStorage"), false).toBool(); +} + +DataStore *ItemFetchHelper::storageBackend() const +{ + if (mConnection) { + if (auto store = mConnection->storageBackend()) { + return store; + } + } + + return DataStore::self(); +} + +bool ItemFetchHelper::fetchItems(std::function &&itemCallback) +{ + BEGIN_TIMER(fetch) + + // retrieve missing parts + // HACK: isScopeLocal() is a workaround for resources that have cache expiration + // because when the cache expires, Baloo is not able to content of the items. So + // we allow fetch of items that belong to local resources (like maildir) to ignore + // cacheOnly and retrieve missing parts from the resource. However ItemRetriever + // is painfully slow with many items and is generally designed to fetch a few + // messages, not all of them. In the long term, we need a better way to do this. + BEGIN_TIMER(itemRetriever) + BEGIN_TIMER(scopeLocal) +#if ENABLE_FETCH_PROFILING + double scopeLocalElapsed = 0; +#endif + if (!mItemFetchScope.cacheOnly() || isScopeLocal(mScope)) { +#if ENABLE_FETCH_PROFILING + scopeLocalElapsed = scopeLocalTimer.elapsed(); +#endif + + // trigger a collection sync if configured to do so + triggerOnDemandFetch(); + + // Prepare for a call to ItemRetriever::exec(); + // From a resource perspective the only parts that can be fetched are payloads. + ItemRetriever retriever(mAkonadi.itemRetrievalManager(), mConnection, mContext); + retriever.setScope(mScope); + retriever.setRetrieveParts(mItemFetchScope.requestedPayloads()); + retriever.setRetrieveFullPayload(mItemFetchScope.fullPayload()); + retriever.setChangedSince(mItemFetchScope.changedSince()); + if (!retriever.exec() && !mItemFetchScope.ignoreErrors()) { // There we go, retrieve the missing parts from the resource. + if (mContext.resource().isValid()) { + throw HandlerException(QStringLiteral("Unable to fetch item from backend (collection %1, resource %2) : %3") + .arg(mContext.collectionId()) + .arg(mContext.resource().id()) + .arg(QString::fromLatin1(retriever.lastError()))); + } else { + throw HandlerException(QStringLiteral("Unable to fetch item from backend (collection %1) : %2") + .arg(mContext.collectionId()) + .arg(QString::fromLatin1(retriever.lastError()))); + } + } + } + END_TIMER(itemRetriever) + + BEGIN_TIMER(items) + QSqlQuery itemQuery = buildItemQuery(); + END_TIMER(items) + + // error if query did not find any item and scope is not listing items but + // a request for a specific item + if (!itemQuery.isValid()) { + if (mItemFetchScope.ignoreErrors()) { + return true; + } + switch (mScope.scope()) { + case Scope::Uid: // fall through + case Scope::Rid: // fall through + case Scope::HierarchicalRid: // fall through + case Scope::Gid: + throw HandlerException("Item query returned empty result set"); + break; + default: + break; + } + } + // build part query if needed + BEGIN_TIMER(parts) + QSqlQuery partQuery(storageBackend()->database()); + if (!mItemFetchScope.requestedParts().isEmpty() || mItemFetchScope.fullPayload() || mItemFetchScope.allAttributes()) { + partQuery = buildPartQuery(mItemFetchScope.requestedParts(), mItemFetchScope.fullPayload(), mItemFetchScope.allAttributes()); + } + END_TIMER(parts) + + // build flag query if needed + BEGIN_TIMER(flags) + QSqlQuery flagQuery(storageBackend()->database()); + if (mItemFetchScope.fetchFlags()) { + flagQuery = buildFlagQuery(); + } + END_TIMER(flags) + + // build tag query if needed + BEGIN_TIMER(tags) + QSqlQuery tagQuery(storageBackend()->database()); + if (mItemFetchScope.fetchTags()) { + tagQuery = buildTagQuery(); + } + END_TIMER(tags) + + BEGIN_TIMER(vRefs) + QSqlQuery vRefQuery(storageBackend()->database()); + if (mItemFetchScope.fetchVirtualReferences()) { + vRefQuery = buildVRefQuery(); + } + END_TIMER(vRefs) + +#if ENABLE_FETCH_PROFILING + int itemsCount = 0; + int flagsCount = 0; + int partsCount = 0; + int tagsCount = 0; + int vRefsCount = 0; +#endif + + BEGIN_TIMER(processing) + QHash flagIdNameCache; + QHash mimeTypeIdNameCache; + QHash partTypeIdNameCache; + while (itemQuery.isValid()) { + PROF_INC(itemsCount) + + const qint64 pimItemId = extractQueryResult(itemQuery, ItemQueryPimItemIdColumn).toLongLong(); + const int pimItemRev = extractQueryResult(itemQuery, ItemQueryRevColumn).toInt(); + + Protocol::FetchItemsResponse response; + response.setId(pimItemId); + response.setRevision(pimItemRev); + const qint64 mimeTypeId = extractQueryResult(itemQuery, ItemQueryMimeTypeIdColumn).toLongLong(); + auto mtIter = mimeTypeIdNameCache.find(mimeTypeId); + if (mtIter == mimeTypeIdNameCache.end()) { + mtIter = mimeTypeIdNameCache.insert(mimeTypeId, MimeType::retrieveById(mimeTypeId).name()); + } + response.setMimeType(mtIter.value()); + if (mItemFetchScope.fetchRemoteId()) { + response.setRemoteId(extractQueryResult(itemQuery, ItemQueryPimItemRidColumn).toString()); + } + response.setParentId(extractQueryResult(itemQuery, ItemQueryCollectionIdColumn).toLongLong()); + + if (mItemFetchScope.fetchSize()) { + response.setSize(extractQueryResult(itemQuery, ItemQuerySizeColumn).toLongLong()); + } + if (mItemFetchScope.fetchMTime()) { + response.setMTime(Utils::variantToDateTime(extractQueryResult(itemQuery, ItemQueryDatetimeColumn))); + } + if (mItemFetchScope.fetchRemoteRevision()) { + response.setRemoteRevision(extractQueryResult(itemQuery, ItemQueryRemoteRevisionColumn).toString()); + } + if (mItemFetchScope.fetchGID()) { + response.setGid(extractQueryResult(itemQuery, ItemQueryPimItemGidColumn).toString()); + } + + if (mItemFetchScope.fetchFlags()) { + QVector flags; + while (flagQuery.isValid()) { + const qint64 id = flagQuery.value(FlagQueryPimItemIdColumn).toLongLong(); + if (id > pimItemId) { + flagQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + const qint64 flagId = flagQuery.value(FlagQueryFlagIdColumn).toLongLong(); + auto flagNameIter = flagIdNameCache.find(flagId); + if (flagNameIter == flagIdNameCache.end()) { + flagNameIter = flagIdNameCache.insert(flagId, Flag::retrieveById(flagId).name().toUtf8()); + } + flags << flagNameIter.value(); + flagQuery.next(); + } + response.setFlags(flags); + } + + if (mItemFetchScope.fetchTags()) { + QVector tagIds; + QVector tags; + while (tagQuery.isValid()) { + PROF_INC(tagsCount) + const qint64 id = tagQuery.value(TagQueryItemIdColumn).toLongLong(); + if (id > pimItemId) { + tagQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + tagIds << tagQuery.value(TagQueryTagIdColumn).toLongLong(); + tagQuery.next(); + } + + if (mTagFetchScope.fetchIdOnly()) { + tags = tagIds | Views::transform([](const auto tagId) { + Protocol::FetchTagsResponse resp; + resp.setId(tagId); + return resp; + }) + | Actions::toQVector; + } else { + tags = tagIds | Views::transform([this](const auto tagId) { + return HandlerHelper::fetchTagsResponse(Tag::retrieveById(tagId), mTagFetchScope, mConnection); + }) + | Actions::toQVector; + } + response.setTags(tags); + } + + if (mItemFetchScope.fetchVirtualReferences()) { + QVector vRefs; + while (vRefQuery.isValid()) { + PROF_INC(vRefsCount) + const qint64 id = vRefQuery.value(VRefQueryItemIdColumn).toLongLong(); + if (id > pimItemId) { + vRefQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + vRefs << vRefQuery.value(VRefQueryCollectionIdColumn).toLongLong(); + vRefQuery.next(); + } + response.setVirtualReferences(vRefs); + } + + if (mItemFetchScope.fetchRelations()) { + SelectQueryBuilder qb; + Query::Condition condition; + condition.setSubQueryMode(Query::Or); + condition.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, pimItemId); + condition.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, pimItemId); + qb.addCondition(condition); + qb.addGroupColumns(QStringList() << Relation::leftIdColumn() << Relation::rightIdColumn() << Relation::typeIdColumn() + << Relation::remoteIdColumn()); + if (!qb.exec()) { + throw HandlerException("Unable to list item relations"); + } + QVector relations; + const auto result = qb.result(); + relations.reserve(result.size()); + for (const Relation &rel : result) { + relations.push_back(HandlerHelper::fetchRelationsResponse(rel)); + ; + } + response.setRelations(relations); + } + + if (mItemFetchScope.ancestorDepth() != Protocol::ItemFetchScope::NoAncestor) { + response.setAncestors(ancestorsForItem(response.parentId())); + } + + bool skipItem = false; + + QVector cachedParts; + QVector parts; + while (partQuery.isValid()) { + PROF_INC(partsCount) + const qint64 id = partQuery.value(PartQueryPimIdColumn).toLongLong(); + if (id > pimItemId) { + partQuery.next(); + continue; + } else if (id < pimItemId) { + break; + } + + const qint64 partTypeId = partQuery.value(PartQueryTypeIdColumn).toLongLong(); + auto ptIter = partTypeIdNameCache.find(partTypeId); + if (ptIter == partTypeIdNameCache.end()) { + ptIter = partTypeIdNameCache.insert(partTypeId, PartTypeHelper::fullName(PartType::retrieveById(partTypeId)).toUtf8()); + } + Protocol::PartMetaData metaPart; + Protocol::StreamPayloadResponse partData; + partData.setPayloadName(ptIter.value()); + metaPart.setName(ptIter.value()); + metaPart.setVersion(partQuery.value(PartQueryVersionColumn).toInt()); + metaPart.setSize(partQuery.value(PartQueryDataSizeColumn).toLongLong()); + + const QByteArray data = Utils::variantToByteArray(partQuery.value(PartQueryDataColumn)); + if (mItemFetchScope.checkCachedPayloadPartsOnly()) { + if (!data.isEmpty()) { + cachedParts << ptIter.value(); + } + partQuery.next(); + } else { + if (mItemFetchScope.ignoreErrors() && data.isEmpty()) { + // We wanted the payload, couldn't get it, and are ignoring errors. Skip the item. + // This is not an error though, it's fine to have empty payload parts (to denote existing but not cached parts) + qCDebug(AKONADISERVER_LOG) << "item" << id << "has an empty payload part in parttable for part" << metaPart.name(); + skipItem = true; + break; + } + metaPart.setStorageType(static_cast(partQuery.value(PartQueryStorageColumn).toInt())); + if (data.isEmpty()) { + partData.setData(QByteArray("")); + } else { + partData.setData(data); + } + partData.setMetaData(metaPart); + + if (mItemFetchScope.requestedParts().contains(ptIter.value()) || mItemFetchScope.fullPayload() || mItemFetchScope.allAttributes()) { + parts.append(partData); + } + + partQuery.next(); + } + } + response.setParts(parts); + + if (skipItem) { + itemQuery.next(); + continue; + } + + if (mItemFetchScope.checkCachedPayloadPartsOnly()) { + response.setCachedParts(cachedParts); + } + + if (itemCallback) { + itemCallback(std::move(response)); + } else { + mConnection->sendResponse(std::move(response)); + } + + itemQuery.next(); + } + tagQuery.finish(); + flagQuery.finish(); + partQuery.finish(); + vRefQuery.finish(); + itemQuery.finish(); + END_TIMER(processing) + + // update atime (only if the payload was actually requested, otherwise a simple resource sync prevents cache clearing) + BEGIN_TIMER(aTime) + if (mUpdateATimeEnabled && (needsAccessTimeUpdate(mItemFetchScope.requestedParts()) || mItemFetchScope.fullPayload())) { + updateItemAccessTime(); + } + END_TIMER(aTime) + + END_TIMER(fetch) +#if ENABLE_FETCH_PROFILING + qCDebug(AKONADISERVER_LOG) << "ItemFetchHelper execution stats:"; + qCDebug(AKONADISERVER_LOG) << "\tItems query:" << itemsElapsed << "ms," << itemsCount << " items in total"; + qCDebug(AKONADISERVER_LOG) << "\tFlags query:" << flagsElapsed << "ms, " << flagsCount << " flags in total"; + qCDebug(AKONADISERVER_LOG) << "\tParts query:" << partsElapsed << "ms, " << partsCount << " parts in total"; + qCDebug(AKONADISERVER_LOG) << "\tTags query: " << tagsElapsed << "ms, " << tagsCount << " tags in total"; + qCDebug(AKONADISERVER_LOG) << "\tVRefs query:" << vRefsElapsed << "ms, " << vRefsCount << " vRefs in total"; + qCDebug(AKONADISERVER_LOG) << "\t------------"; + qCDebug(AKONADISERVER_LOG) << "\tItem retriever:" << itemRetrieverElapsed << "ms (scope local:" << scopeLocalElapsed << "ms)"; + qCDebug(AKONADISERVER_LOG) << "\tTotal query:" << (itemsElapsed + flagsElapsed + partsElapsed + tagsElapsed + vRefsElapsed) << "ms"; + qCDebug(AKONADISERVER_LOG) << "\tTotal processing: " << processingElapsed << "ms"; + qCDebug(AKONADISERVER_LOG) << "\tATime update:" << aTimeElapsed << "ms"; + qCDebug(AKONADISERVER_LOG) << "\t============"; + qCDebug(AKONADISERVER_LOG) << "\tTotal FETCH:" << fetchElapsed << "ms"; + qCDebug(AKONADISERVER_LOG); + qCDebug(AKONADISERVER_LOG); +#endif + + return true; +} + +bool ItemFetchHelper::needsAccessTimeUpdate(const QVector &parts) +{ + // TODO technically we should compare the part list with the cache policy of + // the parent collection of the retrieved items, but that's kinda expensive + // Only updating the atime if the full payload was requested is a good + // approximation though. + return parts.contains(AKONADI_PARAM_PLD_RFC822); +} + +void ItemFetchHelper::updateItemAccessTime() +{ + Transaction transaction(storageBackend(), QStringLiteral("update atime")); + QueryBuilder qb(PimItem::tableName(), QueryBuilder::Update); + qb.setColumnValue(PimItem::atimeColumn(), QDateTime::currentDateTimeUtc()); + ItemQueryHelper::scopeToQuery(mScope, mContext, qb); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Unable to update item access time"; + } else { + transaction.commit(); + } +} + +void ItemFetchHelper::triggerOnDemandFetch() +{ + if (mContext.collectionId() <= 0 || mItemFetchScope.cacheOnly()) { + return; + } + + Collection collection = mContext.collection(); + + // HACK: don't trigger on-demand syncing if the resource is the one triggering it + if (mConnection->sessionId() == collection.resource().name().toLatin1()) { + return; + } + + storageBackend()->activeCachePolicy(collection); + if (!collection.cachePolicySyncOnDemand()) { + return; + } + + mConnection->akonadi().intervalChecker().requestCollectionSync(collection); +} + +QVector ItemFetchHelper::ancestorsForItem(Collection::Id parentColId) +{ + if (mItemFetchScope.ancestorDepth() == Protocol::ItemFetchScope::NoAncestor || parentColId == 0) { + return QVector(); + } + const auto it = mAncestorCache.constFind(parentColId); + if (it != mAncestorCache.cend()) { + return *it; + } + + QVector ancestors; + Collection col = Collection::retrieveById(parentColId); + const int depthNum = mItemFetchScope.ancestorDepth() == Protocol::ItemFetchScope::ParentAncestor ? 1 : INT_MAX; + for (int i = 0; i < depthNum; ++i) { + if (!col.isValid()) { + Protocol::Ancestor ancestor; + ancestor.setId(0); + ancestors << ancestor; + break; + } + Protocol::Ancestor ancestor; + ancestor.setId(col.id()); + ancestor.setRemoteId(col.remoteId()); + ancestors << ancestor; + col = col.parent(); + } + mAncestorCache.insert(parentColId, ancestors); + return ancestors; +} + +QVariant ItemFetchHelper::extractQueryResult(const QSqlQuery &query, ItemFetchHelper::ItemQueryColumns column) const +{ + const int colId = mItemQueryColumnMap[column]; + Q_ASSERT(colId >= 0); + return query.value(colId); +} diff --git a/src/server/handler/itemfetchhelper.h b/src/server/handler/itemfetchhelper.h new file mode 100644 index 0000000..6c9e95f --- /dev/null +++ b/src/server/handler/itemfetchhelper.h @@ -0,0 +1,93 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006-2009 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "commandcontext.h" +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/itemretriever.h" + +#include +#include +#include + +#include + +class ItemFetchHelperTest; + +namespace Akonadi +{ +namespace Server +{ +class Connection; +class AkonadiServer; + +class ItemFetchHelper +{ +public: + ItemFetchHelper(Connection *connection, + const Scope &scope, + const Protocol::ItemFetchScope &itemFetchScope, + const Protocol::TagFetchScope &tagFagScope, + AkonadiServer &akonadi); + ItemFetchHelper(Connection *connection, + const CommandContext &context, + const Scope &scope, + const Protocol::ItemFetchScope &itemFetchScope, + const Protocol::TagFetchScope &tagFetchScope, + AkonadiServer &akonadi); + + bool fetchItems(std::function &&callback = {}); + + void disableATimeUpdates(); + +private: + enum ItemQueryColumns { + ItemQueryPimItemIdColumn, + ItemQueryPimItemRidColumn, + ItemQueryMimeTypeIdColumn, + ItemQueryRevColumn, + ItemQueryRemoteRevisionColumn, + ItemQuerySizeColumn, + ItemQueryDatetimeColumn, + ItemQueryCollectionIdColumn, + ItemQueryPimItemGidColumn, + ItemQueryColumnCount + }; + + void updateItemAccessTime(); + void triggerOnDemandFetch(); + QSqlQuery buildItemQuery(); + QSqlQuery buildPartQuery(const QVector &partList, bool allPayload, bool allAttrs); + QSqlQuery buildFlagQuery(); + QSqlQuery buildTagQuery(); + QSqlQuery buildVRefQuery(); + + QVector ancestorsForItem(Collection::Id parentColId); + static bool needsAccessTimeUpdate(const QVector &parts); + QVariant extractQueryResult(const QSqlQuery &query, ItemQueryColumns column) const; + bool isScopeLocal(const Scope &scope); + DataStore *storageBackend() const; + static QByteArray relationsToByteArray(const Relation::List &relations); + +private: + Connection *mConnection = nullptr; + const CommandContext &mContext; + QHash> mAncestorCache; + Scope mScope; + Protocol::ItemFetchScope mItemFetchScope; + Protocol::TagFetchScope mTagFetchScope; + int mItemQueryColumnMap[ItemQueryColumnCount]; + bool mUpdateATimeEnabled = true; + AkonadiServer &mAkonadi; + + friend class ::ItemFetchHelperTest; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemlinkhandler.cpp b/src/server/handler/itemlinkhandler.cpp new file mode 100644 index 0000000..60ffe19 --- /dev/null +++ b/src/server/handler/itemlinkhandler.cpp @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemlinkhandler.h" + +#include "akonadiserver_debug.h" +#include "connection.h" +#include "handlerhelper.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +ItemLinkHandler::ItemLinkHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ItemLinkHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + const Collection collection = HandlerHelper::collectionFromScope(cmd.destination(), connection()->context()); + if (!collection.isVirtual()) { + return failureResponse(QStringLiteral("Can't link items to non-virtual collections")); + } + + /* FIXME BIN + Resource originalContext; + Scope::SelectionScope itemSelectionScope = Scope::selectionScopeFromByteArray(m_streamParser->peekString()); + if (itemSelectionScope != Scope::None) { + m_streamParser->readString(); + // Unset Resource context if destination collection is specified using HRID/RID, + // because otherwise the Resource context is relative to the destination collection + // instead of the source collection (collection context) + if ((mDestinationScope.scope() == Scope::HierarchicalRid || mDestinationScope.scope() == Scope::Rid) && itemSelectionScope == Scope::Rid) { + originalContext = connection()->context()->resource(); + connection()->context()->setResource(Resource()); + } + } + Scope itemScope(itemSelectionScope); + itemScope.parseScope(m_streamParser); + */ + + SelectQueryBuilder qb; + ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); + + /* + if (originalContext.isValid()) { + connection()->context()->setResource(originalContext); + } + */ + + if (!qb.exec()) { + return failureResponse(QStringLiteral("Unable to execute item query")); + } + + const PimItem::List items = qb.result(); + const bool createLinks = (cmd.action() == Protocol::LinkItemsCommand::Link); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store, createLinks ? QStringLiteral("LINK") : QStringLiteral("UNLINK")); + + PimItem::List toLink; + PimItem::List toUnlink; + for (const PimItem &item : items) { + const bool alreadyLinked = collection.relatesToPimItem(item); + bool result = true; + if (createLinks && !alreadyLinked) { + result = collection.addPimItem(item); + toLink << item; + } else if (!createLinks && alreadyLinked) { + result = collection.removePimItem(item); + toUnlink << item; + } + if (!result) { + return failureResponse(QStringLiteral("Failed to modify item reference")); + } + } + + if (!transaction.commit()) { + return failureResponse(QStringLiteral("Cannot commit transaction.")); + } + + if (!toLink.isEmpty()) { + store->notificationCollector()->itemsLinked(toLink, collection); + } else if (!toUnlink.isEmpty()) { + store->notificationCollector()->itemsUnlinked(toUnlink, collection); + } + + return successResponse(); +} diff --git a/src/server/handler/itemlinkhandler.h b/src/server/handler/itemlinkhandler.h new file mode 100644 index 0000000..ad92b8e --- /dev/null +++ b/src/server/handler/itemlinkhandler.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + * @ingroup akonadi_server_handler + * + * Handler for the LINK and UNLINK commands. + * + * These commands are used to add and remove references of a set of items to a + * virtual collection. + */ +class ItemLinkHandler : public Handler +{ +public: + ItemLinkHandler(AkonadiServer &akonadi); + ~ItemLinkHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemmodifyhandler.cpp b/src/server/handler/itemmodifyhandler.cpp new file mode 100644 index 0000000..7c3f02d --- /dev/null +++ b/src/server/handler/itemmodifyhandler.cpp @@ -0,0 +1,384 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "itemmodifyhandler.h" + +#include "connection.h" +#include "handlerhelper.h" +#include "storage/datastore.h" +#include "storage/dbconfig.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretriever.h" +#include "storage/parthelper.h" +#include "storage/partstreamer.h" +#include "storage/parttypehelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" +#include +#include + +#include "akonadiserver_debug.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +static bool payloadChanged(const QSet &changes) +{ + return changes | AkRanges::Actions::any([](const auto &change) { + return change.startsWith(AKONADI_PARAM_PLD); + }); +} + +ItemModifyHandler::ItemModifyHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ItemModifyHandler::replaceFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) +{ + Flag::List flagList = HandlerHelper::resolveFlags(flags); + DataStore *store = connection()->storageBackend(); + + // TODO: why doesn't this have the "Make sure we don't overwrite some local-only flags" code that itemcreatehandler has? + if (!store->setItemsFlags(items, nullptr, flagList, &flagsChanged)) { + qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceFlags: Unable to replace flags"; + return false; + } + + return true; +} + +bool ItemModifyHandler::addFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) +{ + const Flag::List flagList = HandlerHelper::resolveFlags(flags); + DataStore *store = connection()->storageBackend(); + + if (!store->appendItemsFlags(items, flagList, &flagsChanged)) { + qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addFlags: Unable to add new item flags"; + return false; + } + return true; +} + +bool ItemModifyHandler::deleteFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged) +{ + DataStore *store = connection()->storageBackend(); + + QVector flagList; + flagList.reserve(flags.size()); + for (auto iter = flags.cbegin(), end = flags.cend(); iter != end; ++iter) { + Flag flag = Flag::retrieveByName(QString::fromUtf8(*iter)); + if (!flag.isValid()) { + continue; + } + + flagList.append(flag); + } + + if (!store->removeItemsFlags(items, flagList, &flagsChanged)) { + qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteFlags: Unable to remove item flags"; + return false; + } + return true; +} + +bool ItemModifyHandler::replaceTags(const PimItem::List &item, const Scope &tags, bool &tagsChanged) +{ + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context()); + if (!connection()->storageBackend()->setItemsTags(item, tagList, &tagsChanged)) { + qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::replaceTags: unable to replace tags"; + return false; + } + return true; +} + +bool ItemModifyHandler::addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged) +{ + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context()); + if (!connection()->storageBackend()->appendItemsTags(items, tagList, &tagsChanged)) { + qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::addTags: Unable to add new item tags"; + return false; + } + return true; +} + +bool ItemModifyHandler::deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged) +{ + const Tag::List tagList = HandlerHelper::tagsFromScope(tags, connection()->context()); + if (!connection()->storageBackend()->removeItemsTags(items, tagList, &tagsChanged)) { + qCWarning(AKONADISERVER_LOG) << "ItemModifyHandler::deleteTags: Unable to remove item tags"; + return false; + } + return true; +} + +bool ItemModifyHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + // parseCommand(); + + DataStore *store = connection()->storageBackend(); + Transaction transaction(store, QStringLiteral("STORE")); + ExternalPartStorageTransaction storageTrx; + // Set the same modification time for each item. + QDateTime modificationtime = QDateTime::currentDateTimeUtc(); + if (DbType::type(store->database()) != DbType::Sqlite) { + // Remove milliseconds from the modificationtime. PSQL and MySQL don't + // support milliseconds in DATETIME column, so FETCHed Items will report + // time without milliseconds, while this command would return answer + // with milliseconds + modificationtime = modificationtime.addMSecs(-modificationtime.time().msec()); + } + + // retrieve selected items + SelectQueryBuilder qb; + qb.setForUpdate(); + ItemQueryHelper::scopeToQuery(cmd.items(), connection()->context(), qb); + if (!qb.exec()) { + return failureResponse("Unable to retrieve items"); + } + PimItem::List pimItems = qb.result(); + if (pimItems.isEmpty()) { + return failureResponse("No items found"); + } + + for (int i = 0; i < pimItems.size(); ++i) { + if (cmd.oldRevision() > -1) { + // check for conflicts if a resources tries to overwrite an item with dirty payload + const PimItem &pimItem = pimItems.at(i); + if (connection()->isOwnerResource(pimItem)) { + if (pimItem.dirty()) { + const QString error = + QStringLiteral("[LRCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with dirty payload, aborting STORE."); + return failureResponse( + error.arg(pimItem.collection().resource().name()).arg(pimItem.id()).arg(pimItem.remoteId()).arg(pimItem.collectionId())); + } + } + + // check and update revisions + if (pimItem.rev() != cmd.oldRevision()) { + const QString error = QStringLiteral( + "[LLCONFLICT] Resource %1 tries to modify item %2 (%3) (in collection %4) with revision %5; the item was modified elsewhere and has " + "revision %6, aborting STORE."); + return failureResponse(error.arg(pimItem.collection().resource().name()) + .arg(pimItem.id()) + .arg(pimItem.remoteId()) + .arg(pimItem.collectionId()) + .arg(cmd.oldRevision()) + .arg(pimItems.at(i).rev())); + } + } + } + + PimItem &item = pimItems.first(); + + QSet changes; + qint64 partSizes = 0; + qint64 size = 0; + + bool flagsChanged = false; + bool tagsChanged = false; + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedFlags) { + if (!addFlags(pimItems, cmd.addedFlags(), flagsChanged)) { + return failureResponse("Unable to add item flags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedFlags) { + if (!deleteFlags(pimItems, cmd.removedFlags(), flagsChanged)) { + return failureResponse("Unable to remove item flags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Flags) { + if (!replaceFlags(pimItems, cmd.flags(), flagsChanged)) { + return failureResponse("Unable to reset flags"); + } + } + + if (flagsChanged) { + changes << AKONADI_PARAM_FLAGS; + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::AddedTags) { + if (!addTags(pimItems, cmd.addedTags(), tagsChanged)) { + return failureResponse("Unable to add item tags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedTags) { + if (!deleteTags(pimItems, cmd.removedTags(), tagsChanged)) { + return failureResponse("Unable to remove item tags"); + } + } + + if (cmd.modifiedParts() & Protocol::ModifyItemsCommand::Tags) { + if (!replaceTags(pimItems, cmd.tags(), tagsChanged)) { + return failureResponse("Unable to reset item tags"); + } + } + + if (tagsChanged) { + changes << AKONADI_PARAM_TAGS; + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteID) { + if (item.remoteId() != cmd.remoteId() && !cmd.remoteId().isEmpty()) { + if (!connection()->isOwnerResource(item)) { + qCWarning(AKONADISERVER_LOG) << "Invalid attempt to modify the remoteID for item" << item.id() << "from" << item.remoteId() << "to" + << cmd.remoteId(); + return failureResponse("Only resources can modify remote identifiers"); + } + item.setRemoteId(cmd.remoteId()); + changes << AKONADI_PARAM_REMOTEID; + } + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::GID) { + if (item.gid() != cmd.gid()) { + item.setGid(cmd.gid()); + } + changes << AKONADI_PARAM_GID; + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemoteRevision) { + if (item.remoteRevision() != cmd.remoteRevision()) { + if (!connection()->isOwnerResource(item)) { + return failureResponse("Only resources can modify remote revisions"); + } + item.setRemoteRevision(cmd.remoteRevision()); + changes << AKONADI_PARAM_REMOTEREVISION; + } + } + + if (item.isValid() && !cmd.dirty()) { + item.setDirty(false); + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Size) { + size = cmd.itemSize(); + changes << AKONADI_PARAM_SIZE; + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::RemovedParts) { + if (!cmd.removedParts().isEmpty()) { + if (!store->removeItemParts(item, cmd.removedParts())) { + return failureResponse("Unable to remove item parts"); + } + Q_FOREACH (const QByteArray &part, cmd.removedParts()) { + changes.insert(part); + } + } + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Parts) { + PartStreamer streamer(connection(), item); + Q_FOREACH (const QByteArray &partName, cmd.parts()) { + qint64 partSize = 0; + try { + streamer.stream(true, partName, partSize); + } catch (const PartStreamerException &e) { + return failureResponse(e.what()); + } + + changes.insert(partName); + partSizes += partSize; + } + } + + if (item.isValid() && cmd.modifiedParts() & Protocol::ModifyItemsCommand::Attributes) { + PartStreamer streamer(connection(), item); + const Protocol::Attributes attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + bool changed = false; + try { + streamer.streamAttribute(true, iter.key(), iter.value(), &changed); + } catch (const PartStreamerException &e) { + return failureResponse(e.what()); + } + + if (changed) { + changes.insert(iter.key()); + } + } + } + + QDateTime datetime; + if (!changes.isEmpty() || cmd.invalidateCache() || !cmd.dirty()) { + // update item size + if (pimItems.size() == 1 && (size > 0 || partSizes > 0)) { + pimItems.first().setSize(qMax(size, partSizes)); + } + + const bool onlyRemoteIdChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEID)); + const bool onlyRemoteRevisionChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_REMOTEREVISION)); + const bool onlyRemoteIdAndRevisionChanged = + (changes.size() == 2 && changes.contains(AKONADI_PARAM_REMOTEID) && changes.contains(AKONADI_PARAM_REMOTEREVISION)); + const bool onlyFlagsChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_FLAGS)); + const bool onlyGIDChanged = (changes.size() == 1 && changes.contains(AKONADI_PARAM_GID)); + // If only the remote id and/or the remote revision changed, we don't have to increase the REV, + // because these updates do not change the payload and can only be done by the owning resource -> no conflicts possible + const bool revisionNeedsUpdate = + (!changes.isEmpty() && !onlyRemoteIdChanged && !onlyRemoteRevisionChanged && !onlyRemoteIdAndRevisionChanged && !onlyGIDChanged); + + // run update query and prepare change notifications + for (int i = 0; i < pimItems.count(); ++i) { + PimItem &item = pimItems[i]; + if (revisionNeedsUpdate) { + item.setRev(item.rev() + 1); + } + + item.setDatetime(modificationtime); + item.setAtime(modificationtime); + if (!connection()->isOwnerResource(item) && payloadChanged(changes)) { + item.setDirty(true); + } + if (!item.update()) { + return failureResponse("Unable to write item changes into the database"); + } + + if (cmd.invalidateCache()) { + if (!store->invalidateItemCache(item)) { + return failureResponse("Unable to invalidate item cache in the database"); + } + } + + // flags change notification went separately during command parsing + // GID-only changes are ignored to prevent resources from updating their storage when no actual change happened + if (cmd.notify() && !changes.isEmpty() && !onlyFlagsChanged && !onlyGIDChanged) { + // Don't send FLAGS notification in itemChanged + changes.remove(AKONADI_PARAM_FLAGS); + store->notificationCollector()->itemChanged(item, changes); + } + + if (!cmd.noResponse()) { + Protocol::ModifyItemsResponse resp; + resp.setId(item.id()); + resp.setNewRevision(item.rev()); + sendResponse(std::move(resp)); + } + } + + if (!transaction.commit()) { + return failureResponse("Cannot commit transaction."); + } + // Always commit storage changes (deletion) after DB transaction + storageTrx.commit(); + + datetime = modificationtime; + } else { + datetime = pimItems.first().datetime(); + } + + Protocol::ModifyItemsResponse resp; + resp.setModificationDateTime(datetime); + return successResponse(std::move(resp)); +} diff --git a/src/server/handler/itemmodifyhandler.h b/src/server/handler/itemmodifyhandler.h new file mode 100644 index 0000000..5e46f05 --- /dev/null +++ b/src/server/handler/itemmodifyhandler.h @@ -0,0 +1,73 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2009 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "entities.h" +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the item modification command. + +

Semantics

+ Modifies the selected items. Item selection can happen within the usual three scopes: + - based on a uid set relative to the currently selected collection + - based on a uid set (UID) + - based on a list of remote identifiers within the currently selected collection (RID) + + The following item properties can be modified: + - the remote identifier (@c REMOTEID) + - the remote revision (@c REMOTEREVISION) + - the global identifier (@c GID) + - resetting the dirty flag indication local changes not yet replicated to the backend (@c DIRTY) + - adding/deleting/setting item flags (@c FLAGS) + - setting the item size hint (@c SIZE) + - changing item attributes + - changing item payload parts + + If multiple items are selected only the following operations are valid: + - adding flags + - removing flags + - settings flags + + The following operations are only allowed by resources: + - resetting the dirty flag + - invalidating the cache + - modifying the remote identifier + - modifying the remote revision + + Conflict detection: + - only available when modifying a single item + - requires the previous item revision to be provided (@c REV) +*/ + +class ItemModifyHandler : public Handler +{ +public: + ItemModifyHandler(AkonadiServer &akonadi); + ~ItemModifyHandler() override = default; + + bool parseStream() override; + +private: + bool replaceFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged); + bool addFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged); + bool deleteFlags(const PimItem::List &items, const QSet &flags, bool &flagsChanged); + bool replaceTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged); + bool addTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged); + bool deleteTags(const PimItem::List &items, const Scope &tags, bool &tagsChanged); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/itemmovehandler.cpp b/src/server/handler/itemmovehandler.cpp new file mode 100644 index 0000000..40fe03b --- /dev/null +++ b/src/server/handler/itemmovehandler.cpp @@ -0,0 +1,161 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemmovehandler.h" + +#include "akonadi.h" +#include "akonadiserver_debug.h" +#include "cachecleaner.h" +#include "connection.h" +#include "handlerhelper.h" +#include "storage/collectionqueryhelper.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretrievalmanager.h" +#include "storage/itemretriever.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +ItemMoveHandler::ItemMoveHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +void ItemMoveHandler::itemsRetrieved(const QVector &ids) +{ + DataStore *store = connection()->storageBackend(); + Transaction transaction(store, QStringLiteral("MOVE")); + + SelectQueryBuilder qb; + qb.setForUpdate(); + ItemQueryHelper::itemSetToQuery(ImapSet(ids), qb); + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::NotEquals, mDestination.id()); + + if (!qb.exec()) { + failureResponse("Unable to execute query"); + return; + } + + const QVector items = qb.result(); + if (items.isEmpty()) { + return; + } + + const QDateTime mtime = QDateTime::currentDateTimeUtc(); + // Split the list by source collection + QMultiMap toMove; + QMap sources; + ImapSet toMoveIds; + for (PimItem item : items) { + if (!item.isValid()) { + failureResponse("Invalid item in result set!?"); + return; + } + + const Collection source = item.collection(); + if (!source.isValid()) { + failureResponse("Item without collection found!?"); + return; + } + if (!sources.contains(source.id())) { + sources.insert(source.id(), source); + } + + Q_ASSERT(item.collectionId() != mDestination.id()); + + item.setCollectionId(mDestination.id()); + item.setAtime(mtime); + item.setDatetime(mtime); + // if the resource moved itself, we assume it did so because the change happenned in the backend + if (connection()->context().resource().id() != mDestination.resourceId()) { + item.setDirty(true); + } + + if (!item.update()) { + failureResponse("Unable to update item"); + return; + } + + toMove.insert(source.id(), item); + toMoveIds.add(QVector{item.id()}); + } + + if (!transaction.commit()) { + failureResponse("Unable to commit transaction."); + return; + } + + // Emit notification for each source collection separately + Collection source; + PimItem::List itemsToMove; + for (auto it = toMove.cbegin(), end = toMove.cend(); it != end; ++it) { + if (source.id() != it.key()) { + if (!itemsToMove.isEmpty()) { + store->notificationCollector()->itemsMoved(itemsToMove, source, mDestination); + } + source = sources.value(it.key()); + itemsToMove.clear(); + } + + itemsToMove.push_back(*it); + } + + if (!itemsToMove.isEmpty()) { + store->notificationCollector()->itemsMoved(itemsToMove, source, mDestination); + } + + // Batch-reset RID + // The item should have an empty RID in the destination collection to avoid + // RID conflicts with existing items (see T3904 in Phab). + // We do it after emitting notification so that the FetchHelper can still + // retrieve the RID + QueryBuilder qb2(PimItem::tableName(), QueryBuilder::Update); + qb2.setColumnValue(PimItem::remoteIdColumn(), QString()); + ItemQueryHelper::itemSetToQuery(toMoveIds, connection()->context(), qb2); + if (!qb2.exec()) { + failureResponse("Unable to update RID"); + return; + } +} + +bool ItemMoveHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + mDestination = HandlerHelper::collectionFromScope(cmd.destination(), connection()->context()); + if (mDestination.isVirtual()) { + return failureResponse("Moving items into virtual collection is not allowed"); + } + if (!mDestination.isValid()) { + return failureResponse("Invalid destination collection"); + } + + CommandContext context = connection()->context(); + context.setScopeContext(cmd.itemsContext()); + if (cmd.items().scope() == Scope::Rid) { + if (!context.collection().isValid()) { + return failureResponse("RID move requires valid source collection"); + } + } + + CacheCleanerInhibitor inhibitor(akonadi()); + + // make sure all the items we want to move are in the cache + ItemRetriever retriever(akonadi().itemRetrievalManager(), connection(), context); + retriever.setScope(cmd.items()); + retriever.setRetrieveFullPayload(true); + QObject::connect(&retriever, &ItemRetriever::itemsRetrieved, [this](const QVector &ids) { + itemsRetrieved(ids); + }); + if (!retriever.exec()) { + return failureResponse(retriever.lastError()); + } + + return successResponse(); +} diff --git a/src/server/handler/itemmovehandler.h b/src/server/handler/itemmovehandler.h new file mode 100644 index 0000000..d245023 --- /dev/null +++ b/src/server/handler/itemmovehandler.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the item move command. + +

Semantics

+ Moves the selected items. Item selection can happen within the usual three scopes: + - based on a uid set relative to the currently selected collection + - based on a global uid set (UID) + - based on a list of remote identifiers within the currently selected collection (RID) + + Destination is a collection id. +*/ +class ItemMoveHandler : public Handler +{ +public: + ItemMoveHandler(AkonadiServer &akonadi); + ~ItemMoveHandler() override = default; + + bool parseStream() override; + +private: + void itemsRetrieved(const QVector &ids); + + Collection mDestination; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/loginhandler.cpp b/src/server/handler/loginhandler.cpp new file mode 100644 index 0000000..501f1b8 --- /dev/null +++ b/src/server/handler/loginhandler.cpp @@ -0,0 +1,31 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "loginhandler.h" + +#include "connection.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +LoginHandler::LoginHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool LoginHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (cmd.sessionId().isEmpty()) { + return failureResponse(QStringLiteral("Missing session identifier")); + } + + connection()->setSessionId(cmd.sessionId()); + connection()->setState(Server::Authenticated); + + return successResponse(); +} diff --git a/src/server/handler/loginhandler.h b/src/server/handler/loginhandler.h new file mode 100644 index 0000000..e37cf89 --- /dev/null +++ b/src/server/handler/loginhandler.h @@ -0,0 +1,30 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the login command. +*/ +class LoginHandler : public Handler +{ +public: + LoginHandler(AkonadiServer &akonadi); + ~LoginHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/logouthandler.cpp b/src/server/handler/logouthandler.cpp new file mode 100644 index 0000000..4025ba7 --- /dev/null +++ b/src/server/handler/logouthandler.cpp @@ -0,0 +1,23 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "logouthandler.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +LogoutHandler::LogoutHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool LogoutHandler::parseStream() +{ + sendResponse(); + + connection()->setState(LoggingOut); + return true; +} diff --git a/src/server/handler/logouthandler.h b/src/server/handler/logouthandler.h new file mode 100644 index 0000000..f43ab91 --- /dev/null +++ b/src/server/handler/logouthandler.h @@ -0,0 +1,30 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the logout command. + */ +class LogoutHandler : public Handler +{ +public: + LogoutHandler(AkonadiServer &akonadi); + ~LogoutHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/relationfetchhandler.cpp b/src/server/handler/relationfetchhandler.cpp new file mode 100644 index 0000000..974b1ad --- /dev/null +++ b/src/server/handler/relationfetchhandler.cpp @@ -0,0 +1,77 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2014 Christian Mollekopf * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "relationfetchhandler.h" +#include "connection.h" +#include "handlerhelper.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +RelationFetchHandler::RelationFetchHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool RelationFetchHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + SelectQueryBuilder relationQuery; + if (cmd.side() > 0) { + Query::Condition c; + c.setSubQueryMode(Query::Or); + c.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.side()); + c.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.side()); + relationQuery.addCondition(c); + } else { + if (cmd.left() > 0) { + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.left()); + } + if (cmd.right() > 0) { + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.right()); + } + } + if (!cmd.types().isEmpty()) { + relationQuery.addJoin(QueryBuilder::InnerJoin, RelationType::tableName(), Relation::typeIdFullColumnName(), RelationType::idFullColumnName()); + QStringList types; + types.reserve(cmd.types().size()); + Q_FOREACH (const QByteArray &type, cmd.types()) { + types << QString::fromUtf8(type); + } + relationQuery.addValueCondition(RelationType::nameFullColumnName(), Query::In, types); + } + if (!cmd.resource().isEmpty()) { + Resource res = Resource::retrieveByName(cmd.resource()); + if (!res.isValid()) { + return failureResponse("Invalid resource"); + } + Query::Condition condition; + condition.setSubQueryMode(Query::Or); + condition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, Relation::leftIdFullColumnName()); + condition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, Relation::rightIdFullColumnName()); + relationQuery.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), condition); + + relationQuery.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + + relationQuery.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, res.id()); + relationQuery.addGroupColumns(QStringList() << Relation::leftIdFullColumnName() << Relation::rightIdFullColumnName() + << Relation::typeIdFullColumnName()); + } + + if (!relationQuery.exec()) { + return failureResponse("Failed to query for existing relation"); + } + const Relation::List existingRelations = relationQuery.result(); + for (const Relation &relation : existingRelations) { + sendResponse(HandlerHelper::fetchRelationsResponse(relation)); + } + + return successResponse(); +} diff --git a/src/server/handler/relationfetchhandler.h b/src/server/handler/relationfetchhandler.h new file mode 100644 index 0000000..fa12200 --- /dev/null +++ b/src/server/handler/relationfetchhandler.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2014 Christian Mollekopf * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the RELATIONFETCH command. + */ +class RelationFetchHandler : public Handler +{ +public: + RelationFetchHandler(AkonadiServer &akonadi); + ~RelationFetchHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/relationmodifyhandler.cpp b/src/server/handler/relationmodifyhandler.cpp new file mode 100644 index 0000000..441d789 --- /dev/null +++ b/src/server/handler/relationmodifyhandler.cpp @@ -0,0 +1,109 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "relationmodifyhandler.h" + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +RelationModifyHandler::RelationModifyHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +Relation RelationModifyHandler::fetchRelation(qint64 leftId, qint64 rightId, qint64 typeId) +{ + SelectQueryBuilder relationQuery; + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, leftId); + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, rightId); + relationQuery.addValueCondition(Relation::typeIdFullColumnName(), Query::Equals, typeId); + if (!relationQuery.exec()) { + throw HandlerException("Failed to query for existing relation"); + } + const Relation::List existingRelations = relationQuery.result(); + if (!existingRelations.isEmpty()) { + if (existingRelations.size() == 1) { + return existingRelations.at(0); + } else { + throw HandlerException("Matched more than 1 relation"); + } + } + + return Relation(); +} + +bool RelationModifyHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (cmd.type().isEmpty()) { + return failureResponse("Relation type not specified"); + } + + if (cmd.left() < 0 || cmd.right() < 0) { + return failureResponse("Invalid relation specified"); + } + + if (!cmd.remoteId().isEmpty() && !connection()->context().resource().isValid()) { + return failureResponse("RemoteID can only be set by Resources"); + } + + const QString typeName = QString::fromUtf8(cmd.type()); + const RelationType relationType = RelationType::retrieveByNameOrCreate(typeName); + if (!relationType.isValid()) { + return failureResponse(QStringLiteral("Unable to create relation type '") % typeName % QStringLiteral("'")); + } + + Relation existingRelation = fetchRelation(cmd.left(), cmd.right(), relationType.id()); + if (existingRelation.isValid()) { + existingRelation.setRemoteId(QLatin1String(cmd.remoteId())); + if (!existingRelation.update()) { + return failureResponse("Failed to update relation"); + } + } + + // Can't use insert(), does not work here (no "id" column) + QueryBuilder inQb(Relation::tableName(), QueryBuilder::Insert); + inQb.setIdentificationColumn(QString()); // omit "RETURING xyz" with PSQL + inQb.setColumnValue(Relation::leftIdColumn(), cmd.left()); + inQb.setColumnValue(Relation::rightIdColumn(), cmd.right()); + inQb.setColumnValue(Relation::typeIdColumn(), relationType.id()); + if (!inQb.exec()) { + throw HandlerException("Failed to store relation"); + } + + Relation insertedRelation = fetchRelation(cmd.left(), cmd.right(), relationType.id()); + + // Get all PIM items that are part of the relation + SelectQueryBuilder itemsQuery; + itemsQuery.setSubQueryMode(Query::Or); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.left()); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.right()); + + if (!itemsQuery.exec()) { + return failureResponse("Adding relation failed"); + } + const PimItem::List items = itemsQuery.result(); + + if (items.size() != 2) { + return failureResponse("Couldn't find items for relation"); + } + + /* if (items[0].collection().resourceId() != items[1].collection().resourceId()) { + throw HandlerException("Relations can only be created for items within the same resource"); + } */ + + auto collector = storageBackend()->notificationCollector(); + collector->relationAdded(insertedRelation); + collector->itemsRelationsChanged(items, {insertedRelation}, {}); + + return successResponse(); +} diff --git a/src/server/handler/relationmodifyhandler.h b/src/server/handler/relationmodifyhandler.h new file mode 100644 index 0000000..ee6674d --- /dev/null +++ b/src/server/handler/relationmodifyhandler.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekop + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +class Relation; + +class RelationModifyHandler : public Handler +{ +public: + RelationModifyHandler(AkonadiServer &akonadi); + ~RelationModifyHandler() override = default; + + bool parseStream() override; + +private: + Relation fetchRelation(qint64 leftId, qint64 rightId, qint64 typeId); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/relationremovehandler.cpp b/src/server/handler/relationremovehandler.cpp new file mode 100644 index 0000000..46ebdb9 --- /dev/null +++ b/src/server/handler/relationremovehandler.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "relationremovehandler.h" + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/queryhelper.h" +#include "storage/selectquerybuilder.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +RelationRemoveHandler::RelationRemoveHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool RelationRemoveHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (cmd.left() < 0 || cmd.right() < 0) { + return failureResponse("Invalid relation id's provided"); + } + + RelationType relType; + if (!cmd.type().isEmpty()) { + relType = RelationType::retrieveByName(QString::fromUtf8(cmd.type())); + if (!relType.isValid()) { + return failureResponse("Failed to load relation type"); + } + } + + SelectQueryBuilder relationQuery; + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.left()); + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.right()); + if (relType.isValid()) { + relationQuery.addValueCondition(Relation::typeIdFullColumnName(), Query::Equals, relType.id()); + } + + if (!relationQuery.exec()) { + return failureResponse("Failed to obtain relations"); + } + const Relation::List relations = relationQuery.result(); + for (const Relation &relation : relations) { + storageBackend()->notificationCollector()->relationRemoved(relation); + } + + // Get all PIM items that are part of the relation + SelectQueryBuilder itemsQuery; + itemsQuery.setSubQueryMode(Query::Or); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.left()); + itemsQuery.addValueCondition(PimItem::idColumn(), Query::Equals, cmd.right()); + + if (!itemsQuery.exec()) { + throw failureResponse("Removing relation failed"); + } + const PimItem::List items = itemsQuery.result(); + if (!items.isEmpty()) { + storageBackend()->notificationCollector()->itemsRelationsChanged(items, Relation::List(), relations); + } + + QueryBuilder qb(Relation::tableName(), QueryBuilder::Delete); + qb.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, cmd.left()); + qb.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, cmd.right()); + if (relType.isValid()) { + qb.addValueCondition(Relation::typeIdFullColumnName(), Query::Equals, relType.id()); + } + if (!qb.exec()) { + return failureResponse("Failed to remove relations"); + } + + return successResponse(); +} diff --git a/src/server/handler/relationremovehandler.h b/src/server/handler/relationremovehandler.h new file mode 100644 index 0000000..57d4d4d --- /dev/null +++ b/src/server/handler/relationremovehandler.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +class RelationRemoveHandler : public Handler +{ +public: + RelationRemoveHandler(AkonadiServer &akonadi); + ~RelationRemoveHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/resourceselecthandler.cpp b/src/server/handler/resourceselecthandler.cpp new file mode 100644 index 0000000..bb7bf96 --- /dev/null +++ b/src/server/handler/resourceselecthandler.cpp @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resourceselecthandler.h" + +#include "connection.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +ResourceSelectHandler::ResourceSelectHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool ResourceSelectHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + CommandContext context = connection()->context(); + if (cmd.resourceId().isEmpty()) { + context.setResource({}); + connection()->setContext(context); + return successResponse(); + } + + const Resource res = Resource::retrieveByName(cmd.resourceId()); + if (!res.isValid()) { + return failureResponse(cmd.resourceId() % QStringLiteral(" is not a valid resource identifier")); + } + + context.setResource(res); + connection()->setContext(context); + + return successResponse(); +} diff --git a/src/server/handler/resourceselecthandler.h b/src/server/handler/resourceselecthandler.h new file mode 100644 index 0000000..d33b9c6 --- /dev/null +++ b/src/server/handler/resourceselecthandler.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the resource selection command. + +

Semantics

+ Limits the scope of remote id based operations. Remote ids of collections are only guaranteed + to be unique per resource, so this command should be issued before running any RID based + collection commands. +*/ +class ResourceSelectHandler : public Handler +{ +public: + ResourceSelectHandler(AkonadiServer &akonadi); + ~ResourceSelectHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/searchcreatehandler.cpp b/src/server/handler/searchcreatehandler.cpp new file mode 100644 index 0000000..91c5e3a --- /dev/null +++ b/src/server/handler/searchcreatehandler.cpp @@ -0,0 +1,81 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "searchcreatehandler.h" + +#include "akonadi.h" +#include "akonadiserver_debug.h" +#include "connection.h" +#include "handlerhelper.h" +#include "search/searchmanager.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/transaction.h" +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +SearchCreateHandler::SearchCreateHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool SearchCreateHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (cmd.name().isEmpty()) { + return failureResponse("No name specified"); + } + + if (cmd.query().isEmpty()) { + return failureResponse("No query specified"); + } + + DataStore *db = connection()->storageBackend(); + Transaction transaction(db, QStringLiteral("SEARCH PERSISTENT")); + + QStringList queryAttributes; + + if (cmd.remote()) { + queryAttributes << QStringLiteral(AKONADI_PARAM_REMOTE); + } + if (cmd.recursive()) { + queryAttributes << QStringLiteral(AKONADI_PARAM_RECURSIVE); + } + + QVector queryColIds = cmd.queryCollections(); + std::sort(queryColIds.begin(), queryColIds.end()); + const auto queryCollections = queryColIds | Views::transform([](const auto id) { + return QString::number(id); + }) + | Actions::toQList; + + Collection col; + col.setQueryString(cmd.query()); + col.setQueryAttributes(queryAttributes.join(QLatin1Char(' '))); + col.setQueryCollections(queryCollections.join(QLatin1Char(' '))); + col.setParentId(1); // search root + col.setResourceId(1); // search resource + col.setName(cmd.name()); + col.setIsVirtual(true); + + const QStringList lstMimeTypes = cmd.mimeTypes(); + if (!db->appendCollection(col, lstMimeTypes, {{"AccessRights", "luD"}})) { + return failureResponse("Unable to create persistent search"); + } + + if (!transaction.commit()) { + return failureResponse("Unable to commit transaction"); + } + + akonadi().searchManager().updateSearch(col); + + sendResponse(HandlerHelper::fetchCollectionsResponse(akonadi(), col)); + return successResponse(); +} diff --git a/src/server/handler/searchcreatehandler.h b/src/server/handler/searchcreatehandler.h new file mode 100644 index 0000000..5886388 --- /dev/null +++ b/src/server/handler/searchcreatehandler.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the search_store search_delete commands. +*/ +class SearchCreateHandler : public Handler +{ +public: + SearchCreateHandler(AkonadiServer &akonadi); + ~SearchCreateHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/searchhandler.cpp b/src/server/handler/searchhandler.cpp new file mode 100644 index 0000000..47f4943 --- /dev/null +++ b/src/server/handler/searchhandler.cpp @@ -0,0 +1,96 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2009 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "searchhandler.h" + +#include "akonadi.h" +#include "akonadiserver_search_debug.h" +#include "connection.h" +#include "handlerhelper.h" +#include "itemfetchhelper.h" +#include "search/agentsearchengine.h" +#include "search/searchmanager.h" +#include "search/searchrequest.h" +#include "searchhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +SearchHandler::SearchHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool SearchHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (cmd.query().isEmpty()) { + return failureResponse("No query specified"); + } + + QVector collectionIds; + bool recursive = cmd.recursive(); + + if (cmd.collections().isEmpty() || cmd.collections() == QVector{0LL}) { + collectionIds << 0; + recursive = true; + } + + QVector collections = collectionIds; + if (recursive) { + collections += SearchHelper::matchSubcollectionsByMimeType(collectionIds, cmd.mimeTypes()); + } + + qCDebug(AKONADISERVER_SEARCH_LOG) << "SEARCH:"; + qCDebug(AKONADISERVER_SEARCH_LOG) << "\tQuery:" << cmd.query(); + qCDebug(AKONADISERVER_SEARCH_LOG) << "\tMimeTypes:" << cmd.mimeTypes(); + qCDebug(AKONADISERVER_SEARCH_LOG) << "\tCollections:" << collections; + qCDebug(AKONADISERVER_SEARCH_LOG) << "\tRemote:" << cmd.remote(); + qCDebug(AKONADISERVER_SEARCH_LOG) << "\tRecursive" << recursive; + + if (collections.isEmpty()) { + return successResponse(); + } + + mItemFetchScope = cmd.itemFetchScope(); + mTagFetchScope = cmd.tagFetchScope(); + + SearchRequest request(connection()->sessionId(), akonadi().searchManager(), akonadi().agentSearchManager()); + request.setCollections(collections); + request.setMimeTypes(cmd.mimeTypes()); + request.setQuery(cmd.query()); + request.setRemoteSearch(cmd.remote()); + QObject::connect(&request, &SearchRequest::resultsAvailable, [this](const QSet &results) { + processResults(results); + }); + request.exec(); + + // qCDebug(AKONADISERVER_SEARCH_LOG) << "\tResult:" << uids; + qCDebug(AKONADISERVER_SEARCH_LOG) << "\tResult:" << mAllResults.count() << "matches"; + + return successResponse(); +} + +void SearchHandler::processResults(const QSet &results) +{ + QSet newResults = results; + newResults.subtract(mAllResults); + mAllResults.unite(newResults); + + if (newResults.isEmpty()) { + return; + } + + ImapSet imapSet; + imapSet.add(newResults); + + Scope scope; + scope.setUidSet(imapSet); + + ItemFetchHelper fetchHelper(connection(), scope, mItemFetchScope, mTagFetchScope, akonadi()); + fetchHelper.fetchItems(); +} diff --git a/src/server/handler/searchhandler.h b/src/server/handler/searchhandler.h new file mode 100644 index 0000000..a24b1bd --- /dev/null +++ b/src/server/handler/searchhandler.h @@ -0,0 +1,40 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2009 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "handler.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the search commands. +*/ +class SearchHandler : public Handler +{ +public: + SearchHandler(AkonadiServer &akonadi); + ~SearchHandler() override = default; + + bool parseStream() override; + +private: + void processResults(const QSet &results); + + Protocol::ItemFetchScope mItemFetchScope; + Protocol::TagFetchScope mTagFetchScope; + QSet mAllResults; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/searchhelper.cpp b/src/server/handler/searchhelper.cpp new file mode 100644 index 0000000..8f11b9e --- /dev/null +++ b/src/server/handler/searchhelper.cpp @@ -0,0 +1,95 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2014 Daniel Vrátil * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "searchhelper.h" + +#include "entities.h" +#include "storage/countquerybuilder.h" + +#include + +using namespace Akonadi::Server; + +static qint64 parentCollectionId(qint64 collectionId) +{ + QueryBuilder qb(Collection::tableName(), QueryBuilder::Select); + qb.addColumn(Collection::parentIdColumn()); + qb.addValueCondition(Collection::idColumn(), Query::Equals, collectionId); + if (!qb.exec()) { + return -1; + } + if (!qb.query().next()) { + return -1; + } + const auto parentId = qb.query().value(0).toLongLong(); + qb.query().finish(); + return parentId; +} + +QVector SearchHelper::matchSubcollectionsByMimeType(const QVector &ancestors, const QStringList &mimeTypes) +{ + // Get all collections with given mime types + QueryBuilder qb(Collection::tableName(), QueryBuilder::Select); + qb.setDistinct(true); + qb.addColumn(Collection::idFullColumnName()); + qb.addColumn(Collection::parentIdFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, + CollectionMimeTypeRelation::tableName(), + CollectionMimeTypeRelation::leftFullColumnName(), + Collection::idFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, MimeType::tableName(), CollectionMimeTypeRelation::rightFullColumnName(), MimeType::idFullColumnName()); + Query::Condition cond(Query::Or); + for (const QString &mt : mimeTypes) { + cond.addValueCondition(MimeType::nameFullColumnName(), Query::Equals, mt); + } + if (!cond.isEmpty()) { + qb.addCondition(cond); + } + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to query search collections"; + return QVector(); + } + + QMap /* collectionIds */> candidateCollections; + while (qb.query().next()) { + candidateCollections[qb.query().value(1).toLongLong()].append(qb.query().value(0).toLongLong()); + } + qb.query().finish(); + + // If the ancestors list contains root, then return what we got, since everything + // is sub collection of root + QVector results; + if (ancestors.contains(0)) { + for (const QVector &res : std::as_const(candidateCollections)) { + results += res; + } + return results; + } + + // Try to resolve direct descendants + for (qint64 ancestor : ancestors) { + const QVector cols = candidateCollections.take(ancestor); + if (!cols.isEmpty()) { + results += cols; + } + } + + for (auto iter = candidateCollections.begin(), iterEnd = candidateCollections.end(); iter != iterEnd; ++iter) { + // Traverse the collection chain up to root + qint64 parentId = iter.key(); + while (!ancestors.contains(parentId) && parentId > 0) { + parentId = parentCollectionId(parentId); + } + // Ok, we found a requested ancestor in the parent chain + if (parentId > 0) { + results += iter.value(); + } + } + + return results; +} diff --git a/src/server/handler/searchhelper.h b/src/server/handler/searchhelper.h new file mode 100644 index 0000000..36d4936 --- /dev/null +++ b/src/server/handler/searchhelper.h @@ -0,0 +1,24 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class SearchHelper +{ +public: + static QVector matchSubcollectionsByMimeType(const QVector &ancestors, const QStringList &mimeTypes); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/searchresulthandler.cpp b/src/server/handler/searchresulthandler.cpp new file mode 100644 index 0000000..b734b15 --- /dev/null +++ b/src/server/handler/searchresulthandler.cpp @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2013 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "searchresulthandler.h" + +#include "akonadi.h" +#include "connection.h" +#include "search/searchtaskmanager.h" +#include "storage/itemqueryhelper.h" +#include "storage/querybuilder.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +SearchResultHandler::SearchResultHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool SearchResultHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (!checkScopeConstraints(cmd.result(), Scope::Uid | Scope::Rid)) { + return fail(cmd.searchId(), QStringLiteral("Only UID or RID scopes are allowed in SEARECH_RESULT")); + } + + QSet ids; + if (cmd.result().scope() == Scope::Rid && !cmd.result().isEmpty()) { + QueryBuilder qb(PimItem::tableName()); + qb.addColumn(PimItem::idFullColumnName()); + ItemQueryHelper::remoteIdToQuery(cmd.result().ridSet(), connection()->context(), qb); + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, cmd.collectionId()); + + if (!qb.exec()) { + return fail(cmd.searchId(), QStringLiteral("Failed to convert RID to UID")); + } + + QSqlQuery query = qb.query(); + while (query.next()) { + ids << query.value(0).toLongLong(); + } + query.finish(); + } else if (cmd.result().scope() == Scope::Uid && !cmd.result().isEmpty()) { + const ImapSet result = cmd.result().uidSet(); + const ImapInterval::List lstInterval = result.intervals(); + for (const ImapInterval &interval : lstInterval) { + for (qint64 i = interval.begin(), end = interval.end(); i <= end; ++i) { + ids.insert(i); + } + } + } + akonadi().agentSearchManager().pushResults(cmd.searchId(), ids, connection()); + + return successResponse(); +} + +bool SearchResultHandler::fail(const QByteArray &searchId, const QString &error) +{ + akonadi().agentSearchManager().pushResults(searchId, QSet(), connection()); + return failureResponse(error); +} diff --git a/src/server/handler/searchresulthandler.h b/src/server/handler/searchresulthandler.h new file mode 100644 index 0000000..a77baad --- /dev/null +++ b/src/server/handler/searchresulthandler.h @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2013 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the search_result command +*/ +class SearchResultHandler : public Handler +{ +public: + SearchResultHandler(AkonadiServer &akonadi); + ~SearchResultHandler() override = default; + + bool parseStream() override; + +private: + bool fail(const QByteArray &searchId, const QString &error); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/tagcreatehandler.cpp b/src/server/handler/tagcreatehandler.cpp new file mode 100644 index 0000000..f392785 --- /dev/null +++ b/src/server/handler/tagcreatehandler.cpp @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagcreatehandler.h" + +#include "connection.h" +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/transaction.h" +#include "tagfetchhelper.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagCreateHandler::TagCreateHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool TagCreateHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (!cmd.remoteId().isEmpty() && !connection()->context().resource().isValid()) { + return failureResponse(QStringLiteral("Only resources can create tags with remote ID")); + } + + Transaction trx(storageBackend(), QStringLiteral("TAGAPPEND")); + + TagType tagType; + if (!cmd.type().isEmpty()) { + const QString typeName = QString::fromUtf8(cmd.type()); + tagType = TagType::retrieveByNameOrCreate(typeName); + if (!tagType.isValid()) { + return failureResponse(QStringLiteral("Unable to create tagtype '") % typeName % QStringLiteral("'")); + } + } + + qint64 tagId = -1; + const QString gid = QString::fromUtf8(cmd.gid()); + if (cmd.merge()) { + QueryBuilder qb(Tag::tableName()); + qb.addColumn(Tag::idColumn()); + qb.addValueCondition(Tag::gidColumn(), Query::Equals, gid); + if (!qb.exec()) { + return failureResponse("Unable to list tags"); + } + if (qb.query().next()) { + tagId = qb.query().value(0).toLongLong(); + } + qb.query().finish(); + } + if (tagId < 0) { + Tag insertedTag; + insertedTag.setGid(gid); + if (cmd.parentId() >= 0) { + insertedTag.setParentId(cmd.parentId()); + } + if (tagType.isValid()) { + insertedTag.setTypeId(tagType.id()); + } + if (!insertedTag.insert(&tagId)) { + return failureResponse("Failed to store tag"); + } + + const Protocol::Attributes attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + TagAttribute attribute; + attribute.setTagId(tagId); + attribute.setType(iter.key()); + attribute.setValue(iter.value()); + if (!attribute.insert()) { + return failureResponse("Failed to store tag attribute"); + } + } + + storageBackend()->notificationCollector()->tagAdded(insertedTag); + } + + if (!cmd.remoteId().isEmpty()) { + const qint64 resourceId = connection()->context().resource().id(); + + CountQueryBuilder qb(TagRemoteIdResourceRelation::tableName()); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tagId); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, resourceId); + if (!qb.exec()) { + return failureResponse("Failed to query for existing TagRemoteIdResourceRelation entries"); + } + const bool exists = (qb.result() > 0); + + // If the relation is already existing simply update it (can happen if a resource simply creates the tag again while enabling merge) + bool ret = false; + if (exists) { + // Simply using update() doesn't work since TagRemoteIdResourceRelation only takes the tagId for identification of the column + QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Update); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tagId); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, resourceId); + qb.setColumnValue(TagRemoteIdResourceRelation::remoteIdColumn(), QString::fromUtf8(cmd.remoteId())); + ret = qb.exec(); + } else { + TagRemoteIdResourceRelation rel; + rel.setTagId(tagId); + rel.setResourceId(resourceId); + rel.setRemoteId(QString::fromUtf8(cmd.remoteId())); + ret = rel.insert(); + } + if (!ret) { + return failureResponse("Failed to store tag remote ID"); + } + } + + trx.commit(); + + Scope scope; + ImapSet set; + set.add(QVector() << tagId); + scope.setUidSet(set); + + Protocol::TagFetchScope fetchScope; + fetchScope.setFetchRemoteID(true); + fetchScope.setFetchAllAttributes(true); + + TagFetchHelper helper(connection(), scope, fetchScope); + if (!helper.fetchTags()) { + return failureResponse("Failed to fetch the new tag"); + } + + return successResponse(); +} diff --git a/src/server/handler/tagcreatehandler.h b/src/server/handler/tagcreatehandler.h new file mode 100644 index 0000000..f4ed762 --- /dev/null +++ b/src/server/handler/tagcreatehandler.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +class TagCreateHandler : public Handler +{ +public: + TagCreateHandler(AkonadiServer &akonadi); + ~TagCreateHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/tagdeletehandler.cpp b/src/server/handler/tagdeletehandler.cpp new file mode 100644 index 0000000..7bd4af3 --- /dev/null +++ b/src/server/handler/tagdeletehandler.cpp @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagdeletehandler.h" + +#include "storage/datastore.h" +#include "storage/queryhelper.h" +#include "storage/selectquerybuilder.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagDeleteHandler::TagDeleteHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool TagDeleteHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (!checkScopeConstraints(cmd.tag(), Scope::Uid)) { + return failureResponse(QStringLiteral("Only UID-based TAGREMOVE is supported")); + } + + SelectQueryBuilder tagQuery; + QueryHelper::setToQuery(cmd.tag().uidSet(), Tag::idFullColumnName(), tagQuery); + if (!tagQuery.exec()) { + return failureResponse(QStringLiteral("Failed to obtain tags")); + } + + const Tag::List tags = tagQuery.result(); + + if (!storageBackend()->removeTags(tags)) { + return failureResponse(QStringLiteral("Failed to remove tags")); + } + + return successResponse(); +} diff --git a/src/server/handler/tagdeletehandler.h b/src/server/handler/tagdeletehandler.h new file mode 100644 index 0000000..b5efa7e --- /dev/null +++ b/src/server/handler/tagdeletehandler.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +class TagDeleteHandler : public Handler +{ +public: + TagDeleteHandler(AkonadiServer &akonadi); + ~TagDeleteHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/tagfetchhandler.cpp b/src/server/handler/tagfetchhandler.cpp new file mode 100644 index 0000000..aa7a8f9 --- /dev/null +++ b/src/server/handler/tagfetchhandler.cpp @@ -0,0 +1,33 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2014 Daniel Vrátil * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "tagfetchhandler.h" +#include "connection.h" +#include "tagfetchhelper.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagFetchHandler::TagFetchHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool TagFetchHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + if (!checkScopeConstraints(cmd.scope(), Scope::Uid)) { + return failureResponse("Only UID-based TAGFETCH is supported"); + } + + TagFetchHelper helper(connection(), cmd.scope(), cmd.fetchScope()); + if (!helper.fetchTags()) { + return failureResponse("Failed to fetch tags"); + } + + return successResponse(); +} diff --git a/src/server/handler/tagfetchhandler.h b/src/server/handler/tagfetchhandler.h new file mode 100644 index 0000000..903192d --- /dev/null +++ b/src/server/handler/tagfetchhandler.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2014 Daniel Vrátil * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for the FETCHTAG command. + */ +class TagFetchHandler : public Handler +{ +public: + TagFetchHandler(AkonadiServer &akonadi); + ~TagFetchHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/tagfetchhelper.cpp b/src/server/handler/tagfetchhelper.cpp new file mode 100644 index 0000000..688defa --- /dev/null +++ b/src/server/handler/tagfetchhelper.cpp @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagfetchhelper.h" +#include "connection.h" +#include "handler.h" +#include "storage/querybuilder.h" +#include "storage/tagqueryhelper.h" +#include "utils.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TagFetchHelper::TagFetchHelper(Connection *connection, const Scope &scope, const Protocol::TagFetchScope &fetchScope) + : mConnection(connection) + , mScope(scope) + , mFetchScope(fetchScope) +{ +} + +QSqlQuery TagFetchHelper::buildAttributeQuery() const +{ + QueryBuilder qb(TagAttribute::tableName()); + qb.addColumn(TagAttribute::tagIdFullColumnName()); + qb.addColumn(TagAttribute::typeFullColumnName()); + qb.addColumn(TagAttribute::valueFullColumnName()); + qb.addSortColumn(TagAttribute::tagIdFullColumnName(), Query::Descending); + qb.addJoin(QueryBuilder::InnerJoin, Tag::tableName(), TagAttribute::tagIdFullColumnName(), Tag::idFullColumnName()); + TagQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); + + if (!qb.exec()) { + throw HandlerException("Unable to list tag attributes"); + } + + qb.query().next(); + return qb.query(); +} + +QSqlQuery TagFetchHelper::buildAttributeQuery(qint64 id, const Protocol::TagFetchScope &fetchScope) +{ + QueryBuilder qb(TagAttribute::tableName()); + qb.addColumn(TagAttribute::tagIdColumn()); + qb.addColumn(TagAttribute::typeColumn()); + qb.addColumn(TagAttribute::valueColumn()); + qb.addSortColumn(TagAttribute::tagIdColumn(), Query::Descending); + + qb.addValueCondition(TagAttribute::tagIdColumn(), Query::Equals, id); + if (!fetchScope.fetchAllAttributes() && !fetchScope.attributes().isEmpty()) { + QVariantList typeNames; + const auto attrs = fetchScope.attributes(); + std::transform(attrs.cbegin(), attrs.cend(), std::back_inserter(typeNames), [](const QByteArray &ba) { + return QVariant(ba); + }); + qb.addValueCondition(TagAttribute::typeColumn(), Query::In, typeNames); + } + + if (!qb.exec()) { + throw HandlerException("Unable to list tag attributes"); + } + + qb.query().next(); + return qb.query(); +} + +QSqlQuery TagFetchHelper::buildTagQuery() +{ + QueryBuilder qb(Tag::tableName()); + qb.addColumn(Tag::idFullColumnName()); + qb.addColumn(Tag::gidFullColumnName()); + qb.addColumn(Tag::parentIdFullColumnName()); + + qb.addJoin(QueryBuilder::InnerJoin, TagType::tableName(), Tag::typeIdFullColumnName(), TagType::idFullColumnName()); + qb.addColumn(TagType::nameFullColumnName()); + + // Expose tag's remote ID only to resources + if (mFetchScope.fetchRemoteID() && mConnection->context().resource().isValid()) { + qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName()); + Query::Condition joinCondition; + joinCondition.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, mConnection->context().resource().id()); + joinCondition.addColumnCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, Tag::idFullColumnName()); + qb.addJoin(QueryBuilder::LeftJoin, TagRemoteIdResourceRelation::tableName(), joinCondition); + } + + qb.addSortColumn(Tag::idFullColumnName(), Query::Descending); + TagQueryHelper::scopeToQuery(mScope, mConnection->context(), qb); + if (!qb.exec()) { + throw HandlerException("Unable to list tags"); + } + + qb.query().next(); + return qb.query(); +} + +QMap TagFetchHelper::fetchTagAttributes(qint64 tagId, const Protocol::TagFetchScope &fetchScope) +{ + QMap attributes; + + QSqlQuery attributeQuery = buildAttributeQuery(tagId, fetchScope); + while (attributeQuery.isValid()) { + attributes.insert(Utils::variantToByteArray(attributeQuery.value(1)), Utils::variantToByteArray(attributeQuery.value(2))); + attributeQuery.next(); + } + attributeQuery.finish(); + return attributes; +} + +bool TagFetchHelper::fetchTags() +{ + QSqlQuery tagQuery = buildTagQuery(); + QSqlQuery attributeQuery; + if (!mFetchScope.fetchIdOnly()) { + attributeQuery = buildAttributeQuery(); + } + + while (tagQuery.isValid()) { + const qint64 tagId = tagQuery.value(0).toLongLong(); + Protocol::FetchTagsResponse response; + response.setId(tagId); + if (!mFetchScope.fetchIdOnly()) { + response.setGid(Utils::variantToByteArray(tagQuery.value(1))); + if (tagQuery.value(2).isNull()) { + // client indicates invalid or null parent as ID -1 + response.setParentId(-1); + } else { + response.setParentId(tagQuery.value(2).toLongLong()); + } + response.setType(Utils::variantToByteArray(tagQuery.value(3))); + if (mFetchScope.fetchRemoteID() && mConnection->context().resource().isValid()) { + response.setRemoteId(Utils::variantToByteArray(tagQuery.value(4))); + } + + QMap tagAttributes; + while (attributeQuery.isValid()) { + const qint64 id = attributeQuery.value(0).toLongLong(); + if (id > tagId) { + attributeQuery.next(); + continue; + } else if (id < tagId) { + break; + } + + tagAttributes.insert(Utils::variantToByteArray(attributeQuery.value(1)), Utils::variantToByteArray(attributeQuery.value(2))); + attributeQuery.next(); + } + + response.setAttributes(tagAttributes); + } + + mConnection->sendResponse(std::move(response)); + + tagQuery.next(); + } + attributeQuery.finish(); + tagQuery.finish(); + + return true; +} diff --git a/src/server/handler/tagfetchhelper.h b/src/server/handler/tagfetchhelper.h new file mode 100644 index 0000000..89c5ea8 --- /dev/null +++ b/src/server/handler/tagfetchhelper.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class Connection; + +class TagFetchHelper +{ +public: + TagFetchHelper(Connection *connection, const Scope &scope, const Protocol::TagFetchScope &fetchScope); + ~TagFetchHelper() = default; + + bool fetchTags(); + + static QMap fetchTagAttributes(qint64 tagId, const Protocol::TagFetchScope &fetchScope); + +private: + QSqlQuery buildTagQuery(); + QSqlQuery buildAttributeQuery() const; + static QSqlQuery buildAttributeQuery(qint64 id, const Protocol::TagFetchScope &fetchScope); + +private: + Connection *mConnection = nullptr; + Scope mScope; + Protocol::TagFetchScope mFetchScope; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/tagmodifyhandler.cpp b/src/server/handler/tagmodifyhandler.cpp new file mode 100644 index 0000000..876efb2 --- /dev/null +++ b/src/server/handler/tagmodifyhandler.cpp @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagmodifyhandler.h" + +#include "connection.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "tagfetchhelper.h" +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +TagModifyHandler::TagModifyHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool TagModifyHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + Tag changedTag = Tag::retrieveById(cmd.tagId()); + if (!changedTag.isValid()) { + return failureResponse("No such tag"); + } + + QSet changes; + + // Retrieve all tag's attributes + const TagAttribute::List attributes = TagAttribute::retrieveFiltered(TagAttribute::tagIdFullColumnName(), cmd.tagId()); + const auto attributesMap = attributes | Views::transform([](const auto &attr) { + return std::make_pair(attr.type(), attr); + }) + | Actions::toQMap; + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::ParentId) { + if (cmd.parentId() != changedTag.parentId()) { + changedTag.setParentId(cmd.parentId()); + changes << AKONADI_PARAM_PARENT; + } + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::Type) { + TagType type = TagType::retrieveById(changedTag.typeId()); + const QString newTypeName = QString::fromUtf8(cmd.type()); + if (newTypeName != type.name()) { + const TagType newType = TagType::retrieveByNameOrCreate(newTypeName); + if (!newType.isValid()) { + return failureResponse("Failed to create new tag type"); + } + changedTag.setTagType(newType); + changes << AKONADI_PARAM_MIMETYPE; + } + } + + bool tagRemoved = false; + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::RemoteId) { + if (!connection()->context().resource().isValid()) { + return failureResponse("Only resources can change tag remote ID"); + } + + // Simply using remove() doesn't work since we need two arguments + QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Delete); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, cmd.tagId()); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, connection()->context().resource().id()); + qb.exec(); + + if (!cmd.remoteId().isEmpty()) { + TagRemoteIdResourceRelation remoteIdRelation; + remoteIdRelation.setRemoteId(QString::fromUtf8(cmd.remoteId())); + remoteIdRelation.setResourceId(connection()->context().resource().id()); + remoteIdRelation.setTag(changedTag); + if (!remoteIdRelation.insert()) { + return failureResponse("Failed to insert remotedid resource relation"); + } + } else { + const int tagRidsCount = TagRemoteIdResourceRelation::count(TagRemoteIdResourceRelation::tagIdColumn(), changedTag.id()); + // We just removed the last RID of the tag, which means that no other + // resource owns this tag, so we have to remove it to simulate tag + // removal + if (tagRidsCount == 0) { + if (!storageBackend()->removeTags(Tag::List() << changedTag)) { + return failureResponse("Failed to remove tag"); + } + tagRemoved = true; + } + } + // Do not notify about remoteid changes, otherwise we bounce back and forth + // between resources recording it's change and updating the remote id. + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::RemovedAttributes) { + Q_FOREACH (const QByteArray &attrName, cmd.removedAttributes()) { + TagAttribute attribute = attributesMap.value(attrName); + TagAttribute::remove(attribute.id()); + changes << attrName; + } + } + + if (cmd.modifiedParts() & Protocol::ModifyTagCommand::Attributes) { + const QMap attrs = cmd.attributes(); + for (auto iter = attrs.cbegin(), end = attrs.cend(); iter != end; ++iter) { + if (attributesMap.contains(iter.key())) { + TagAttribute attribute = attributesMap.value(iter.key()); + attribute.setValue(iter.value()); + if (!attribute.update()) { + return failureResponse("Failed to update attribute"); + } + } else { + TagAttribute attribute; + attribute.setTagId(cmd.tagId()); + attribute.setType(iter.key()); + attribute.setValue(iter.value()); + if (!attribute.insert()) { + return failureResponse("Failed to insert attribute"); + } + } + changes << iter.key(); + } + } + + if (!tagRemoved) { + if (!changedTag.update()) { + return failureResponse("Failed to store changes"); + } + if (!changes.isEmpty()) { + storageBackend()->notificationCollector()->tagChanged(changedTag); + } + + ImapSet set; + set.add(QVector() << cmd.tagId()); + + Protocol::TagFetchScope fetchScope; + fetchScope.setFetchRemoteID(true); + fetchScope.setFetchAllAttributes(true); + + Scope scope; + scope.setUidSet(set); + TagFetchHelper helper(connection(), scope, fetchScope); + if (!helper.fetchTags()) { + return failureResponse("Failed to fetch response"); + } + } else { + successResponse(); + } + + return successResponse(); +} diff --git a/src/server/handler/tagmodifyhandler.h b/src/server/handler/tagmodifyhandler.h new file mode 100644 index 0000000..c7b63ee --- /dev/null +++ b/src/server/handler/tagmodifyhandler.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +class TagModifyHandler : public Handler +{ +public: + TagModifyHandler(AkonadiServer &akonadi); + ~TagModifyHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handler/transactionhandler.cpp b/src/server/handler/transactionhandler.cpp new file mode 100644 index 0000000..9274322 --- /dev/null +++ b/src/server/handler/transactionhandler.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "transactionhandler.h" +#include "connection.h" +#include "storage/datastore.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +TransactionHandler::TransactionHandler(AkonadiServer &akonadi) + : Handler(akonadi) +{ +} + +bool TransactionHandler::parseStream() +{ + const auto &cmd = Protocol::cmdCast(m_command); + + DataStore *store = connection()->storageBackend(); + + switch (cmd.mode()) { + case Protocol::TransactionCommand::Invalid: + return failureResponse("Invalid operation"); + case Protocol::TransactionCommand::Begin: + if (!store->beginTransaction(QStringLiteral("CLIENT TRANSACTION"))) { + return failureResponse("Unable to begin transaction."); + } + break; + case Protocol::TransactionCommand::Rollback: + if (!store->inTransaction()) { + return failureResponse("There is no transaction in progress."); + } + if (!store->rollbackTransaction()) { + return failureResponse("Unable to roll back transaction."); + } + break; + case Protocol::TransactionCommand::Commit: + if (!store->inTransaction()) { + return failureResponse("There is no transaction in progress."); + } + if (!store->commitTransaction()) { + return failureResponse("Unable to commit transaction."); + } + break; + } + + return successResponse(); +} diff --git a/src/server/handler/transactionhandler.h b/src/server/handler/transactionhandler.h new file mode 100644 index 0000000..8f001dc --- /dev/null +++ b/src/server/handler/transactionhandler.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "handler.h" + +namespace Akonadi +{ +namespace Server +{ +/** + @ingroup akonadi_server_handler + + Handler for transaction commands (BEGIN, COMMIT, ROLLBACK). +*/ +class TransactionHandler : public Handler +{ +public: + TransactionHandler(AkonadiServer &akonadi); + ~TransactionHandler() override = default; + + bool parseStream() override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/handlerhelper.cpp b/src/server/handlerhelper.cpp new file mode 100644 index 0000000..3eecb84 --- /dev/null +++ b/src/server/handlerhelper.cpp @@ -0,0 +1,427 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "handlerhelper.h" +#include "akonadi.h" +#include "commandcontext.h" +#include "connection.h" +#include "handler.h" +#include "storage/collectionqueryhelper.h" +#include "storage/collectionstatistics.h" +#include "storage/countquerybuilder.h" +#include "storage/datastore.h" +#include "storage/queryhelper.h" +#include "storage/selectquerybuilder.h" +#include "utils.h" + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Collection HandlerHelper::collectionFromIdOrName(const QByteArray &id) +{ + // id is a number + bool ok = false; + qint64 collectionId = id.toLongLong(&ok); + if (ok) { + return Collection::retrieveById(collectionId); + } + + // id is a path + QString path = QString::fromUtf8(id); // ### should be UTF-7 for real IMAP compatibility + + const QStringList pathParts = path.split(QLatin1Char('/'), Qt::SkipEmptyParts); + + Collection col; + for (const QString &part : pathParts) { + SelectQueryBuilder qb; + qb.addValueCondition(Collection::nameColumn(), Query::Equals, part); + if (col.isValid()) { + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, col.id()); + } else { + qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); + } + if (!qb.exec()) { + return Collection(); + } + Collection::List list = qb.result(); + if (list.count() != 1) { + return Collection(); + } + col = list.first(); + } + return col; +} + +QString HandlerHelper::pathForCollection(const Collection &col) +{ + QStringList parts; + Collection current = col; + while (current.isValid()) { + parts.prepend(current.name()); + current = current.parent(); + } + return parts.join(QLatin1Char('/')); +} + +Protocol::CachePolicy HandlerHelper::cachePolicyResponse(const Collection &col) +{ + Protocol::CachePolicy cachePolicy; + cachePolicy.setInherit(col.cachePolicyInherit()); + cachePolicy.setCacheTimeout(col.cachePolicyCacheTimeout()); + cachePolicy.setCheckInterval(col.cachePolicyCheckInterval()); + if (!col.cachePolicyLocalParts().isEmpty()) { + cachePolicy.setLocalParts(col.cachePolicyLocalParts().split(QLatin1Char(' '))); + } + cachePolicy.setSyncOnDemand(col.cachePolicySyncOnDemand()); + return cachePolicy; +} + +Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(AkonadiServer &akonadi, const Collection &col) +{ + QStringList mimeTypes; + const auto mimeTypesList = col.mimeTypes(); + mimeTypes.reserve(mimeTypesList.size()); + for (const MimeType &mt : mimeTypesList) { + mimeTypes << mt.name(); + } + + return fetchCollectionsResponse(akonadi, col, col.attributes(), false, 0, QStack(), QStack(), mimeTypes); +} + +Protocol::FetchCollectionsResponse HandlerHelper::fetchCollectionsResponse(AkonadiServer &akonadi, + const Collection &col, + const CollectionAttribute::List &attrs, + bool includeStatistics, + int ancestorDepth, + const QStack &ancestors, + const QStack &ancestorAttributes, + const QStringList &mimeTypes) +{ + Protocol::FetchCollectionsResponse response; + response.setId(col.id()); + response.setParentId(col.parentId()); + response.setName(col.name()); + response.setMimeTypes(mimeTypes); + response.setRemoteId(col.remoteId()); + response.setRemoteRevision(col.remoteRevision()); + response.setResource(col.resource().name()); + response.setIsVirtual(col.isVirtual()); + + if (includeStatistics) { + const auto stats = akonadi.collectionStatistics().statistics(col); + if (stats.count > -1) { + Protocol::FetchCollectionStatsResponse statsResponse; + statsResponse.setCount(stats.count); + statsResponse.setUnseen(stats.count - stats.read); + statsResponse.setSize(stats.size); + response.setStatistics(statsResponse); + } + } + + if (!col.queryString().isEmpty()) { + response.setSearchQuery(col.queryString()); + QVector searchCols; + const QStringList searchColIds = col.queryCollections().split(QLatin1Char(' ')); + searchCols.reserve(searchColIds.size()); + for (const QString &searchColId : searchColIds) { + searchCols << searchColId.toLongLong(); + } + response.setSearchCollections(searchCols); + } + + Protocol::CachePolicy cachePolicy = cachePolicyResponse(col); + response.setCachePolicy(cachePolicy); + + if (ancestorDepth) { + QVector ancestorList = HandlerHelper::ancestorsResponse(ancestorDepth, ancestors, ancestorAttributes); + response.setAncestors(ancestorList); + } + + response.setEnabled(col.enabled()); + response.setDisplayPref(static_cast(col.displayPref())); + response.setSyncPref(static_cast(col.syncPref())); + response.setIndexPref(static_cast(col.indexPref())); + + QMap ra; + for (const CollectionAttribute &attr : attrs) { + ra.insert(attr.type(), attr.value()); + } + response.setAttributes(ra); + + return response; +} + +QVector +HandlerHelper::ancestorsResponse(int ancestorDepth, const QStack &_ancestors, const QStack &_ancestorsAttributes) +{ + QVector rv; + if (ancestorDepth > 0) { + QStack ancestors(_ancestors); + QStack ancestorAttributes(_ancestorsAttributes); + for (int i = 0; i < ancestorDepth; ++i) { + if (ancestors.isEmpty()) { + Protocol::Ancestor ancestor; + ancestor.setId(0); + rv << ancestor; + break; + } + const Collection c = ancestors.pop(); + Protocol::Ancestor a; + a.setId(c.id()); + a.setRemoteId(c.remoteId()); + a.setName(c.name()); + if (!ancestorAttributes.isEmpty()) { + QMap attrs; + Q_FOREACH (const CollectionAttribute &attr, ancestorAttributes.pop()) { + attrs.insert(attr.type(), attr.value()); + } + a.setAttributes(attrs); + } + + rv << a; + } + } + + return rv; +} + +Protocol::FetchTagsResponse HandlerHelper::fetchTagsResponse(const Tag &tag, const Protocol::TagFetchScope &tagFetchScope, Connection *connection) +{ + Protocol::FetchTagsResponse response; + response.setId(tag.id()); + if (tagFetchScope.fetchIdOnly()) { + return response; + } + + response.setType(tag.tagType().name().toUtf8()); + // FIXME FIXME FIXME Terrible hack to workaround limitations of the generated entities code: + // The invalid parent is represented in code by -1 but in the DB it is stored as NULL, which + // gets converted to 0 by our entities code. + if (tag.parentId() == 0) { + response.setParentId(-1); + } else { + response.setParentId(tag.parentId()); + } + response.setGid(tag.gid().toUtf8()); + if (tagFetchScope.fetchRemoteID() && connection) { + // Fail silently if retrieving tag RID is not allowed in current context + if (connection->context().resource().isValid()) { + QueryBuilder qb(TagRemoteIdResourceRelation::tableName()); + qb.addColumn(TagRemoteIdResourceRelation::remoteIdColumn()); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdColumn(), Query::Equals, connection->context().resource().id()); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdColumn(), Query::Equals, tag.id()); + if (!qb.exec()) { + throw HandlerException("Unable to query Tag Remote ID"); + } + QSqlQuery query = qb.query(); + // RID may not be available + if (query.next()) { + response.setRemoteId(Utils::variantToByteArray(query.value(0))); + } + query.finish(); + } + } + + if (tagFetchScope.fetchAllAttributes() || !tagFetchScope.attributes().isEmpty()) { + QueryBuilder qb(TagAttribute::tableName()); + qb.addColumns({TagAttribute::typeFullColumnName(), TagAttribute::valueFullColumnName()}); + Query::Condition cond(Query::And); + cond.addValueCondition(TagAttribute::tagIdFullColumnName(), Query::Equals, tag.id()); + if (!tagFetchScope.fetchAllAttributes() && !tagFetchScope.attributes().isEmpty()) { + QVariantList types; + const auto scope = tagFetchScope.attributes(); + std::transform(scope.cbegin(), scope.cend(), std::back_inserter(types), [](const QByteArray &ba) { + return QVariant(ba); + }); + cond.addValueCondition(TagAttribute::typeFullColumnName(), Query::In, types); + } + qb.addCondition(cond); + if (!qb.exec()) { + throw HandlerException("Unable to query Tag Attributes"); + } + QSqlQuery query = qb.query(); + Protocol::Attributes attributes; + while (query.next()) { + attributes.insert(Utils::variantToByteArray(query.value(0)), Utils::variantToByteArray(query.value(1))); + } + query.finish(); + response.setAttributes(attributes); + } + + return response; +} + +Protocol::FetchRelationsResponse HandlerHelper::fetchRelationsResponse(const Relation &relation) +{ + Protocol::FetchRelationsResponse resp; + resp.setLeft(relation.leftId()); + resp.setLeftMimeType(relation.left().mimeType().name().toUtf8()); + resp.setRight(relation.rightId()); + resp.setRightMimeType(relation.right().mimeType().name().toUtf8()); + resp.setType(relation.relationType().name().toUtf8()); + return resp; +} + +Flag::List HandlerHelper::resolveFlags(const QSet &flagNames) +{ + Flag::List flagList; + flagList.reserve(flagNames.size()); + for (const QByteArray &flagName : flagNames) { + const Flag flag = Flag::retrieveByNameOrCreate(QString::fromUtf8(flagName)); + if (!flag.isValid()) { + throw HandlerException("Unable to create flag"); + } + flagList.append(flag); + } + return flagList; +} + +Tag::List HandlerHelper::resolveTagsByUID(const ImapSet &tags) +{ + if (tags.isEmpty()) { + return Tag::List(); + } + SelectQueryBuilder qb; + QueryHelper::setToQuery(tags, Tag::idFullColumnName(), qb); + if (!qb.exec()) { + throw HandlerException("Unable to resolve tags"); + } + const Tag::List result = qb.result(); + if (result.isEmpty()) { + throw HandlerException("No tags found"); + } + return result; +} + +Tag::List HandlerHelper::resolveTagsByGID(const QStringList &tagsGIDs) +{ + Tag::List tagList; + if (tagsGIDs.isEmpty()) { + return tagList; + } + + for (const QString &tagGID : tagsGIDs) { + Tag::List tags = Tag::retrieveFiltered(Tag::gidColumn(), tagGID); + Tag tag; + if (tags.isEmpty()) { + tag.setGid(tagGID); + tag.setParentId(0); + + const TagType type = TagType::retrieveByNameOrCreate(QStringLiteral("PLAIN")); + if (!type.isValid()) { + throw HandlerException("Unable to create tag type"); + } + tag.setTagType(type); + if (!tag.insert()) { + throw HandlerException("Unable to create tag"); + } + } else if (tags.count() == 1) { + tag = tags[0]; + } else { + // Should not happen + throw HandlerException("Tag GID is not unique"); + } + + tagList.append(tag); + } + + return tagList; +} + +Tag::List HandlerHelper::resolveTagsByRID(const QStringList &tagsRIDs, const CommandContext &context) +{ + Tag::List tags; + if (tagsRIDs.isEmpty()) { + return tags; + } + + if (!context.resource().isValid()) { + throw HandlerException("Tags can be resolved by their RID only in resource context"); + } + + tags.reserve(tagsRIDs.size()); + for (const QString &tagRID : tagsRIDs) { + SelectQueryBuilder qb; + Query::Condition cond; + cond.addColumnCondition(Tag::idFullColumnName(), Query::Equals, TagRemoteIdResourceRelation::tagIdFullColumnName()); + cond.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + qb.addJoin(QueryBuilder::LeftJoin, TagRemoteIdResourceRelation::tableName(), cond); + qb.addValueCondition(TagRemoteIdResourceRelation::remoteIdFullColumnName(), Query::Equals, tagRID); + if (!qb.exec()) { + throw HandlerException("Unable to resolve tags"); + } + + Tag tag; + Tag::List results = qb.result(); + if (results.isEmpty()) { + // If the tag does not exist, we create a new one with GID matching RID + Tag::List tags = resolveTagsByGID(QStringList() << tagRID); + if (tags.count() != 1) { + throw HandlerException("Unable to resolve tag"); + } + tag = tags[0]; + TagRemoteIdResourceRelation rel; + rel.setRemoteId(tagRID); + rel.setTagId(tag.id()); + rel.setResourceId(context.resource().id()); + if (!rel.insert()) { + throw HandlerException("Unable to create tag"); + } + } else if (results.count() == 1) { + tag = results[0]; + } else { + throw HandlerException("Tag RID is not unique within this resource context"); + } + + tags.append(tag); + } + + return tags; +} + +Collection HandlerHelper::collectionFromScope(const Scope &scope, const CommandContext &context) +{ + if (scope.scope() == Scope::Invalid || scope.scope() == Scope::Gid) { + throw HandlerException("Invalid collection scope"); + } + + SelectQueryBuilder qb; + CollectionQueryHelper::scopeToQuery(scope, context, qb); + if (!qb.exec()) { + throw HandlerException("Failed to execute SQL query"); + } + + const Collection::List c = qb.result(); + if (c.isEmpty()) { + return Collection(); + } else if (c.count() == 1) { + return c.at(0); + } else { + throw HandlerException("Query returned more than one reslut"); + } +} + +Tag::List HandlerHelper::tagsFromScope(const Scope &scope, const CommandContext &context) +{ + if (scope.scope() == Scope::Invalid || scope.scope() == Scope::HierarchicalRid) { + throw HandlerException("Invalid tag scope"); + } + + if (scope.scope() == Scope::Uid) { + return resolveTagsByUID(scope.uidSet()); + } else if (scope.scope() == Scope::Gid) { + return resolveTagsByGID(scope.gidSet()); + } else if (scope.scope() == Scope::Rid) { + return resolveTagsByRID(scope.ridSet(), context); + } + + Q_ASSERT(false); + return Tag::List(); +} diff --git a/src/server/handlerhelper.h b/src/server/handlerhelper.h new file mode 100644 index 0000000..4edca70 --- /dev/null +++ b/src/server/handlerhelper.h @@ -0,0 +1,116 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "entities.h" + +#include +#include +#include + +namespace Akonadi +{ +class Scope; +class ImapSet; + +namespace Protocol +{ +class Ancestor; +class CachePolicy; +class FetchCollectionsResponse; +class TagFetchScope; +class FetchTagsResponse; +using FetchTagsResponsePtr = QSharedPointer; +class FetchRelationsResponse; +using FetchRelationsResponsePtr = QSharedPointer; +} + +namespace Server +{ +class CommandContext; +class Connection; +class AkonadiServer; + +/** + Helper functions for command handlers. +*/ +class HandlerHelper +{ +public: + /** + Returns the collection identified by the given id or path. + */ + static Collection collectionFromIdOrName(const QByteArray &id); + + /** + Returns the full path for the given collection. + */ + static QString pathForCollection(const Collection &col); + + /** + Returns the protocol representation of the cache policy of the given + Collection object. + */ + static Protocol::CachePolicy cachePolicyResponse(const Collection &col); + + /** + Returns the protocol representation of the given collection. + Make sure DataStore::activeCachePolicy() has been called before to include + the effective cache policy + */ + static Protocol::FetchCollectionsResponse fetchCollectionsResponse(AkonadiServer &akonadi, const Collection &col); + + /** + Returns the protocol representation of the given collection. + Make sure DataStore::activeCachePolicy() has been called before to include + the effective cache policy + */ + static Protocol::FetchCollectionsResponse + fetchCollectionsResponse(AkonadiServer &akonadi, + const Collection &col, + const CollectionAttribute::List &attributeList, + bool includeStatistics = false, + int ancestorDepth = 0, + const QStack &ancestors = QStack(), + const QStack &ancestorAttributes = QStack(), + const QStringList &mimeTypes = QStringList()); + + /** + Returns the protocol representation of a collection ancestor chain. + */ + static QVector ancestorsResponse(int ancestorDepth, + const QStack &ancestors, + const QStack &_ancestorsAttributes = QStack()); + + static Protocol::FetchTagsResponse fetchTagsResponse(const Tag &tag, const Protocol::TagFetchScope &tagFetchScope, Connection *connection = nullptr); + + static Protocol::FetchRelationsResponse fetchRelationsResponse(const Relation &relation); + + /** + Converts a bytearray list of flag names into flag records. + @throws HandlerException on errors during database operations + */ + static Flag::List resolveFlags(const QSet &flagNames); + + /** + Converts a imap set of tags into tag records. + @throws HandlerException on errors during database operations + */ + static Tag::List resolveTagsByUID(const ImapSet &tags); + + static Tag::List resolveTagsByGID(const QStringList &tagsGIDs); + + static Tag::List resolveTagsByRID(const QStringList &tagsRIDs, const CommandContext &context); + + static Collection collectionFromScope(const Scope &scope, const CommandContext &context); + + static Tag::List tagsFromScope(const Scope &scope, const CommandContext &context); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/intervalcheck.cpp b/src/server/intervalcheck.cpp new file mode 100644 index 0000000..e3e69b5 --- /dev/null +++ b/src/server/intervalcheck.cpp @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "intervalcheck.h" +#include "storage/datastore.h" +#include "storage/entity.h" +#include "storage/itemretrievalmanager.h" + +using namespace Akonadi::Server; + +static const int MINIMUM_AUTOSYNC_INTERVAL = 5; // minutes +static const int MINIMUM_COLTREESYNC_INTERVAL = 5; // minutes + +IntervalCheck::IntervalCheck(ItemRetrievalManager &itemRetrievalManager) + : CollectionScheduler(QStringLiteral("IntervalCheck"), QThread::IdlePriority) + , mItemRetrievalManager(itemRetrievalManager) +{ +} + +IntervalCheck::~IntervalCheck() +{ + quitThread(); +} + +void IntervalCheck::requestCollectionSync(const Collection &collection) +{ + QMetaObject::invokeMethod( + this, + [this, collection]() { + collectionExpired(collection); + }, + Qt::QueuedConnection); +} + +int IntervalCheck::collectionScheduleInterval(const Collection &collection) +{ + return collection.cachePolicyCheckInterval(); +} + +bool IntervalCheck::hasChanged(const Collection &collection, const Collection &changed) +{ + return collection.cachePolicyCheckInterval() != changed.cachePolicyCheckInterval() || collection.enabled() != changed.enabled() + || collection.syncPref() != changed.syncPref(); +} + +bool IntervalCheck::shouldScheduleCollection(const Collection &collection) +{ + return collection.cachePolicyCheckInterval() > 0 + && ((collection.syncPref() == Collection::True) || ((collection.syncPref() == Collection::Undefined) && collection.enabled())); +} + +void IntervalCheck::collectionExpired(const Collection &collection) +{ + const QDateTime now(QDateTime::currentDateTime()); + + if (collection.parentId() == 0) { + const QString resourceName = collection.resource().name(); + + const int interval = qMax(MINIMUM_COLTREESYNC_INTERVAL, collection.cachePolicyCheckInterval()); + + const QDateTime lastExpectedCheck = now.addSecs(interval * -60); + if (!mLastCollectionTreeSyncs.contains(resourceName) || mLastCollectionTreeSyncs.value(resourceName) < lastExpectedCheck) { + mLastCollectionTreeSyncs.insert(resourceName, now); + mItemRetrievalManager.triggerCollectionTreeSync(resourceName); + } + } + + // now on to the actual collection syncing + const int interval = qMax(MINIMUM_AUTOSYNC_INTERVAL, collection.cachePolicyCheckInterval()); + + const QDateTime lastExpectedCheck = now.addSecs(interval * -60); + if (mLastChecks.contains(collection.id()) && mLastChecks.value(collection.id()) > lastExpectedCheck) { + return; + } + mLastChecks.insert(collection.id(), now); + mItemRetrievalManager.triggerCollectionSync(collection.resource().name(), collection.id()); +} diff --git a/src/server/intervalcheck.h b/src/server/intervalcheck.h new file mode 100644 index 0000000..8d5f77f --- /dev/null +++ b/src/server/intervalcheck.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collectionscheduler.h" + +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class ItemRetrievalManager; + +/** + Interval checking thread. +*/ +class IntervalCheck : public CollectionScheduler +{ + Q_OBJECT + +public: + explicit IntervalCheck(ItemRetrievalManager &itemRetrievalManager); + ~IntervalCheck() override; + + /** + * Requests the given collection to be synced. + * Executed from any thread, forwards to triggerCollectionXSync() in the + * retrieval thread. + * A minimum time interval between two sync requests is ensured. + */ + void requestCollectionSync(const Collection &collection); + +protected: + int collectionScheduleInterval(const Collection &collection) override; + bool hasChanged(const Collection &collection, const Collection &changed) override; + bool shouldScheduleCollection(const Collection &collection) override; + + void collectionExpired(const Collection &collection) override; + +private: + QHash mLastChecks; + QHash mLastCollectionTreeSyncs; + ItemRetrievalManager &mItemRetrievalManager; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/main.cpp b/src/server/main.cpp new file mode 100644 index 0000000..1c40bad --- /dev/null +++ b/src/server/main.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Till Adam * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "akonadi.h" +#include "akonadifull-version.h" +#include "akonadiserver_debug.h" + +#include + +#include + +#include +#include +#include + +#include + +#include + +#ifdef QT_STATICPLUGIN + +Q_IMPORT_PLUGIN(qsqlite3) +#endif + +int main(int argc, char **argv) +{ + Q_INIT_RESOURCE(akonadidb); + AkCoreApplication app(argc, argv, AKONADISERVER_LOG()); + app.setDescription(QStringLiteral("Akonadi Server\nDo not run manually, use 'akonadictl' instead to start/stop Akonadi.")); + + // Set KAboutData so that DrKonqi can report bugs + KAboutData aboutData(QStringLiteral("akonadiserver"), + QStringLiteral("Akonadi Server"), // we don't have any localization in the server + QStringLiteral(AKONADI_FULL_VERSION), + QStringLiteral("Akonadi Server"), // we don't have any localization in the server + KAboutLicense::LGPL_V2); + KAboutData::setApplicationData(aboutData); + +#if !defined(NDEBUG) + const QCommandLineOption startWithoutControlOption(QStringLiteral("start-without-control"), + QStringLiteral("Allow to start the Akonadi server without the Akonadi control process being available")); + app.addCommandLineOptions(startWithoutControlOption); +#endif + + app.parseCommandLine(); + +#if !defined(NDEBUG) + if (!app.commandLineArguments().isSet(QStringLiteral("start-without-control")) && +#else + if (true && +#endif + !QDBusConnection::sessionBus().interface()->isServiceRegistered(Akonadi::DBus::serviceName(Akonadi::DBus::ControlLock))) { + qCCritical(AKONADISERVER_LOG) << "Akonadi control process not found - aborting."; + qCCritical(AKONADISERVER_LOG) << "If you started akonadiserver manually, try 'akonadictl start' instead."; + } + + Akonadi::Server::AkonadiServer akonadi; + // Make sure we do initialization from eventloop, otherwise + // org.freedesktop.Akonadi.upgrading service won't be registered to DBus at all + QTimer::singleShot(0, &akonadi, &Akonadi::Server::AkonadiServer::init); + + const int result = app.exec(); + + qCInfo(AKONADISERVER_LOG) << "Shutting down AkonadiServer..."; + akonadi.quit(); + + Q_CLEANUP_RESOURCE(akonadidb); + + return result; +} diff --git a/src/server/notificationmanager.cpp b/src/server/notificationmanager.cpp new file mode 100644 index 0000000..f07d5d7 --- /dev/null +++ b/src/server/notificationmanager.cpp @@ -0,0 +1,202 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + SPDX-FileCopyrightText: 2010 Michael Jansen + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationmanager.h" +#include "aggregatedfetchscope.h" +#include "akonadiserver_debug.h" +#include "handlerhelper.h" +#include "notificationsubscriber.h" +#include "storage/collectionstatistics.h" +#include "storage/notificationcollector.h" +#include "tracer.h" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +NotificationManager::NotificationManager(StartMode startMode) + : AkThread(QStringLiteral("NotificationManager"), startMode) + , mTimer(nullptr) + , mNotifyThreadPool(nullptr) + , mDebugNotifications(0) +{ +} + +NotificationManager::~NotificationManager() +{ + quitThread(); +} + +void NotificationManager::init() +{ + AkThread::init(); + + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + + mTimer = new QTimer(this); + mTimer->setInterval(settings.value(QStringLiteral("NotificationManager/Interval"), 50).toInt()); + mTimer->setSingleShot(true); + connect(mTimer, &QTimer::timeout, this, &NotificationManager::emitPendingNotifications); + + mNotifyThreadPool = new QThreadPool(this); + mNotifyThreadPool->setMaxThreadCount(5); + + mCollectionFetchScope = new AggregatedCollectionFetchScope(); + mItemFetchScope = new AggregatedItemFetchScope(); + mTagFetchScope = new AggregatedTagFetchScope(); +} + +void NotificationManager::quit() +{ + mQuitting = true; + + mTimer->stop(); + delete mTimer; + + mNotifyThreadPool->clear(); + mNotifyThreadPool->waitForDone(); + delete mNotifyThreadPool; + + qDeleteAll(mSubscribers); + + delete mCollectionFetchScope; + delete mItemFetchScope; + delete mTagFetchScope; + + AkThread::quit(); +} + +void NotificationManager::registerConnection(quintptr socketDescriptor) +{ + Q_ASSERT(thread() == QThread::currentThread()); + + auto subscriber = new NotificationSubscriber(this, socketDescriptor); + qCInfo(AKONADISERVER_LOG) << "New notification connection (registered as" << subscriber << ")"; + connect(subscriber, &NotificationSubscriber::notificationDebuggingChanged, this, [this](bool enabled) { + if (enabled) { + ++mDebugNotifications; + } else { + --mDebugNotifications; + } + Q_ASSERT(mDebugNotifications >= 0); + Q_ASSERT(mDebugNotifications <= mSubscribers.count()); + }); + + mSubscribers.push_back(subscriber); +} +void NotificationManager::forgetSubscriber(NotificationSubscriber *subscriber) +{ + Q_ASSERT(QThread::currentThread() == thread()); + mSubscribers.removeAll(subscriber); +} + +void NotificationManager::slotNotify(const Protocol::ChangeNotificationList &msgs) +{ + Q_ASSERT(QThread::currentThread() == thread()); + for (const auto &msg : msgs) { + switch (msg->type()) { + case Protocol::Command::CollectionChangeNotification: + Protocol::CollectionChangeNotification::appendAndCompress(mNotifications, msg); + continue; + case Protocol::Command::ItemChangeNotification: + case Protocol::Command::TagChangeNotification: + case Protocol::Command::RelationChangeNotification: + case Protocol::Command::SubscriptionChangeNotification: + case Protocol::Command::DebugChangeNotification: + mNotifications.push_back(msg); + continue; + + default: + Q_ASSERT_X(false, "slotNotify", "Invalid notification type!"); + continue; + } + } + + if (!mTimer->isActive()) { + mTimer->start(); + } +} + +class NotifyRunnable : public QRunnable +{ +public: + explicit NotifyRunnable(NotificationSubscriber *subscriber, const Protocol::ChangeNotificationList ¬ifications) + : mSubscriber(subscriber) + , mNotifications(notifications) + { + } + + ~NotifyRunnable() override = default; + + void run() override + { + for (const auto &ntf : std::as_const(mNotifications)) { + if (mSubscriber) { + mSubscriber->notify(ntf); + } else { + break; + } + } + } + +private: + Q_DISABLE_COPY_MOVE(NotifyRunnable) + + QPointer mSubscriber; + Protocol::ChangeNotificationList mNotifications; +}; + +void NotificationManager::emitPendingNotifications() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + if (mNotifications.isEmpty()) { + return; + } + + if (mDebugNotifications == 0) { + mSubscribers | Views::filter(IsNotNull) | Actions::forEach([this](const auto &subscriber) { + mNotifyThreadPool->start(new NotifyRunnable(subscriber, mNotifications)); + }); + } else { + // When debugging notification we have to use a non-threaded approach + // so that we can work with return value of notify() + for (const auto ¬ification : std::as_const(mNotifications)) { + QVector listeners; + for (NotificationSubscriber *subscriber : std::as_const(mSubscribers)) { + if (subscriber && subscriber->notify(notification)) { + listeners.push_back(subscriber->subscriber()); + } + } + + emitDebugNotification(notification, listeners); + } + } + + mNotifications.clear(); +} + +void NotificationManager::emitDebugNotification(const Protocol::ChangeNotificationPtr &ntf, const QVector &listeners) +{ + auto debugNtf = Protocol::DebugChangeNotificationPtr::create(); + debugNtf->setNotification(ntf); + debugNtf->setListeners(listeners); + debugNtf->setTimestamp(QDateTime::currentMSecsSinceEpoch()); + mSubscribers | Views::filter(IsNotNull) | Actions::forEach([this, &debugNtf](const auto &subscriber) { + mNotifyThreadPool->start(new NotifyRunnable(subscriber, {debugNtf})); + }); +} diff --git a/src/server/notificationmanager.h b/src/server/notificationmanager.h new file mode 100644 index 0000000..56f709e --- /dev/null +++ b/src/server/notificationmanager.h @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akthread.h" + +#include + +#include +class QTimer; + +class NotificationManagerTest; +class QThreadPool; + +namespace Akonadi +{ +namespace Server +{ +class NotificationSubscriber; +class AggregatedCollectionFetchScope; +class AggregatedItemFetchScope; +class AggregatedTagFetchScope; + +class NotificationManager : public AkThread +{ + Q_OBJECT + +public: + explicit NotificationManager(StartMode startMode = AutoStart); + ~NotificationManager() override; + + void forgetSubscriber(NotificationSubscriber *subscriber); + + AggregatedCollectionFetchScope *collectionFetchScope() const + { + return mCollectionFetchScope; + } + AggregatedItemFetchScope *itemFetchScope() const + { + return mItemFetchScope; + } + AggregatedTagFetchScope *tagFetchScope() const + { + return mTagFetchScope; + } + +public Q_SLOTS: + void registerConnection(quintptr socketDescriptor); + + void emitPendingNotifications(); + + void slotNotify(const Akonadi::Protocol::ChangeNotificationList &msgs); + +protected: + void init() override; + void quit() override; + + void emitDebugNotification(const Protocol::ChangeNotificationPtr &ntf, const QVector &listeners); + +private: + Protocol::ChangeNotificationList mNotifications; + QTimer *mTimer = nullptr; + + QThreadPool *mNotifyThreadPool = nullptr; + QVector> mSubscribers; + int mDebugNotifications; + AggregatedCollectionFetchScope *mCollectionFetchScope = nullptr; + AggregatedItemFetchScope *mItemFetchScope = nullptr; + AggregatedTagFetchScope *mTagFetchScope = nullptr; + + bool mWaiting = false; + bool mQuitting = false; + + friend class NotificationSubscriber; + friend class ::NotificationManagerTest; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/notificationsubscriber.cpp b/src/server/notificationsubscriber.cpp new file mode 100644 index 0000000..66f10f2 --- /dev/null +++ b/src/server/notificationsubscriber.cpp @@ -0,0 +1,660 @@ +/* + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationsubscriber.h" +#include "aggregatedfetchscope.h" +#include "akonadiserver_debug.h" +#include "notificationmanager.h" +#include "utils.h" + +#include +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +#define TRACE_NTF(x) +//#define TRACE_NTF(x) qCDebug(AKONADISERVER_LOG) << mSubscriber << x + +NotificationSubscriber::NotificationSubscriber(NotificationManager *manager) + : mManager(manager) + , mSocket(nullptr) + , mAllMonitored(false) + , mExclusive(false) + , mNotificationDebugging(false) +{ + if (mManager) { + mManager->itemFetchScope()->addSubscriber(); + mManager->collectionFetchScope()->addSubscriber(); + mManager->tagFetchScope()->addSubscriber(); + } +} + +NotificationSubscriber::NotificationSubscriber(NotificationManager *manager, quintptr socketDescriptor) + : NotificationSubscriber(manager) +{ + mSocket = new QLocalSocket(this); + connect(mSocket, &QLocalSocket::readyRead, this, &NotificationSubscriber::handleIncomingData); + connect(mSocket, &QLocalSocket::disconnected, this, &NotificationSubscriber::socketDisconnected); + mSocket->setSocketDescriptor(socketDescriptor); + + const SchemaVersion schema = SchemaVersion::retrieveAll().at(0); + + auto hello = Protocol::HelloResponsePtr::create(); + hello->setServerName(QStringLiteral("Akonadi")); + hello->setMessage(QStringLiteral("Not really IMAP server")); + hello->setProtocolVersion(Protocol::version()); + hello->setGeneration(schema.generation()); + writeCommand(0, hello); +} + +NotificationSubscriber::~NotificationSubscriber() +{ + QMutexLocker locker(&mLock); + + if (mNotificationDebugging) { + Q_EMIT notificationDebuggingChanged(false); + } +} + +void NotificationSubscriber::handleIncomingData() +{ + while (mSocket->bytesAvailable() > static_cast(sizeof(qint64))) { + Protocol::DataStream stream(mSocket); + + // Ignored atm + qint64 tag = -1; + stream >> tag; + + Protocol::CommandPtr cmd; + try { + cmd = Protocol::deserialize(mSocket); + } catch (const Akonadi::ProtocolException &e) { + qCWarning(AKONADISERVER_LOG) << "ProtocolException while reading from notification bus for" << mSubscriber << ":" << e.what(); + disconnectSubscriber(); + return; + } catch (const std::exception &e) { + qCWarning(AKONADISERVER_LOG) << "Unknown exception while reading from notification bus for" << mSubscriber << ":" << e.what(); + disconnectSubscriber(); + return; + } + if (cmd->type() == Protocol::Command::Invalid) { + qCWarning(AKONADISERVER_LOG) << "Invalid command while reading from notification bus for " << mSubscriber << ", resetting connection"; + disconnectSubscriber(); + return; + } + + switch (cmd->type()) { + case Protocol::Command::CreateSubscription: + registerSubscriber(Protocol::cmdCast(cmd)); + writeCommand(tag, Protocol::CreateSubscriptionResponsePtr::create()); + break; + case Protocol::Command::ModifySubscription: + if (mSubscriber.isEmpty()) { + qCWarning(AKONADISERVER_LOG) << "Notification subscriber received ModifySubscription command before RegisterSubscriber"; + disconnectSubscriber(); + return; + } + modifySubscription(Protocol::cmdCast(cmd)); + writeCommand(tag, Protocol::ModifySubscriptionResponsePtr::create()); + break; + case Protocol::Command::Logout: + disconnectSubscriber(); + break; + default: + qCWarning(AKONADISERVER_LOG) << "Notification subscriber for" << mSubscriber << "received an invalid command" << cmd->type(); + disconnectSubscriber(); + break; + } + } +} + +void NotificationSubscriber::socketDisconnected() +{ + qCInfo(AKONADISERVER_LOG) << "Subscriber" << mSubscriber << "disconnected"; + disconnectSubscriber(); +} + +void NotificationSubscriber::disconnectSubscriber() +{ + QMutexLocker locker(&mLock); + + auto changeNtf = Protocol::SubscriptionChangeNotificationPtr::create(); + changeNtf->setSubscriber(mSubscriber); + changeNtf->setSessionId(mSession); + changeNtf->setOperation(Protocol::SubscriptionChangeNotification::Remove); + mManager->slotNotify({changeNtf}); + + if (mSocket) { + disconnect(mSocket, &QLocalSocket::disconnected, this, &NotificationSubscriber::socketDisconnected); + mSocket->close(); + } + + // Unregister ourselves from the aggregated collection fetch scope + auto cfs = mManager->collectionFetchScope(); + cfs->apply(mCollectionFetchScope, Protocol::CollectionFetchScope()); + cfs->removeSubscriber(); + + auto tfs = mManager->tagFetchScope(); + tfs->apply(mTagFetchScope, Protocol::TagFetchScope()); + tfs->removeSubscriber(); + + auto ifs = mManager->itemFetchScope(); + ifs->apply(mItemFetchScope, Protocol::ItemFetchScope()); + ifs->removeSubscriber(); + + mManager->forgetSubscriber(this); + deleteLater(); +} + +void NotificationSubscriber::registerSubscriber(const Protocol::CreateSubscriptionCommand &command) +{ + QMutexLocker locker(&mLock); + + qCInfo(AKONADISERVER_LOG) << "Subscriber" << this << "identified as" << command.subscriberName(); + mSubscriber = command.subscriberName(); + mSession = command.session(); + + auto changeNtf = Protocol::SubscriptionChangeNotificationPtr::create(); + changeNtf->setSubscriber(mSubscriber); + changeNtf->setSessionId(mSession); + changeNtf->setOperation(Protocol::SubscriptionChangeNotification::Add); + mManager->slotNotify({changeNtf}); +} + +static QStringList canonicalMimeTypes(const QStringList &mimes) +{ + static QMimeDatabase sMimeDatabase; + QStringList ret; + ret.reserve(mimes.count()); + auto canonicalMime = [](const QString &mime) { + return sMimeDatabase.mimeTypeForName(mime).name(); + }; + std::transform(mimes.begin(), mimes.end(), std::back_inserter(ret), canonicalMime); + return ret; +} + +void NotificationSubscriber::modifySubscription(const Protocol::ModifySubscriptionCommand &command) +{ + QMutexLocker locker(&mLock); + + const auto modifiedParts = command.modifiedParts(); + +#define START_MONITORING(type) \ + (modifiedParts & Protocol::ModifySubscriptionCommand::ModifiedParts(Protocol::ModifySubscriptionCommand::type | Protocol::ModifySubscriptionCommand::Add)) +#define STOP_MONITORING(type) \ + (modifiedParts \ + & Protocol::ModifySubscriptionCommand::ModifiedParts(Protocol::ModifySubscriptionCommand::type | Protocol::ModifySubscriptionCommand::Remove)) + +#define APPEND(set, newItems) \ + Q_FOREACH (const auto &entity, (newItems)) { \ + (set).insert(entity); \ + } + +#define REMOVE(set, items) \ + Q_FOREACH (const auto &entity, (items)) { \ + (set).remove(entity); \ + } + + if (START_MONITORING(Types)) { + APPEND(mMonitoredTypes, command.startMonitoringTypes()) + } + if (STOP_MONITORING(Types)) { + REMOVE(mMonitoredTypes, command.stopMonitoringTypes()) + } + if (START_MONITORING(Collections)) { + APPEND(mMonitoredCollections, command.startMonitoringCollections()) + } + if (STOP_MONITORING(Collections)) { + REMOVE(mMonitoredCollections, command.stopMonitoringCollections()) + } + if (START_MONITORING(Items)) { + APPEND(mMonitoredItems, command.startMonitoringItems()) + } + if (STOP_MONITORING(Items)) { + REMOVE(mMonitoredItems, command.stopMonitoringItems()) + } + if (START_MONITORING(Tags)) { + APPEND(mMonitoredTags, command.startMonitoringTags()) + } + if (STOP_MONITORING(Tags)) { + REMOVE(mMonitoredTags, command.stopMonitoringTags()) + } + if (START_MONITORING(Resources)) { + APPEND(mMonitoredResources, command.startMonitoringResources()) + } + if (STOP_MONITORING(Resources)) { + REMOVE(mMonitoredResources, command.stopMonitoringResources()) + } + if (START_MONITORING(MimeTypes)) { + APPEND(mMonitoredMimeTypes, canonicalMimeTypes(command.startMonitoringMimeTypes())) + } + if (STOP_MONITORING(MimeTypes)) { + REMOVE(mMonitoredMimeTypes, canonicalMimeTypes(command.stopMonitoringMimeTypes())) + } + if (START_MONITORING(Sessions)) { + APPEND(mIgnoredSessions, command.startIgnoringSessions()) + } + if (STOP_MONITORING(Sessions)) { + REMOVE(mIgnoredSessions, command.stopIgnoringSessions()) + } + if (modifiedParts & Protocol::ModifySubscriptionCommand::AllFlag) { + mAllMonitored = command.allMonitored(); + } + if (modifiedParts & Protocol::ModifySubscriptionCommand::ExclusiveFlag) { + mExclusive = command.isExclusive(); + } + if (modifiedParts & Protocol::ModifySubscriptionCommand::ItemFetchScope) { + const auto newScope = command.itemFetchScope(); + mManager->itemFetchScope()->apply(mItemFetchScope, newScope); + mItemFetchScope = newScope; + } + if (modifiedParts & Protocol::ModifySubscriptionCommand::CollectionFetchScope) { + const auto newScope = command.collectionFetchScope(); + mManager->collectionFetchScope()->apply(mCollectionFetchScope, newScope); + mCollectionFetchScope = newScope; + } + if (modifiedParts & Protocol::ModifySubscriptionCommand::TagFetchScope) { + const auto newScope = command.tagFetchScope(); + mManager->tagFetchScope()->apply(mTagFetchScope, newScope); + mTagFetchScope = newScope; + if (!newScope.fetchIdOnly()) { + Q_ASSERT(!mManager->tagFetchScope()->fetchIdOnly()); + } + } + + if (mManager) { + if (modifiedParts & Protocol::ModifySubscriptionCommand::Types) { + // Did the caller just subscribed to subscription changes? + if (command.startMonitoringTypes().contains(Protocol::ModifySubscriptionCommand::SubscriptionChanges)) { + // If yes, then send them list of all existing subscribers + mManager->mSubscribers | Views::filter(IsNotNull) | Actions::forEach([this](const auto &subscriber) { + QMetaObject::invokeMethod(this, + "notify", + Qt::QueuedConnection, + Q_ARG(Akonadi::Protocol::ChangeNotificationPtr, subscriber->toChangeNotification())); + }); + } + if (command.startMonitoringTypes().contains(Protocol::ModifySubscriptionCommand::ChangeNotifications)) { + if (!mNotificationDebugging) { + mNotificationDebugging = true; + Q_EMIT notificationDebuggingChanged(true); + } + } else if (command.stopMonitoringTypes().contains(Protocol::ModifySubscriptionCommand::ChangeNotifications)) { + if (mNotificationDebugging) { + mNotificationDebugging = false; + Q_EMIT notificationDebuggingChanged(false); + } + } + } + + // Emit subscription change notification + auto changeNtf = toChangeNotification(); + changeNtf->setOperation(Protocol::SubscriptionChangeNotification::Modify); + mManager->slotNotify({changeNtf}); + } + +#undef START_MONITORING +#undef STOP_MONITORING +#undef APPEND +#undef REMOVE +} + +Protocol::SubscriptionChangeNotificationPtr NotificationSubscriber::toChangeNotification() const +{ + // Assumes mLock being locked by caller + + auto ntf = Protocol::SubscriptionChangeNotificationPtr::create(); + ntf->setSessionId(mSession); + ntf->setSubscriber(mSubscriber); + ntf->setOperation(Protocol::SubscriptionChangeNotification::Add); + ntf->setCollections(mMonitoredCollections); + ntf->setItems(mMonitoredItems); + ntf->setTags(mMonitoredTags); + ntf->setTypes(mMonitoredTypes); + ntf->setMimeTypes(mMonitoredMimeTypes); + ntf->setResources(mMonitoredResources); + ntf->setIgnoredSessions(mIgnoredSessions); + ntf->setAllMonitored(mAllMonitored); + ntf->setExclusive(mExclusive); + ntf->setItemFetchScope(mItemFetchScope); + ntf->setTagFetchScope(mTagFetchScope); + ntf->setCollectionFetchScope(mCollectionFetchScope); + return ntf; +} + +bool NotificationSubscriber::isCollectionMonitored(Entity::Id id) const +{ + // Assumes mLock being locked by caller + + if (id < 0) { + return false; + } + + return mMonitoredCollections.contains(id) || mMonitoredCollections.contains(0); +} + +bool NotificationSubscriber::isMimeTypeMonitored(const QString &mimeType) const +{ + // Assumes mLock being locked by caller + + // KContacts::Addressee::mimeType() unfortunately uses an alias + if (mimeType == QLatin1String("text/directory")) { + return mMonitoredMimeTypes.contains(QStringLiteral("text/vcard")); + } + return mMonitoredMimeTypes.contains(mimeType); +} + +bool NotificationSubscriber::isMoveDestinationResourceMonitored(const Protocol::ItemChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + if (msg.operation() != Protocol::ItemChangeNotification::Move) { + return false; + } + return mMonitoredResources.contains(msg.destinationResource()); +} + +bool NotificationSubscriber::isMoveDestinationResourceMonitored(const Protocol::CollectionChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + if (msg.operation() != Protocol::CollectionChangeNotification::Move) { + return false; + } + return mMonitoredResources.contains(msg.destinationResource()); +} + +bool NotificationSubscriber::acceptsItemNotification(const Protocol::ItemChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + if (msg.items().isEmpty()) { + return false; + } + + if (mAllMonitored) { + TRACE_NTF("ACCEPTS ITEM: all monitored"); + return true; + } + + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::ItemChanges)) { + TRACE_NTF("ACCEPTS ITEM: REJECTED - Item changes not monitored"); + return false; + } + + // we have a resource or mimetype filter + if (!mMonitoredResources.isEmpty() || !mMonitoredMimeTypes.isEmpty()) { + if (mMonitoredResources.contains(msg.resource())) { + TRACE_NTF("ACCEPTS ITEM: ACCEPTED - resource monitored"); + return true; + } + + if (isMoveDestinationResourceMonitored(msg)) { + TRACE_NTF("ACCEPTS ITEM: ACCEPTED: move destination monitored"); + return true; + } + + Q_FOREACH (const auto &item, msg.items()) { + if (isMimeTypeMonitored(item.mimeType())) { + TRACE_NTF("ACCEPTS ITEM: ACCEPTED - mimetype monitored"); + return true; + } + } + + TRACE_NTF("ACCEPTS ITEM: REJECTED: resource nor mimetype monitored"); + return false; + } + + // we explicitly monitor that item or the collections it's in + Q_FOREACH (const auto &item, msg.items()) { + if (mMonitoredItems.contains(item.id())) { + TRACE_NTF("ACCEPTS ITEM: ACCEPTED: item explicitly monitored"); + return true; + } + } + + if (isCollectionMonitored(msg.parentCollection())) { + TRACE_NTF("ACCEPTS ITEM: ACCEPTED: parent collection monitored"); + return true; + } + if (isCollectionMonitored(msg.parentDestCollection())) { + TRACE_NTF("ACCEPTS ITEM: ACCEPTED: destination collection monitored"); + return true; + } + + TRACE_NTF("ACCEPTS ITEM: REJECTED"); + return false; +} + +bool NotificationSubscriber::acceptsCollectionNotification(const Protocol::CollectionChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + const auto &collection = msg.collection(); + if (collection.id() < 0) { + return false; + } + + // HACK: We need to dispatch notifications about disabled collections to SOME + // agents (that's what we have the exclusive subscription for) - but because + // querying each Collection from database would be expensive, we use the + // metadata hack to transfer this information from NotificationCollector + if (msg.metadata().contains("DISABLED") && (msg.operation() != Protocol::CollectionChangeNotification::Unsubscribe) + && !msg.changedParts().contains("ENABLED")) { + // Exclusive subscriber always gets it + // If the subscriber is not exclusive (i.e. if we got here), then the subscriber does + // not care about this one, so drop it + return mExclusive; + } + + if (mAllMonitored) { + return true; + } + + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::CollectionChanges)) { + return false; + } + + // we have a resource filter + if (!mMonitoredResources.isEmpty()) { + const bool resourceMatches = mMonitoredResources.contains(msg.resource()) || isMoveDestinationResourceMonitored(msg); + + // a bit hacky, but match the behaviour from the item case, + // if resource is the only thing we are filtering on, stop here, and if the resource filter matched, of course + if (mMonitoredMimeTypes.isEmpty() || resourceMatches) { + return resourceMatches; + } + // else continue + } + + // we explicitly monitor that collection, or all of them + if (isCollectionMonitored(collection.id())) { + return true; + } + + return isCollectionMonitored(msg.parentCollection()) || isCollectionMonitored(msg.parentDestCollection()); +} + +bool NotificationSubscriber::acceptsTagNotification(const Protocol::TagChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + if (msg.tag().id() < 0) { + return false; + } + + // Special handling for Tag removal notifications: When a Tag is removed, + // a notification is emitted for each Resource that owns the tag (i.e. + // each resource that owns a Tag RID - Tag RIDs are resource-specific). + // Additionally then we send one more notification without any RID that is + // destined for regular applications (which don't know anything about Tag RIDs) + if (msg.operation() == Protocol::TagChangeNotification::Remove) { + // HACK: Since have no way to determine which resource this NotificationSource + // belongs to, we are abusing the fact that each resource ignores it's own + // main session, which is called the same name as the resource. + + // If there are any ignored sessions, but this notification does not have + // a specific resource set, then we ignore it, as this notification is + // for clients, not resources (does not have tag RID) + if (!mIgnoredSessions.isEmpty() && msg.resource().isEmpty()) { + return false; + } + + // If this source ignores a session (i.e. we assume it is a resource), + // but this notification is for another resource, then we ignore it + if (!msg.resource().isEmpty() && !mIgnoredSessions.contains(msg.resource())) { + return false; + } + // Now we got here, which means that this notification either has empty + // resource, i.e. it is destined for a client applications, or it's + // destined for resource that we *think* (see the hack above) this + // NotificationSource belongs too. Which means we approve this notification, + // but it can still be discarded in the generic Tag notification filter + // below + } + + if (mAllMonitored) { + return true; + } + + if (!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::TagChanges)) { + return false; + } + + if (mMonitoredTags.isEmpty()) { + return true; + } + + if (mMonitoredTags.contains(msg.tag().id())) { + return true; + } + + return true; +} + +bool NotificationSubscriber::acceptsRelationNotification(const Protocol::RelationChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + Q_UNUSED(msg) + + if (mAllMonitored) { + return true; + } + + return !(!mMonitoredTypes.isEmpty() && !mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::RelationChanges)); +} + +bool NotificationSubscriber::acceptsSubscriptionNotification(const Protocol::SubscriptionChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + Q_UNUSED(msg) + + // Unlike other types, subscription notifications must be explicitly enabled + // by caller and are excluded from "monitor all" as well + return mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::SubscriptionChanges); +} + +bool NotificationSubscriber::acceptsDebugChangeNotification(const Protocol::DebugChangeNotification &msg) const +{ + // Assumes mLock being locked by caller + + // We should never end up sending debug notification about a debug notification. + // This could get very messy very quickly... + Q_ASSERT(msg.notification()->type() != Protocol::Command::DebugChangeNotification); + if (msg.notification()->type() == Protocol::Command::DebugChangeNotification) { + return false; + } + + // Unlike other types, debug change notifications must be explicitly enabled + // by caller and are excluded from "monitor all" as well + return mMonitoredTypes.contains(Protocol::ModifySubscriptionCommand::ChangeNotifications); +} + +bool NotificationSubscriber::acceptsNotification(const Protocol::ChangeNotification &msg) const +{ + // Assumes mLock being locked + + // Uninitialized subscriber gets nothing + if (mSubscriber.isEmpty()) { + return false; + } + + // session is ignored + // TODO: Should this affect SubscriptionChangeNotification and DebugChangeNotification? + if (mIgnoredSessions.contains(msg.sessionId())) { + return false; + } + + switch (msg.type()) { + case Protocol::Command::ItemChangeNotification: + return acceptsItemNotification(static_cast(msg)); + case Protocol::Command::CollectionChangeNotification: + return acceptsCollectionNotification(static_cast(msg)); + case Protocol::Command::TagChangeNotification: + return acceptsTagNotification(static_cast(msg)); + case Protocol::Command::RelationChangeNotification: + return acceptsRelationNotification(static_cast(msg)); + case Protocol::Command::SubscriptionChangeNotification: + return acceptsSubscriptionNotification(static_cast(msg)); + case Protocol::Command::DebugChangeNotification: + return acceptsDebugChangeNotification(static_cast(msg)); + + default: + qCWarning(AKONADISERVER_LOG) << "NotificationSubscriber" << mSubscriber << "received an invalid notification type" << msg.type(); + return false; + } +} + +bool NotificationSubscriber::notify(const Protocol::ChangeNotificationPtr ¬ification) +{ + // Guard against this object being deleted while we are waiting for the lock + QPointer ptr(this); + QMutexLocker locker(&mLock); + if (!ptr) { + return false; + } + + if (acceptsNotification(*notification)) { + QMetaObject::invokeMethod(this, "writeNotification", Qt::QueuedConnection, Q_ARG(Akonadi::Protocol::ChangeNotificationPtr, notification)); + return true; + } + return false; +} + +void NotificationSubscriber::writeNotification(const Protocol::ChangeNotificationPtr ¬ification) +{ + // tag chosen by fair dice roll + writeCommand(4, notification); +} + +void NotificationSubscriber::writeCommand(qint64 tag, const Protocol::CommandPtr &cmd) +{ + Q_ASSERT(QThread::currentThread() == thread()); + + Protocol::DataStream stream(mSocket); + stream << tag; + try { + Protocol::serialize(stream, cmd); + stream.flush(); + if (!mSocket->waitForBytesWritten()) { + if (mSocket->state() == QLocalSocket::ConnectedState) { + qCWarning(AKONADISERVER_LOG) << "NotificationSubscriber for" << mSubscriber << ": timeout writing into stream"; + } else { + // client has disconnected, just discard the message + } + } + } catch (const ProtocolException &e) { + qCWarning(AKONADISERVER_LOG) << "ProtocolException while writing into stream for subscriber" << mSubscriber << ":" << e.what(); + } +} diff --git a/src/server/notificationsubscriber.h b/src/server/notificationsubscriber.h new file mode 100644 index 0000000..47045e6 --- /dev/null +++ b/src/server/notificationsubscriber.h @@ -0,0 +1,105 @@ +/* + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include "entities.h" +#include + +class QLocalSocket; + +namespace Akonadi +{ +namespace Server +{ +class NotificationManager; + +class NotificationSubscriber : public QObject +{ + Q_OBJECT + +public: + explicit NotificationSubscriber(NotificationManager *manager, quintptr socketDescriptor); + ~NotificationSubscriber(); + + Q_REQUIRED_RESULT inline QByteArray subscriber() const + { + return mSubscriber; + } + + Q_REQUIRED_RESULT QLocalSocket *socket() const + { + return mSocket; + } + + void handleIncomingData(); + +public Q_SLOTS: + bool notify(const Akonadi::Protocol::ChangeNotificationPtr ¬ification); + +private Q_SLOTS: + void socketDisconnected(); + +Q_SIGNALS: + void notificationDebuggingChanged(bool enabled); + +protected: + void registerSubscriber(const Protocol::CreateSubscriptionCommand &command); + void modifySubscription(const Protocol::ModifySubscriptionCommand &command); + void disconnectSubscriber(); + +private: + bool acceptsNotification(const Protocol::ChangeNotification ¬ification) const; + bool acceptsItemNotification(const Protocol::ItemChangeNotification ¬ification) const; + bool acceptsCollectionNotification(const Protocol::CollectionChangeNotification ¬ification) const; + bool acceptsTagNotification(const Protocol::TagChangeNotification ¬ification) const; + bool acceptsRelationNotification(const Protocol::RelationChangeNotification ¬ification) const; + bool acceptsSubscriptionNotification(const Protocol::SubscriptionChangeNotification ¬ification) const; + bool acceptsDebugChangeNotification(const Protocol::DebugChangeNotification ¬ification) const; + + bool isCollectionMonitored(Entity::Id id) const; + bool isMimeTypeMonitored(const QString &mimeType) const; + bool isMoveDestinationResourceMonitored(const Protocol::ItemChangeNotification &msg) const; + bool isMoveDestinationResourceMonitored(const Protocol::CollectionChangeNotification &msg) const; + + Protocol::SubscriptionChangeNotificationPtr toChangeNotification() const; + +protected Q_SLOTS: + virtual void writeNotification(const Akonadi::Protocol::ChangeNotificationPtr ¬ification); + +protected: + explicit NotificationSubscriber(NotificationManager *manager = nullptr); + + void writeCommand(qint64 tag, const Protocol::CommandPtr &cmd); + + mutable QMutex mLock; + NotificationManager *mManager = nullptr; + QLocalSocket *mSocket = nullptr; + QByteArray mSubscriber; + QSet mMonitoredCollections; + QSet mMonitoredItems; + QSet mMonitoredTags; + QSet mMonitoredTypes; + QSet mMonitoredMimeTypes; + QSet mMonitoredResources; + QSet mIgnoredSessions; + QByteArray mSession; + Protocol::ItemFetchScope mItemFetchScope; + Protocol::CollectionFetchScope mCollectionFetchScope; + Protocol::TagFetchScope mTagFetchScope; + bool mAllMonitored; + bool mExclusive; + bool mNotificationDebugging; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/preprocessorinstance.cpp b/src/server/preprocessorinstance.cpp new file mode 100644 index 0000000..53ff482 --- /dev/null +++ b/src/server/preprocessorinstance.cpp @@ -0,0 +1,208 @@ +/****************************************************************************** + * + * File : preprocessorinstance.cpp + * Creation date : Sat 18 Jul 2009 02:50:39 + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#include "preprocessorinstance.h" +#include "akonadiserver_debug.h" +#include "preprocessorinterface.h" +#include "preprocessormanager.h" + +#include "entities.h" + +#include "agentcontrolinterface.h" +#include "agentmanagerinterface.h" + +#include "tracer.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +PreprocessorInstance::PreprocessorInstance(const QString &id, PreprocessorManager &manager, Tracer &tracer) + : mManager(manager) + , mTracer(tracer) + , mId(id) +{ + Q_ASSERT(!id.isEmpty()); +} + +PreprocessorInstance::~PreprocessorInstance() = default; + +bool PreprocessorInstance::init() +{ + Q_ASSERT(!mBusy); // must be called very early + Q_ASSERT(!mInterface); + + mInterface = new OrgFreedesktopAkonadiPreprocessorInterface(DBus::agentServiceName(mId, DBus::Preprocessor), + QStringLiteral("/Preprocessor"), + QDBusConnection::sessionBus(), + this); + + if (!mInterface || !mInterface->isValid()) { + mTracer.warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Could not connect to pre-processor instance '%1': %2").arg(mId, mInterface ? mInterface->lastError().message() : QString())); + delete mInterface; + mInterface = nullptr; + return false; + } + + QObject::connect(mInterface, &OrgFreedesktopAkonadiPreprocessorInterface::itemProcessed, this, &PreprocessorInstance::itemProcessed); + + return true; +} + +void PreprocessorInstance::enqueueItem(qint64 itemId) +{ + qCDebug(AKONADISERVER_LOG) << "PreprocessorInstance::enqueueItem(" << itemId << ")"; + + mItemQueue.push_back(itemId); + + // If the preprocessor is already busy processing another item then do nothing. + if (mBusy) { + // The "head" item is the one being processed and we have just added another one. + Q_ASSERT(mItemQueue.size() > 1); + return; + } + + // Not busy: handle the item. + processHeadItem(); +} + +void PreprocessorInstance::processHeadItem() +{ + // We shouldn't be called if there are no items in the queue + Q_ASSERT(!mItemQueue.empty()); + // We shouldn't be here with no interface + Q_ASSERT(mInterface); + + qint64 itemId = mItemQueue.front(); + + // Fetch the actual item data (as it may have changed since it was enqueued) + // The fetch will hit the cache if the item wasn't changed. + + PimItem actualItem = PimItem::retrieveById(itemId); + + while (!actualItem.isValid()) { + // hum... item is gone ? + mManager.preProcessorFinishedHandlingItem(this, itemId); + + mItemQueue.pop_front(); + if (mItemQueue.empty()) { + // nothing more to process for this instance: jump out + mBusy = false; + return; + } + + // try the next one in the queue + itemId = mItemQueue.front(); + actualItem = PimItem::retrieveById(itemId); + } + + // Ok.. got a valid item to process: collection and mimetype is known. + + qCDebug(AKONADISERVER_LOG) << "PreprocessorInstance::processHeadItem(): about to begin processing item " << itemId; + + mBusy = true; + + mItemProcessingStartDateTime = QDateTime::currentDateTime(); + + // The beginProcessItem() D-Bus call is asynchronous (marked with NoReply attribute) + mInterface->beginProcessItem(itemId, actualItem.collectionId(), actualItem.mimeType().name()); + + qCDebug(AKONADISERVER_LOG) << "PreprocessorInstance::processHeadItem(): processing started for item " << itemId; +} + +qint64 PreprocessorInstance::currentProcessingTime() +{ + if (!mBusy) { + return -1; // nothing being processed + } + + return mItemProcessingStartDateTime.secsTo(QDateTime::currentDateTime()); +} + +bool PreprocessorInstance::abortProcessing() +{ + Q_ASSERT_X(mBusy, "PreprocessorInstance::abortProcessing()", "You shouldn't call this method when isBusy() returns false"); + + OrgFreedesktopAkonadiAgentControlInterface iface(DBus::agentServiceName(mId, DBus::Agent), QStringLiteral("/"), QDBusConnection::sessionBus(), this); + + if (!iface.isValid()) { + mTracer.warning(QStringLiteral("PreprocessorInstance"), + QStringLiteral("Could not connect to pre-processor instance '%1': %2").arg(mId, iface.lastError().message())); + return false; + } + + // We don't check the return value.. as this is a "warning" + // The preprocessor manager will check again in a while and eventually + // terminate the agent at all... + iface.abort(); + + return true; +} + +bool PreprocessorInstance::invokeRestart() +{ + Q_ASSERT_X(mBusy, "PreprocessorInstance::invokeRestart()", "You shouldn't call this method when isBusy() returns false"); + + OrgFreedesktopAkonadiAgentManagerInterface iface(DBus::serviceName(DBus::Control), QStringLiteral("/AgentManager"), QDBusConnection::sessionBus(), this); + + if (!iface.isValid()) { + mTracer.warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Could not connect to the AgentManager in order to restart pre-processor instance '%1': %2").arg(mId, iface.lastError().message())); + return false; + } + + iface.restartAgentInstance(mId); + + return true; +} + +void PreprocessorInstance::itemProcessed(qlonglong id) +{ + qCDebug(AKONADISERVER_LOG) << "PreprocessorInstance::itemProcessed(" << id << ")"; + + // We shouldn't be called if there are no items in the queue + if (mItemQueue.empty()) { + mTracer.warning(QStringLiteral("PreprocessorInstance"), + QStringLiteral("Pre-processor instance '%1' emitted itemProcessed(%2) but we actually have no item in the queue").arg(mId).arg(id)); + mBusy = false; + return; // preprocessor is buggy (FIXME: What now ?) + } + + // We should be busy now: this is more likely our fault, not the preprocessor's one. + Q_ASSERT(mBusy); + + qlonglong itemId = mItemQueue.front(); + + if (itemId != id) { + mTracer.warning( + QStringLiteral("PreprocessorInstance"), + QStringLiteral("Pre-processor instance '%1' emitted itemProcessed(%2) but the head item in the queue has id %3").arg(mId).arg(id).arg(itemId)); + + // FIXME: And what now ? + } + + mItemQueue.pop_front(); + + mManager.preProcessorFinishedHandlingItem(this, itemId); + + if (mItemQueue.empty()) { + // Nothing more to do + mBusy = false; + return; + } + + // Stay busy and process next item in the queue + processHeadItem(); +} diff --git a/src/server/preprocessorinstance.h b/src/server/preprocessorinstance.h new file mode 100644 index 0000000..3e44173 --- /dev/null +++ b/src/server/preprocessorinstance.h @@ -0,0 +1,186 @@ +/****************************************************************************** + * + * File : preprocessorinstance.h + * Creation date : Sat 18 Jul 2009 02:50:39 + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#pragma once + +#include +#include + +#include + +class OrgFreedesktopAkonadiPreprocessorInterface; + +namespace Akonadi +{ +namespace Server +{ +class PreprocessorManager; +class AgentInstance; +class Tracer; + +/** + * A single preprocessor (agent) instance. + * + * Most of the interface of this class is protected and is exposed only + * to PreprocessorManager (singleton). + * + * This class is NOT thread safe. The caller is responsible of protecting + * against concurrent access. + */ +class PreprocessorInstance : public QObject +{ + friend class PreprocessorManager; + + Q_OBJECT + +protected: + /** + * Create an instance of a PreprocessorInstance descriptor. + */ + PreprocessorInstance(const QString &id, PreprocessorManager &manager, Tracer &tracer); + +public: // This is public only for qDeleteAll() called from PreprocessorManager + // ...for some reason couldn't convince gcc to have it as friend... + + /** + * Destroy this instance of the PreprocessorInstance descriptor. + */ + ~PreprocessorInstance(); + +private: + PreprocessorManager &mManager; + Tracer &mTracer; + + /** + * The internal queue if item identifiers. + * The head item in the queue is the one currently being processed. + * The other ones are waiting. + */ + std::deque mItemQueue; + + /** + * Is this processor busy ? + * This, in fact, *should* be equivalent to "mItemQueue.count() > 0" + * as the head item in the queue is the one being processed now. + */ + bool mBusy = false; + + /** + * The date-time at that we have started processing the current + * item in the queue. This is used to compute the processing time + * and eventually spot a "dead" preprocessor (which takes longer + * than N minutes to process an item). + */ + QDateTime mItemProcessingStartDateTime; + + /** + * The id of this preprocessor instance. This is actually + * the AgentInstance identifier. + */ + QString mId; + + /** + * The preprocessor D-Bus interface. Owned. + */ + OrgFreedesktopAkonadiPreprocessorInterface *mInterface = nullptr; + +protected: + /** + * This is called by PreprocessorManager just after the construction + * in order to connect to the preprocessor instance via D-Bus. + * In case of failure this object should be destroyed as it can't + * operate properly. The error message is printed via Tracer. + */ + bool init(); + + /** + * Returns true if this preprocessor instance is currently processing an item. + * That is: if we have called "processItem()" on it and it hasn't emitted + * itemProcessed() yet. + */ + bool isBusy() const + { + return mBusy; + } + + /** + * Returns the time in seconds elapsed since the current item was submitted + * to the slave preprocessor instance. If no item is currently being + * processed then this function returns -1; + */ + qint64 currentProcessingTime(); + + /** + * Returns the id of this preprocessor. This is actually + * the AgentInstance identifier but it's not a requirement. + */ + const QString &id() const + { + return mId; + } + + /** + * Returns a pointer to the internal preprocessor instance + * item queue. Don't mess with it unless you *really* know + * what you're doing. Use enqueueItem() to add an item + * to the queue. This method is provided to the PreprocessorManager + * to take over the item queue of a dying preprocessor. + * + * The returned pointer is granted to be non null. + */ + std::deque *itemQueue() + { + return &mItemQueue; + } + + /** + * This is called by PreprocessorManager to enqueue a PimItem + * for processing by this preprocessor instance. + */ + void enqueueItem(qint64 itemId); + + /** + * Attempts to abort the processing of the current item. + * May be called only if isBusy() returns true and an assertion + * will remind you of that. + * Returns true if the abort request was successfully sent + * (but not necessarily handled by the slave) and false + * if the request couldn't be sent for some reason. + */ + bool abortProcessing(); + + /** + * Attempts to invoke the preprocessor slave restart via + * AgentManager. This is the "last resort" action before + * starting to ignore the preprocessor (after it misbehaved). + */ + bool invokeRestart(); + +private: + /** + * This function starts processing of the first item in mItemQueue. + * It's only used internally. + */ + void processHeadItem(); + +private Q_SLOTS: + + /** + * This is invoked to signal that the processing of the current (head) + * item has terminated and the next item should be processed. + */ + void itemProcessed(qlonglong id); + +}; // class PreprocessorInstance + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/preprocessormanager.cpp b/src/server/preprocessormanager.cpp new file mode 100644 index 0000000..9041a47 --- /dev/null +++ b/src/server/preprocessormanager.cpp @@ -0,0 +1,434 @@ +/****************************************************************************** + * + * File : preprocessormanager.cpp + * Creation date : Sat 18 Jul 2009 01:58:50 + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#include "preprocessormanager.h" +#include "akonadiserver_debug.h" + +#include "akonadi.h" +#include "entities.h" // Akonadi::Server::PimItem +#include "storage/datastore.h" +#include "tracer.h" + +#include "preprocessormanageradaptor.h" + +namespace Akonadi +{ +namespace Server +{ +const int gHeartbeatTimeoutInMSecs = 30000; // 30 sec heartbeat + +// 2 minutes should be really enough to process an item. +// After this timeout elapses we assume that the preprocessor +// is "stuck" and we attempt to kick it by requesting an abort(). +const int gWarningItemProcessingTimeInSecs = 120; +// After 3 minutes, if the preprocessor is still "stuck" then +// we attempt to restart it via AgentManager.... +const int gMaximumItemProcessingTimeInSecs = 180; +// After 4 minutes, if the preprocessor is still "stuck" then +// we assume it's dead and just drop it's interface. +const int gDeadlineItemProcessingTimeInSecs = 240; + +} // namespace Server +} // namespace Akonadi + +using namespace Akonadi::Server; + +// The one and only PreprocessorManager object +PreprocessorManager::PreprocessorManager(Tracer &tracer) + : mEnabled(true) + , mTracer(tracer) +{ + // Hook in our D-Bus interface "shell". + new PreprocessorManagerAdaptor(this); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/PreprocessorManager"), this, QDBusConnection::ExportAdaptors); + + mHeartbeatTimer = new QTimer(this); + + QObject::connect(mHeartbeatTimer, &QTimer::timeout, this, &PreprocessorManager::heartbeat); + + mHeartbeatTimer->start(gHeartbeatTimeoutInMSecs); +} + +PreprocessorManager::~PreprocessorManager() +{ + mHeartbeatTimer->stop(); + + // FIXME: Explicitly interrupt pre-processing here ? + // Pre-Processors should auto-protect themselves from re-processing an item: + // they are "closer to the DB" from this point of view. + + qDeleteAll(mPreprocessorChain); + qDeleteAll(mTransactionWaitQueueHash); // this should also disconnect all the signals from the data store objects... +} + +bool PreprocessorManager::isActive() +{ + QMutexLocker locker(&mMutex); + + if (!mEnabled) { + return false; + } + return !mPreprocessorChain.isEmpty(); +} + +PreprocessorInstance *PreprocessorManager::lockedFindInstance(const QString &id) +{ + for (PreprocessorInstance *instance : std::as_const(mPreprocessorChain)) { + if (instance->id() == id) { + return instance; + } + } + + return nullptr; +} + +void PreprocessorManager::registerInstance(const QString &id) +{ + QMutexLocker locker(&mMutex); + + qCDebug(AKONADISERVER_LOG) << "PreprocessorManager::registerInstance(" << id << ")"; + + PreprocessorInstance *instance = lockedFindInstance(id); + if (instance) { + return; // already registered + } + + // The PreprocessorInstance objects are actually always added at the end of the queue + // TODO: Maybe we need some kind of ordering here ? + // In that case we'll need to fiddle with the items that are currently enqueued for processing... + + instance = new PreprocessorInstance(id, *this, mTracer); + if (!instance->init()) { + mTracer.warning(QStringLiteral("PreprocessorManager"), QStringLiteral("Could not initialize preprocessor instance '%1'").arg(id)); + delete instance; + return; + } + + qCDebug(AKONADISERVER_LOG) << "Registering preprocessor instance " << id; + + mPreprocessorChain.append(instance); +} + +void PreprocessorManager::unregisterInstance(const QString &id) +{ + QMutexLocker locker(&mMutex); + + qCDebug(AKONADISERVER_LOG) << "PreprocessorManager::unregisterInstance(" << id << ")"; + + lockedUnregisterInstance(id); +} + +void PreprocessorManager::lockedUnregisterInstance(const QString &id) +{ + PreprocessorInstance *instance = lockedFindInstance(id); + if (!instance) { + return; // not our instance: don't complain (as we might be called for non-preprocessor agents too) + } + + // All of the preprocessor's waiting items must be queued to the next preprocessor (if there is one) + + std::deque *itemList = instance->itemQueue(); + Q_ASSERT(itemList); + + int idx = mPreprocessorChain.indexOf(instance); + Q_ASSERT(idx >= 0); // must be there! + + if (idx < (mPreprocessorChain.count() - 1)) { + // This wasn't the last preprocessor: trigger the next one. + PreprocessorInstance *nextPreprocessor = mPreprocessorChain[idx + 1]; + Q_ASSERT(nextPreprocessor); + Q_ASSERT(nextPreprocessor != instance); + + for (qint64 itemId : *itemList) { + nextPreprocessor->enqueueItem(itemId); + } + } else { + // This was the last preprocessor: end handling the items + for (qint64 itemId : *itemList) { + lockedEndHandleItem(itemId); + } + } + + mPreprocessorChain.removeOne(instance); + delete instance; +} + +void PreprocessorManager::beginHandleItem(const PimItem &item, const DataStore *dataStore) +{ + Q_ASSERT(dataStore); + Q_ASSERT(item.isValid()); + + // This is the entry point of the pre-processing chain. + QMutexLocker locker(&mMutex); + + if (!mEnabled) { + // Preprocessing is disabled: immediately end handling the item. + // In fact we shouldn't even be here as the caller should + // have checked isActive() before calling this function. + // However, since setEnabled() may be called concurrently + // then this might not be the caller's fault. Just drop a warning. + + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::beginHandleItem(" << item.id() << ") called with a disabled preprocessor"; + + lockedEndHandleItem(item.id()); + return; + } + +#if 0 + // Now the hidden flag is stored as a part.. too hard to assert its existence :D + Q_ASSERT_X(item.hidden(), "PreprocessorManager::beginHandleItem()", "The item you pass to this function should be hidden!"); +#endif + + if (mPreprocessorChain.isEmpty()) { + // No preprocessors at all: immediately end handling the item. + lockedEndHandleItem(item.id()); + return; + } + + if (dataStore->inTransaction()) { + qCDebug(AKONADISERVER_LOG) << "PreprocessorManager::beginHandleItem(" << item.id() + << "): the DataStore is in transaction, pushing item to a wait queue"; + + // The calling thread data store is in a transaction: push the item into a wait queue + std::deque *waitQueue = mTransactionWaitQueueHash.value(dataStore, nullptr); + + if (!waitQueue) { + // No wait queue for this transaction yet... + waitQueue = new std::deque(); + + mTransactionWaitQueueHash.insert(dataStore, waitQueue); + + // This will usually end up being a queued connection. + QObject::connect(dataStore, &QObject::destroyed, this, &PreprocessorManager::dataStoreDestroyed); + QObject::connect(dataStore, &DataStore::transactionCommitted, this, &PreprocessorManager::dataStoreTransactionCommitted); + QObject::connect(dataStore, &DataStore::transactionRolledBack, this, &PreprocessorManager::dataStoreTransactionRolledBack); + } + + waitQueue->push_back(item.id()); + + // nothing more to do here + return; + } + + // The calling thread data store is NOT in a transaction: we can proceed directly. + lockedActivateFirstPreprocessor(item.id()); +} + +void PreprocessorManager::lockedActivateFirstPreprocessor(qint64 itemId) +{ + // Activate the first preprocessor. + PreprocessorInstance *preProcessor = mPreprocessorChain.first(); + Q_ASSERT(preProcessor); + + preProcessor->enqueueItem(itemId); + // The preprocessor will call our "preProcessorFinishedHandlingItem() method" + // when done with the item. + // + // The call should be asynchronous, that is it should never happen that + // preProcessorFinishedHandlingItem() is called from "inside" enqueueItem()... + // FIXME: Am I *really* sure of this ? If I'm wrong for some obscure reason then we have a deadlock. +} + +void PreprocessorManager::lockedKillWaitQueue(const DataStore *dataStore, bool disconnectSlots) +{ + std::deque *waitQueue = mTransactionWaitQueueHash.value(dataStore, nullptr); + if (!waitQueue) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::lockedKillWaitQueue(): called for dataStore which has no wait queue"; + return; + } + + mTransactionWaitQueueHash.remove(dataStore); + + delete waitQueue; + + if (!disconnectSlots) { + return; + } + + QObject::disconnect(dataStore, &QObject::destroyed, this, &PreprocessorManager::dataStoreDestroyed); + QObject::disconnect(dataStore, &DataStore::transactionCommitted, this, &PreprocessorManager::dataStoreTransactionCommitted); + QObject::disconnect(dataStore, &DataStore::transactionRolledBack, this, &PreprocessorManager::dataStoreTransactionRolledBack); +} + +void PreprocessorManager::dataStoreDestroyed() +{ + QMutexLocker locker(&mMutex); + + qCDebug(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreDestroyed(): killing the wait queue"; + + const auto dataStore = dynamic_cast(sender()); + if (!dataStore) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreDestroyed(): got the signal from a non DataStore object"; + return; + } + + lockedKillWaitQueue(dataStore, false); // no need to disconnect slots, qt will do that +} + +void PreprocessorManager::dataStoreTransactionCommitted() +{ + QMutexLocker locker(&mMutex); + + qCDebug(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): pushing items in wait queue to the preprocessing chain"; + + const auto dataStore = dynamic_cast(sender()); + if (!dataStore) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): got the signal from a non DataStore object"; + return; + } + + std::deque *waitQueue = mTransactionWaitQueueHash.value(dataStore, nullptr); + if (!waitQueue) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): called for dataStore which has no wait queue"; + return; + } + + if (!mEnabled || mPreprocessorChain.isEmpty()) { + // Preprocessing has been disabled in the meantime or all the preprocessors died + for (qint64 id : *waitQueue) { + lockedEndHandleItem(id); + } + } else { + for (qint64 id : *waitQueue) { + lockedActivateFirstPreprocessor(id); + } + } + + lockedKillWaitQueue(dataStore, true); // disconnect slots this time +} + +void PreprocessorManager::dataStoreTransactionRolledBack() +{ + QMutexLocker locker(&mMutex); + + qCDebug(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionRolledBack(): killing the wait queue"; + + const auto dataStore = dynamic_cast(sender()); + if (!dataStore) { + qCWarning(AKONADISERVER_LOG) << "PreprocessorManager::dataStoreTransactionCommitted(): got the signal from a non DataStore object"; + return; + } + + lockedKillWaitQueue(dataStore, true); // disconnect slots this time +} + +void PreprocessorManager::preProcessorFinishedHandlingItem(PreprocessorInstance *preProcessor, qint64 itemId) +{ + QMutexLocker locker(&mMutex); + + int idx = mPreprocessorChain.indexOf(preProcessor); + Q_ASSERT(idx >= 0); // must be there! + + if (idx < (mPreprocessorChain.count() - 1)) { + // This wasn't the last preprocessor: trigger the next one. + PreprocessorInstance *nextPreprocessor = mPreprocessorChain[idx + 1]; + Q_ASSERT(nextPreprocessor); + Q_ASSERT(nextPreprocessor != preProcessor); + + nextPreprocessor->enqueueItem(itemId); + } else { + // This was the last preprocessor: end handling the item. + lockedEndHandleItem(itemId); + } +} + +void PreprocessorManager::lockedEndHandleItem(qint64 itemId) +{ + // The exit point of the pre-processing chain. + + // Refetch the PimItem, the Collection and the MimeType now: preprocessing might have changed them. + PimItem item = PimItem::retrieveById(itemId); + if (!item.isValid()) { + // HUM... the preprocessor killed the item ? + // ... or retrieveById() failed ? + // Well.. if the preprocessor killed the item then this might be actually OK (spam?). + qCDebug(AKONADISERVER_LOG) << "Invalid PIM item id '" << itemId << "' passed to preprocessing chain termination function"; + return; + } + +#if 0 + if (!item.hidden()) { + // HUM... the item was already unhidden for some reason: we have nothing more to do here. + qCDebug(AKONADISERVER_LOG) << "The PIM item with id '" << itemId << "' reached the preprocessing chain termination function in unhidden state"; + return; + } +#endif + + if (!DataStore::self()->unhidePimItem(item)) { + mTracer.warning( + QStringLiteral("PreprocessorManager"), + QStringLiteral("Failed to unhide the PIM item '%1': data is not lost but a server restart is required in order to unhide it").arg(itemId)); + } +} + +void PreprocessorManager::heartbeat() +{ + QMutexLocker locker(&mMutex); + + // Loop through the processor instances and check their current processing time. + + QList firedPreprocessors; + + for (PreprocessorInstance *instance : std::as_const(mPreprocessorChain)) { + // In this loop we check for "stuck" preprocessors. + + int elapsedTime = instance->currentProcessingTime(); + + if (elapsedTime < gWarningItemProcessingTimeInSecs) { + continue; // ok, still in time. + } + + // Ooops... the preprocessor looks to be "stuck". + // This is a rather critical condition and the question is "what we can do about it ?". + // The fact is that it doesn't really make sense to push another item for + // processing as the slave process is either dead (silently ?) or stuck anyway. + + // We then proceed as following: + // - we first kindly ask the preprocessor to abort the job (via Agent.Control interface) + // - if it doesn't obey after some time we attempt to restart it (via AgentManager) + // - if it doesn't obey, we drop the interface and assume it's dead until + // it's effectively restarted. + + if (elapsedTime < gMaximumItemProcessingTimeInSecs) { + // Kindly ask the preprocessor to abort the job. + + mTracer.warning(QStringLiteral("PreprocessorManager"), + QStringLiteral("Preprocessor '%1' seems to be stuck... trying to abort its job.").arg(instance->id())); + + if (instance->abortProcessing()) { + continue; + } + // If we're here then abortProcessing() failed. + } + + if (elapsedTime < gDeadlineItemProcessingTimeInSecs) { + // Attempt to restart the preprocessor via AgentManager interface + + mTracer.warning(QStringLiteral("PreprocessorManager"), QStringLiteral("Preprocessor '%1' is stuck... trying to restart it").arg(instance->id())); + + if (instance->invokeRestart()) { + continue; + } + // If we're here then invokeRestart() failed. + } + + mTracer.warning(QStringLiteral("PreprocessorManager"), QStringLiteral("Preprocessor '%1' is broken... ignoring it from now on").arg(instance->id())); + + // You're fired! Go Away! + firedPreprocessors.append(instance); + } + + // Kill the fired preprocessors, if any. + for (PreprocessorInstance *instance : std::as_const(firedPreprocessors)) { + lockedUnregisterInstance(instance->id()); + } +} diff --git a/src/server/preprocessormanager.h b/src/server/preprocessormanager.h new file mode 100644 index 0000000..3140ae3 --- /dev/null +++ b/src/server/preprocessormanager.h @@ -0,0 +1,259 @@ +/****************************************************************************** + * + * File : preprocessormanager.h + * Creation date : Sat 18 Jul 2009 01:58:50 + * + * SPDX-FileCopyrightText: 2009 Szymon Stefanek + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#include + +class QTimer; + +#include "preprocessorinstance.h" + +namespace Akonadi +{ +namespace Server +{ +class PimItem; +class DataStore; +class Tracer; + +/** + * \class PreprocessorManager + * \brief The manager for preprocessor agents + * + * This class takes care of synchronizing the preprocessor agents. + * + * The preprocessors see the incoming PimItem objects before the user + * can see them (as long as the UI applications honor the hidden attribute). + * The items are marked as hidden (by the Append and AkAppend + * handlers) and then enqueued to the preprocessor chain via this class. + * Once all the preprocessors have done their work the item is unhidden again. + * + * Preprocessing isn't designed for critical tasks. There may + * be circumstances under that the Akonadi server fails to push an item + * to all the preprocessors. Most notably after a server restart all + * the items for that preprocessing was interrupted are just unhidden + * without any attempt to resume the preprocessor jobs. + * + * The enqueue requests may or may not arrive from "inside" a database + * transaction. The uncommitted transaction would "hide" the newly created items + * from the preprocessor instances (which are separate processes). + * This class, then, takes care of holding the newly arrived items + * in a wait queue until their transaction is committed (or rolled back). + */ +class PreprocessorManager : public QObject +{ + friend class PreprocessorInstance; + + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.PreprocessorManager") + +protected: + /** + * The hashtable of transaction wait queues. There is one wait + * queue for each DataStore that is currently in a transaction. + */ + QHash *> mTransactionWaitQueueHash; + + /** + * The preprocessor chain. + * The pointers inside the list are owned. + * + * In all the algorithms we assume that this list is actually very short + * (say 3-4 elements) and reverse lookup (pointer->index) is really fast. + */ + QList mPreprocessorChain; + + /** + * Is preprocessing enabled at all in this Akonadi server instance? + * This is true by default and can be set via setEnabled(). + * Mainly used to disable preprocessing via configuration file. + */ + bool mEnabled = false; + + /** + * The mutex used to protect the internals of this class (mainly + * the mPreprocessorChain member). + */ + QMutex mMutex; + + /** + * The heartbeat timer. Used mainly to expire preprocessor jobs. + */ + QTimer *mHeartbeatTimer = nullptr; + + Tracer &mTracer; + +public: + /** + * Creates an instance of PreprocessorManager + */ + explicit PreprocessorManager(Tracer &tracer); + + /** + * Destroys the instance of PreprocessorManager + * and frees all the relevant resources + */ + ~PreprocessorManager(); + + /** + * Returns true if preprocessing is active in this Akonadi server. + * This means that we have at least one active preprocessor and + * preprocessing hasn't been explicitly disabled via configuration + * (so if isActive() returns true then also isEnabled() will return true). + * + * This function is thread-safe. + */ + bool isActive(); + + /** + * Returns true if this preprocessor hasn't been explicitly disabled + * via setEnabled( false ). This is used to disable preprocessing + * via configuration even if we have a valid chain of preprocessors. + * + * Please note that this flag doesn't tell if we actually have + * some registered preprocessors and thus we can do some meaningful job. + * You should use isActive() for this purpose. + */ + bool isEnabled() const + { + return mEnabled; + } + + /** + * Explicitly enables or disables the preprocessing in this Akonadi server. + * The PreprocessorManager starts in enabled state but can be disabled + * at a later stage: this is mainly used to disable preprocessing via + * configuration. + * + * Please note that setting this to true doesn't interrupt the currently + * running preprocessing jobs. Anything that was enqueued will be processed + * anyway. However, in Akonadi this is only invoked very early, + * when no preprocessors are alive yet. + */ + void setEnabled(bool enabled) + { + mEnabled = enabled; + } + + /** + * Trigger the preprocessor chain for the specified item. + * The item should have been added to the Akonadi database via + * the specified DataStore object. If the DataStore is in a + * transaction then this class will put the item in a wait + * queue until the transaction is committed. If the transaction + * is rolled back the whole wait queue will be discarded. + * If the DataStore is not in a transaction then the item + * will be pushed directly to the preprocessing chain. + * + * You should make sure that the preprocessor chain isActive() + * before calling this method. The items you pass to this method, + * also, should have the hidden attribute set. + * + * This function is thread-safe. + */ + void beginHandleItem(const PimItem &item, const DataStore *dataStore); + + /** + * This is called via D-Bus from AgentManager to register a preprocessor instance. + * + * This function is thread-safe. + */ + void registerInstance(const QString &id); + + /** + * This is called via D-Bus from AgentManager to unregister a preprocessor instance. + * + * This function is thread-safe. + */ + void unregisterInstance(const QString &id); + +protected: + /** + * This is called by PreprocessorInstance to signal that a certain preprocessor has finished + * handling an item. + * + * This function is thread-safe. + */ + void preProcessorFinishedHandlingItem(PreprocessorInstance *preProcessor, qint64 itemId); + +private: + /** + * Finds the preprocessor instance by its identifier. + * + * This must be called with mMutex locked. + */ + PreprocessorInstance *lockedFindInstance(const QString &id); + + /** + * Pushes the specified item to the first preprocessor. + * The caller *MUST* make sure that there is at least one preprocessor in the chain. + */ + void lockedActivateFirstPreprocessor(qint64 itemId); + + /** + * This is called internally to terminate the pre-processing + * chain for the specified Item. All the preprocessors have + * been triggered for it. + * + * This must be called with mMutex locked. + */ + void lockedEndHandleItem(qint64 itemId); + + /** + * This is the unprotected core of the unregisterInstance() function above. + */ + void lockedUnregisterInstance(const QString &id); + + /** + * Kill the wait queue for the specific DataStore object. + */ + void lockedKillWaitQueue(const DataStore *dataStore, bool disconnectSlots); + +private Q_SLOTS: + + /** + * Connected to the mHeartbeatTimer. Triggered every minute or something like that :D + * Mainly used to expire preprocessor jobs. + */ + void heartbeat(); + + /** + * This is used to handle database transactions and wait queues. + * The call to this slot usually comes from a queued signal/slot connection + * (i.e. from the *Append handler thread). + */ + void dataStoreDestroyed(); + + /** + * This is used to handle database transactions and wait queues. + * The call to this slot usually comes from a queued signal/slot connection + * (i.e. from the *Append handler thread). + */ + void dataStoreTransactionCommitted(); + + /** + * This is used to handle database transactions and wait queues. + * The call to this slot usually comes from a queued signal/slot connection + * (i.e. from the *Append handler thread). + */ + void dataStoreTransactionRolledBack(); + +}; // class PreprocessorManager + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/resourcemanager.cpp b/src/server/resourcemanager.cpp new file mode 100644 index 0000000..2d86abb --- /dev/null +++ b/src/server/resourcemanager.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "resourcemanager.h" +#include "resourcemanageradaptor.h" +#include "storage/datastore.h" +#include "storage/transaction.h" +#include "tracer.h" + +#include +#include + +#include + +using namespace Akonadi::Server; +using namespace AkRanges; + +ResourceManager::ResourceManager(Tracer &tracer) + : mTracer(tracer) +{ + new ResourceManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ResourceManager"), this); +} + +void ResourceManager::addResourceInstance(const QString &name, const QStringList &capabilities) +{ + Transaction transaction(DataStore::self(), QStringLiteral("ADD RESOURCE INSTANCE")); + Resource resource = Resource::retrieveByName(name); + if (resource.isValid()) { + mTracer.error("ResourceManager", QStringLiteral("Resource '%1' already exists.").arg(name)); + return; // resource already exists + } + + // create the resource + resource.setName(name); + resource.setIsVirtual(capabilities.contains(QLatin1String(AKONADI_AGENT_CAPABILITY_VIRTUAL))); + if (!resource.insert()) { + mTracer.error("ResourceManager", QStringLiteral("Could not create resource '%1'.").arg(name)); + } + transaction.commit(); +} + +void ResourceManager::removeResourceInstance(const QString &name) +{ + // remove items and collections + Resource resource = Resource::retrieveByName(name); + if (resource.isValid()) { + resource.collections() | Actions::forEach([](Collection col) { + DataStore::self()->cleanupCollection(col); + }); + + // remove resource + resource.remove(); + } +} + +QStringList ResourceManager::resourceInstances() const +{ + return Resource::retrieveAll() | Views::transform(&Resource::name) | Actions::toQList; +} diff --git a/src/server/resourcemanager.h b/src/server/resourcemanager.h new file mode 100644 index 0000000..0cec4cc --- /dev/null +++ b/src/server/resourcemanager.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +class Tracer; + +/** + Listens to agent instance added/removed signals and creates/removes + the corresponding data in the database. +*/ +class ResourceManager : public QObject +{ + Q_OBJECT + +public: + explicit ResourceManager(Tracer &tracer); + + QStringList resourceInstances() const; + +public Q_SLOTS: + void addResourceInstance(const QString &name, const QStringList &capabilities); + void removeResourceInstance(const QString &name); + +private: + Tracer &mTracer; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/search/abstractsearchengine.h b/src/server/search/abstractsearchengine.h new file mode 100644 index 0000000..8f64c69 --- /dev/null +++ b/src/server/search/abstractsearchengine.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +class Collection; + +/** + * Abstract interface for search engines. + * Executed in the main thread. Must not block. + */ +class AbstractSearchEngine +{ +public: + virtual ~AbstractSearchEngine() = default; + + /** + * Adds the given @p collection to the search. + */ + virtual void addSearch(const Collection &collection) = 0; + + /** + * Removes the collection with the given @p id from the search. + */ + virtual void removeSearch(qint64 id) = 0; + +protected: + explicit AbstractSearchEngine() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractSearchEngine) +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/search/abstractsearchplugin.h b/src/server/search/abstractsearchplugin.h new file mode 100644 index 0000000..1c04e93 --- /dev/null +++ b/src/server/search/abstractsearchplugin.h @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace Akonadi +{ +/** + * @class AbstractSearchPlugin + * + * 3rd party applications can install a search plugin for Akonadi server to + * provide access to their search capability. + * + * When the server performs a search, it will send the query to all available + * search plugins and merge the results. + * + * @since 1.12 + */ +class AbstractSearchPlugin +{ +public: + /** + * Destructor. + */ + virtual ~AbstractSearchPlugin() = default; + + /** + * Reimplement this method to provide the actual search capability. + * + * The implementation can block. + * + * @param query Search query to execute. + * @return List of Akonadi Item IDs referring to items that are matching + * the query. + */ + virtual QSet search(const QString &query, const QVector &collections, const QStringList &mimeTypes) = 0; + +protected: + explicit AbstractSearchPlugin() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractSearchPlugin) +}; + +} + +Q_DECLARE_INTERFACE(Akonadi::AbstractSearchPlugin, "org.freedesktop.Akonadi.AbstractSearchPlugin") + diff --git a/src/server/search/agentsearchengine.cpp b/src/server/search/agentsearchengine.cpp new file mode 100644 index 0000000..d324820 --- /dev/null +++ b/src/server/search/agentsearchengine.cpp @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentsearchengine.h" +#include "akonadiserver_search_debug.h" +#include "entities.h" + +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void AgentSearchEngine::addSearch(const Collection &collection) +{ + QDBusInterface agentMgr(DBus::serviceName(DBus::Control), + QStringLiteral(AKONADI_DBUS_AGENTMANAGER_PATH), + QStringLiteral("org.freedesktop.Akonadi.AgentManagerInternal")); + if (agentMgr.isValid()) { + const QList args = QList() << collection.queryString() << QLatin1String("") << collection.id(); + agentMgr.callWithArgumentList(QDBus::NoBlock, QStringLiteral("addSearch"), args); + return; + } + + qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to connect to agent manager: " << agentMgr.lastError().message(); +} + +void AgentSearchEngine::removeSearch(qint64 id) +{ + QDBusInterface agentMgr(DBus::serviceName(DBus::Control), + QStringLiteral(AKONADI_DBUS_AGENTMANAGER_PATH), + QStringLiteral("org.freedesktop.Akonadi.AgentManagerInternal")); + if (agentMgr.isValid()) { + const QList args = {id}; + agentMgr.callWithArgumentList(QDBus::NoBlock, QStringLiteral("removeSearch"), args); + return; + } + + qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to connect to agent manager: " << agentMgr.lastError().message(); +} diff --git a/src/server/search/agentsearchengine.h b/src/server/search/agentsearchengine.h new file mode 100644 index 0000000..862cb2b --- /dev/null +++ b/src/server/search/agentsearchengine.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "abstractsearchengine.h" + +namespace Akonadi +{ +namespace Server +{ +/** Search engine for distributing searches to agents. */ +class AgentSearchEngine : public AbstractSearchEngine +{ +public: + void addSearch(const Collection &collection) override; + void removeSearch(qint64 id) override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/search/agentsearchinstance.cpp b/src/server/search/agentsearchinstance.cpp new file mode 100644 index 0000000..536c85c --- /dev/null +++ b/src/server/search/agentsearchinstance.cpp @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentsearchinstance.h" +#include "agentsearchinterface.h" +#include "searchtaskmanager.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +AgentSearchInstance::AgentSearchInstance(const QString &id, SearchTaskManager &manager) + : mId(id) + , mInterface(nullptr) + , mManager(manager) +{ +} + +AgentSearchInstance::~AgentSearchInstance() +{ + delete mInterface; +} + +bool AgentSearchInstance::init() +{ + Q_ASSERT(!mInterface); + + mInterface = + new OrgFreedesktopAkonadiAgentSearchInterface(DBus::agentServiceName(mId, DBus::Agent), QStringLiteral("/Search"), QDBusConnection::sessionBus()); + + if (!mInterface || !mInterface->isValid()) { + delete mInterface; + mInterface = nullptr; + return false; + } + + mServiceWatcher = std::make_unique(DBus::agentServiceName(mId, DBus::Agent), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForUnregistration); + connect(mServiceWatcher.get(), &QDBusServiceWatcher::serviceUnregistered, this, [this]() { + mManager.unregisterInstance(mId); + }); + + return true; +} + +void AgentSearchInstance::search(const QByteArray &searchId, const QString &query, qlonglong collectionId) +{ + mInterface->search(searchId, query, collectionId); +} + +OrgFreedesktopAkonadiAgentSearchInterface *AgentSearchInstance::interface() const +{ + return mInterface; +} diff --git a/src/server/search/agentsearchinstance.h b/src/server/search/agentsearchinstance.h new file mode 100644 index 0000000..416bfeb --- /dev/null +++ b/src/server/search/agentsearchinstance.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +class QDBusServiceWatcher; +class OrgFreedesktopAkonadiAgentSearchInterface; + +namespace Akonadi +{ +namespace Server +{ +class SearchTaskManager; +class AgentSearchInstance : public QObject +{ + Q_OBJECT +public: + explicit AgentSearchInstance(const QString &id, SearchTaskManager &manager); + ~AgentSearchInstance() override; + + bool init(); + + void search(const QByteArray &searchId, const QString &query, qlonglong collectionId); + + OrgFreedesktopAkonadiAgentSearchInterface *interface() const; + +private: + QString mId; + OrgFreedesktopAkonadiAgentSearchInterface *mInterface; + std::unique_ptr mServiceWatcher; + SearchTaskManager &mManager; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/search/searchmanager.cpp b/src/server/search/searchmanager.cpp new file mode 100644 index 0000000..c7390d0 --- /dev/null +++ b/src/server/search/searchmanager.cpp @@ -0,0 +1,421 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "searchmanager.h" +#include "abstractsearchplugin.h" +#include "akonadiserver_search_debug.h" + +#include "agentsearchengine.h" +#include "akonadi.h" +#include "handler/searchhelper.h" +#include "notificationmanager.h" +#include "searchrequest.h" +#include "searchtaskmanager.h" +#include "storage/datastore.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include + +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(Akonadi::Server::NotificationCollector *) + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Collection) + +SearchManager::SearchManager(const QStringList &searchEngines, SearchTaskManager &agentSearchManager) + : AkThread(QStringLiteral("SearchManager"), AkThread::ManualStart, QThread::InheritPriority) + , mAgentSearchManager(agentSearchManager) + , mEngineNames(searchEngines) + , mSearchUpdateTimer(nullptr) +{ + qRegisterMetaType(); + + // We load search plugins (as in QLibrary::load()) in the main thread so that + // static initialization happens in the QApplication thread + loadSearchPlugins(); + + // Register to DBus on the main thread connection - otherwise we don't appear + // on the service. + QDBusConnection conn = QDBusConnection::sessionBus(); + conn.registerObject(QStringLiteral("/SearchManager"), this, QDBusConnection::ExportAllSlots); + + // Delay-call init() + startThread(); +} + +void SearchManager::init() +{ + AkThread::init(); + + mEngines.reserve(mEngineNames.size()); + for (const QString &engineName : std::as_const(mEngineNames)) { + if (engineName == QLatin1String("Agent")) { + mEngines.append(new AgentSearchEngine); + } else { + qCCritical(AKONADISERVER_SEARCH_LOG) << "Unknown search engine type: " << engineName; + } + } + + initSearchPlugins(); + + // The timer will tick 15 seconds after last change notification. If a new notification + // is delivered in the meantime, the timer is reset + mSearchUpdateTimer = new QTimer(this); + mSearchUpdateTimer->setInterval(15 * 1000); + mSearchUpdateTimer->setSingleShot(true); + connect(mSearchUpdateTimer, &QTimer::timeout, this, &SearchManager::searchUpdateTimeout); +} + +void SearchManager::quit() +{ + QDBusConnection conn = QDBusConnection::sessionBus(); + conn.unregisterObject(QStringLiteral("/SearchManager"), QDBusConnection::UnregisterTree); + conn.disconnectFromBus(conn.name()); + + // Make sure all children are deleted within context of this thread + qDeleteAll(children()); + + qDeleteAll(mEngines); + qDeleteAll(mPlugins); + /* + * FIXME: Unloading plugin messes up some global statics from client libs + * and causes crash on Akonadi shutdown (below main). Keeping the plugins + * loaded is not really a big issue as this is only invoked on server shutdown + * anyway, so we are not leaking any memory. + Q_FOREACH (QPluginLoader *loader, mPluginLoaders) { + loader->unload(); + delete loader; + } + */ + + AkThread::quit(); +} + +SearchManager::~SearchManager() +{ + quitThread(); +} + +void SearchManager::registerInstance(const QString &id) +{ + mAgentSearchManager.registerInstance(id); +} + +void SearchManager::unregisterInstance(const QString &id) +{ + mAgentSearchManager.unregisterInstance(id); +} + +QVector SearchManager::searchPlugins() const +{ + return mPlugins; +} + +void SearchManager::loadSearchPlugins() +{ + QStringList loadedPlugins; + const QString pluginOverride = QString::fromLatin1(qgetenv("AKONADI_OVERRIDE_SEARCHPLUGIN")); + if (!pluginOverride.isEmpty()) { + qCInfo(AKONADISERVER_SEARCH_LOG) << "Overriding the search plugins with: " << pluginOverride; + } + + const QStringList dirs = QCoreApplication::libraryPaths(); + for (const QString &pluginDir : dirs) { + QDir dir(pluginDir + QLatin1String("/akonadi")); + const QStringList fileNames = dir.entryList(QDir::Files); + qCDebug(AKONADISERVER_SEARCH_LOG) << "SEARCH MANAGER: searching in " << pluginDir + QLatin1String("/akonadi") << ":" << fileNames; + for (const QString &fileName : fileNames) { + const QString filePath = pluginDir % QLatin1String("/akonadi/") % fileName; + std::unique_ptr loader(new QPluginLoader(filePath)); + const QVariantMap metadata = loader->metaData().value(QStringLiteral("MetaData")).toVariant().toMap(); + if (metadata.value(QStringLiteral("X-Akonadi-PluginType")).toString() != QLatin1String("SearchPlugin")) { + continue; + } + + const QString libraryName = metadata.value(QStringLiteral("X-Akonadi-Library")).toString(); + if (loadedPlugins.contains(libraryName)) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Already loaded one version of this plugin, skipping: " << libraryName; + continue; + } + + // When search plugin override is active, ignore all plugins except for the override + if (!pluginOverride.isEmpty()) { + if (libraryName != pluginOverride) { + qCDebug(AKONADISERVER_SEARCH_LOG) << libraryName << "skipped because of AKONADI_OVERRIDE_SEARCHPLUGIN"; + continue; + } + + // When there's no override, only load plugins enabled by default + } else if (!metadata.value(QStringLiteral("X-Akonadi-LoadByDefault"), true).toBool()) { + continue; + } + + if (!loader->load()) { + qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to load search plugin" << libraryName << ":" << loader->errorString(); + continue; + } + + mPluginLoaders << loader.release(); + loadedPlugins << libraryName; + } + } +} + +void SearchManager::initSearchPlugins() +{ + for (QPluginLoader *loader : std::as_const(mPluginLoaders)) { + if (!loader->load()) { + qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to load search plugin" << loader->fileName() << ":" << loader->errorString(); + continue; + } + + AbstractSearchPlugin *plugin = qobject_cast(loader->instance()); + if (!plugin) { + qCCritical(AKONADISERVER_SEARCH_LOG) << loader->fileName() << "is not a valid Akonadi search plugin"; + continue; + } + + qCDebug(AKONADISERVER_SEARCH_LOG) << "SearchManager: loaded search plugin" << loader->fileName(); + mPlugins << plugin; + } +} + +void SearchManager::scheduleSearchUpdate() +{ + // Reset if the timer is active (use QueuedConnection to invoke start() from + // the thread the QTimer lives in instead of caller's thread, otherwise crashes + // and weird things can happen. + QMetaObject::invokeMethod(mSearchUpdateTimer, QOverload<>::of(&QTimer::start), Qt::QueuedConnection); +} + +void SearchManager::searchUpdateTimeout() +{ + // Get all search collections, that is subcollections of "Search", which always has ID 1 + const Collection::List collections = Collection::retrieveFiltered(Collection::parentIdFullColumnName(), 1); + for (const Collection &collection : collections) { + updateSearchAsync(collection); + } +} + +void SearchManager::updateSearchAsync(const Collection &collection) +{ + QMetaObject::invokeMethod( + this, + [this, collection]() { + updateSearchImpl(collection); + }, + Qt::QueuedConnection); +} + +void SearchManager::updateSearch(const Collection &collection) +{ + mLock.lock(); + if (mUpdatingCollections.contains(collection.id())) { + mLock.unlock(); + return; + // FIXME: If another thread already requested an update, we return to the caller before the + // search update is performed; this contradicts the docs + } + mUpdatingCollections.insert(collection.id()); + mLock.unlock(); + QMetaObject::invokeMethod( + this, + [this, collection]() { + updateSearchImpl(collection); + }, + Qt::BlockingQueuedConnection); + mLock.lock(); + mUpdatingCollections.remove(collection.id()); + mLock.unlock(); +} + +void SearchManager::updateSearchImpl(const Collection &collection) +{ + if (collection.queryString().size() >= 32768) { + qCWarning(AKONADISERVER_SEARCH_LOG) << "The query is at least 32768 chars long, which is the maximum size supported by the akonadi db schema. The " + "query is therefore most likely truncated and will not be executed."; + return; + } + if (collection.queryString().isEmpty()) { + return; + } + + const QStringList queryAttributes = collection.queryAttributes().split(QLatin1Char(' ')); + const bool remoteSearch = queryAttributes.contains(QLatin1String(AKONADI_PARAM_REMOTE)); + bool recursive = queryAttributes.contains(QLatin1String(AKONADI_PARAM_RECURSIVE)); + + QStringList queryMimeTypes; + const QVector mimeTypes = collection.mimeTypes(); + queryMimeTypes.reserve(mimeTypes.count()); + + for (const MimeType &mt : mimeTypes) { + queryMimeTypes << mt.name(); + } + + QVector queryAncestors; + if (collection.queryCollections().isEmpty()) { + queryAncestors << 0; + recursive = true; + } else { + const QStringList collectionIds = collection.queryCollections().split(QLatin1Char(' ')); + queryAncestors.reserve(collectionIds.count()); + for (const QString &colId : collectionIds) { + queryAncestors << colId.toLongLong(); + } + } + + // Always query the given collections + QVector queryCollections = queryAncestors; + + if (recursive) { + // Resolve subcollections if necessary + queryCollections += SearchHelper::matchSubcollectionsByMimeType(queryAncestors, queryMimeTypes); + } + + // This happens if we try to search a virtual collection in recursive mode (because virtual collections are excluded from listCollectionsRecursive) + if (queryCollections.isEmpty()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "No collections to search, you're probably trying to search a virtual collection."; + return; + } + + // Query all plugins for search results + const QByteArray id = "searchUpdate-" + QByteArray::number(QDateTime::currentDateTimeUtc().toTime_t()); + SearchRequest request(id, *this, mAgentSearchManager); + request.setCollections(queryCollections); + request.setMimeTypes(queryMimeTypes); + request.setQuery(collection.queryString()); + request.setRemoteSearch(remoteSearch); + request.setStoreResults(true); + request.setProperty("SearchCollection", QVariant::fromValue(collection)); + connect(&request, &SearchRequest::resultsAvailable, this, &SearchManager::searchUpdateResultsAvailable); + request.exec(); // blocks until all searches are done + + const QSet results = request.results(); + + // Get all items in the collection + QueryBuilder qb(CollectionPimItemRelation::tableName()); + qb.addColumn(CollectionPimItemRelation::rightColumn()); + qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id()); + if (!qb.exec()) { + return; + } + + Transaction transaction(DataStore::self(), QStringLiteral("UPDATE SEARCH")); + + // Unlink all items that were not in search results from the collection + QVariantList toRemove; + while (qb.query().next()) { + const qint64 id = qb.query().value(0).toLongLong(); + if (!results.contains(id)) { + toRemove << id; + Collection::removePimItem(collection.id(), id); + } + } + + if (!transaction.commit()) { + return; + } + + if (!toRemove.isEmpty()) { + SelectQueryBuilder qb; + qb.addValueCondition(PimItem::idFullColumnName(), Query::In, toRemove); + if (!qb.exec()) { + return; + } + + const QVector removedItems = qb.result(); + DataStore::self()->notificationCollector()->itemsUnlinked(removedItems, collection); + } + + qCInfo(AKONADISERVER_SEARCH_LOG) << "Search update for collection" << collection.name() << "(" << collection.id() << ") finished:" + << "all results: " << results.count() << ", removed results:" << toRemove.count(); +} + +void SearchManager::searchUpdateResultsAvailable(const QSet &results) +{ + const auto collection = sender()->property("SearchCollection").value(); + qCDebug(AKONADISERVER_SEARCH_LOG) << "searchUpdateResultsAvailable" << collection.id() << results.count() << "results"; + + QSet newMatches = results; + QSet existingMatches; + { + QueryBuilder qb(CollectionPimItemRelation::tableName()); + qb.addColumn(CollectionPimItemRelation::rightColumn()); + qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id()); + if (!qb.exec()) { + return; + } + + while (qb.query().next()) { + const qint64 id = qb.query().value(0).toLongLong(); + if (newMatches.contains(id)) { + existingMatches << id; + } + } + } + + qCDebug(AKONADISERVER_SEARCH_LOG) << "Got" << newMatches.count() << "results, out of which" << existingMatches.count() << "are already in the collection"; + + newMatches = newMatches - existingMatches; + if (newMatches.isEmpty()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results: 0 (fast path)"; + return; + } + + Transaction transaction(DataStore::self(), QStringLiteral("PUSH SEARCH RESULTS"), !DataStore::self()->inTransaction()); + + // First query all the IDs we got from search plugin/agent against the DB. + // This will remove IDs that no longer exist in the DB. + QVariantList newMatchesVariant; + newMatchesVariant.reserve(newMatches.count()); + for (qint64 id : std::as_const(newMatches)) { + newMatchesVariant << id; + } + + SelectQueryBuilder qb; + qb.addValueCondition(PimItem::idFullColumnName(), Query::In, newMatchesVariant); + if (!qb.exec()) { + return; + } + + const auto items = qb.result(); + if (items.count() != newMatches.count()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Search backend returned" << (newMatches.count() - items.count()) << "results that no longer exist in Akonadi."; + qCDebug(AKONADISERVER_SEARCH_LOG) << "Please reindex collection" << collection.id(); + // TODO: Request the reindexing directly from here + } + + if (items.isEmpty()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results: 0 (no existing result)"; + return; + } + + for (const auto &item : items) { + Collection::addPimItem(collection.id(), item.id()); + } + + if (!transaction.commit()) { + qCWarning(AKONADISERVER_SEARCH_LOG) << "Failed to commit search results transaction"; + return; + } + + DataStore::self()->notificationCollector()->itemsLinked(items, collection); + // Force collector to dispatch the notification now + DataStore::self()->notificationCollector()->dispatchNotifications(); + + qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results:" << items.count(); +} diff --git a/src/server/search/searchmanager.h b/src/server/search/searchmanager.h new file mode 100644 index 0000000..6c24275 --- /dev/null +++ b/src/server/search/searchmanager.h @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2010 Volker Krause + SPDX-FileCopyrightText: 2013 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akthread.h" + +#include +#include +#include + +class QTimer; +class QPluginLoader; + +namespace Akonadi +{ +class AbstractSearchPlugin; + +namespace Server +{ +class AbstractSearchEngine; +class Collection; +class SearchTaskManager; + +/** + * SearchManager creates and deletes persistent searches for all currently + * active search engines. + */ +class SearchManager : public AkThread +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.SearchManager") + +public: + /** Create a new search manager with the given @p searchEngines. */ + explicit SearchManager(const QStringList &searchEngines, SearchTaskManager &agentSearchManager); + + ~SearchManager() override; + + /** + * Updates the search query asynchronously. Returns immediately + */ + virtual void updateSearchAsync(const Collection &collection); + + /** + * Updates the search query synchronously. + */ + virtual void updateSearch(const Collection &collection); + + /** + * Returns currently available search plugins. + */ + virtual QVector searchPlugins() const; + +public Q_SLOTS: + virtual void scheduleSearchUpdate(); + + /** + * This is called via D-Bus from AgentManager to register an agent with + * search interface. + */ + virtual void registerInstance(const QString &id); + + /** + * This is called via D-Bus from AgentManager to unregister an agent with + * search interface. + */ + virtual void unregisterInstance(const QString &id); + +private Q_SLOTS: + void searchUpdateTimeout(); + void searchUpdateResultsAvailable(const QSet &results); + + /** + * Actual implementation of search updates. + * + * This method has to be called using QMetaObject::invokeMethod. + */ + void updateSearchImpl(const Akonadi::Server::Collection &collection); + +private: + void init() override; + void quit() override; + + // Called from main thread + void loadSearchPlugins(); + // Called from manager thread + void initSearchPlugins(); + + SearchTaskManager &mAgentSearchManager; + QStringList mEngineNames; + QVector mPluginLoaders; + QVector mEngines; + QVector mPlugins; + + QTimer *mSearchUpdateTimer = nullptr; + + QMutex mLock; + QSet mUpdatingCollections; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/search/searchrequest.cpp b/src/server/search/searchrequest.cpp new file mode 100644 index 0000000..a7fdf8b --- /dev/null +++ b/src/server/search/searchrequest.cpp @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2013, 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "searchrequest.h" + +#include "abstractsearchplugin.h" +#include "akonadi.h" +#include "akonadiserver_search_debug.h" +#include "connection.h" +#include "searchmanager.h" +#include "searchtaskmanager.h" + +using namespace Akonadi::Server; + +SearchRequest::SearchRequest(const QByteArray &connectionId, SearchManager &searchManager, SearchTaskManager &agentSearchManager) + : mConnectionId(connectionId) + , mSearchManager(searchManager) + , mAgentSearchManager(agentSearchManager) +{ +} + +SearchRequest::~SearchRequest() +{ +} + +QByteArray SearchRequest::connectionId() const +{ + return mConnectionId; +} + +void SearchRequest::setQuery(const QString &query) +{ + mQuery = query; +} + +QString SearchRequest::query() const +{ + return mQuery; +} + +void SearchRequest::setCollections(const QVector &collectionsIds) +{ + mCollections = collectionsIds; +} + +QVector SearchRequest::collections() const +{ + return mCollections; +} + +void SearchRequest::setMimeTypes(const QStringList &mimeTypes) +{ + mMimeTypes = mimeTypes; +} + +QStringList SearchRequest::mimeTypes() const +{ + return mMimeTypes; +} + +void SearchRequest::setRemoteSearch(bool remote) +{ + mRemoteSearch = remote; +} + +bool SearchRequest::remoteSearch() const +{ + return mRemoteSearch; +} + +void SearchRequest::setStoreResults(bool storeResults) +{ + mStoreResults = storeResults; +} + +QSet SearchRequest::results() const +{ + return mResults; +} + +void SearchRequest::emitResults(const QSet &results) +{ + Q_EMIT resultsAvailable(results); + if (mStoreResults) { + mResults.unite(results); + } +} + +void SearchRequest::searchPlugins() +{ + const QVector plugins = mSearchManager.searchPlugins(); + for (AbstractSearchPlugin *plugin : plugins) { + const QSet result = plugin->search(mQuery, mCollections, mMimeTypes); + emitResults(result); + } +} + +void SearchRequest::exec() +{ + qCInfo(AKONADISERVER_SEARCH_LOG) << "Executing search" << mConnectionId; + + // TODO should we move this to the AgentSearchManager as well? If we keep it here the agents can be searched in parallel + // since the plugin search is executed in this thread directly. + searchPlugins(); + + // If remote search is disabled, just finish here after searching the plugins + if (!mRemoteSearch) { + qCInfo(AKONADISERVER_SEARCH_LOG) << "Search " << mConnectionId << "done (without remote search)"; + return; + } + + SearchTask task; + task.id = mConnectionId; + task.query = mQuery; + task.mimeTypes = mMimeTypes; + task.collections = mCollections; + task.complete = false; + + mAgentSearchManager.addTask(&task); + + task.sharedLock.lock(); + Q_FOREVER { + if (task.complete) { + break; + } + + task.notifier.wait(&task.sharedLock); + + qCDebug(AKONADISERVER_SEARCH_LOG) << task.pendingResults.count() << "search results available in search" << task.id; + if (!task.pendingResults.isEmpty()) { + emitResults(task.pendingResults); + } + task.pendingResults.clear(); + } + + if (!task.pendingResults.isEmpty()) { + emitResults(task.pendingResults); + } + task.sharedLock.unlock(); + + qCInfo(AKONADISERVER_SEARCH_LOG) << "Search" << mConnectionId << "done (with remote search)"; +} diff --git a/src/server/search/searchrequest.h b/src/server/search/searchrequest.h new file mode 100644 index 0000000..ea179fd --- /dev/null +++ b/src/server/search/searchrequest.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2013, 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class Connection; +class SearchManager; +class SearchTaskManager; + +class SearchRequest : public QObject +{ + Q_OBJECT + +public: + explicit SearchRequest(const QByteArray &connectionId, SearchManager &searchManager, SearchTaskManager &agentSearchTask); + ~SearchRequest(); + + void setQuery(const QString &query); + QString query() const; + void setCollections(const QVector &collections); + QVector collections() const; + void setMimeTypes(const QStringList &mimeTypes); + QStringList mimeTypes() const; + void setRemoteSearch(bool remote); + bool remoteSearch() const; + + /** + * Whether results should be stored after they are emitted via resultsAvailable(), + * so that they can be extracted via results() after the search is over. This + * is disabled by default. + */ + void setStoreResults(bool storeResults); + + QByteArray connectionId() const; + + void exec(); + + QSet results() const; + +Q_SIGNALS: + void resultsAvailable(const QSet &results); + +private: + void searchPlugins(); + void emitResults(const QSet &results); + + QByteArray mConnectionId; + QString mQuery; + QVector mCollections; + QStringList mMimeTypes; + bool mRemoteSearch = false; + bool mStoreResults = false; + QSet mResults; + + SearchManager &mSearchManager; + SearchTaskManager &mAgentSearchManager; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/search/searchtaskmanager.cpp b/src/server/search/searchtaskmanager.cpp new file mode 100644 index 0000000..09a8228 --- /dev/null +++ b/src/server/search/searchtaskmanager.cpp @@ -0,0 +1,300 @@ +/* + SPDX-FileCopyrightText: 2013, 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "searchtaskmanager.h" +#include "agentsearchinstance.h" +#include "akonadiserver_search_debug.h" +#include "connection.h" +#include "entities.h" +#include "storage/selectquerybuilder.h" + +#include + +#include +#include +#include +#include +#include +using namespace Akonadi; +using namespace Akonadi::Server; + +SearchTaskManager::SearchTaskManager() + : AkThread(QStringLiteral("SearchTaskManager")) + , mShouldStop(false) +{ + QTimer::singleShot(0, this, &SearchTaskManager::searchLoop); +} + +SearchTaskManager::~SearchTaskManager() +{ + QMutexLocker locker(&mLock); + mShouldStop = true; + mWait.wakeAll(); + locker.unlock(); + + quitThread(); + + mInstancesLock.lock(); + qDeleteAll(mInstances); + mInstancesLock.unlock(); +} + +void SearchTaskManager::registerInstance(const QString &id) +{ + QMutexLocker locker(&mInstancesLock); + + qCDebug(AKONADISERVER_SEARCH_LOG) << "SearchManager::registerInstance(" << id << ")"; + + AgentSearchInstance *instance = mInstances.value(id); + if (instance) { + return; // already registered + } + + instance = new AgentSearchInstance(id, *this); + if (!instance->init()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Failed to initialize Search agent"; + delete instance; + return; + } + + qCDebug(AKONADISERVER_SEARCH_LOG) << "Registering search instance " << id; + mInstances.insert(id, instance); +} + +void SearchTaskManager::unregisterInstance(const QString &id) +{ + QMutexLocker locker(&mInstancesLock); + + QMap::Iterator it = mInstances.find(id); + if (it != mInstances.end()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Unregistering search instance" << id; + it.value()->deleteLater(); + mInstances.erase(it); + } +} + +void SearchTaskManager::addTask(SearchTask *task) +{ + QueryBuilder qb(Collection::tableName()); + qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); + qb.addColumn(Collection::idFullColumnName()); + qb.addColumn(Resource::nameFullColumnName()); + + Q_ASSERT(!task->collections.isEmpty()); + QVariantList list; + list.reserve(task->collections.size()); + for (qint64 collection : std::as_const(task->collections)) { + list << collection; + } + qb.addValueCondition(Collection::idFullColumnName(), Query::In, list); + + if (!qb.exec()) { + throw SearchException(qb.query().lastError().text()); + } + + QSqlQuery query = qb.query(); + if (!query.next()) { + return; + } + + mInstancesLock.lock(); + + org::freedesktop::Akonadi::AgentManager agentManager(DBus::serviceName(DBus::Control), QStringLiteral("/AgentManager"), QDBusConnection::sessionBus()); + do { + const QString resourceId = query.value(1).toString(); + if (!mInstances.contains(resourceId)) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Resource" << resourceId << "does not implement Search interface, skipping"; + } else if (!agentManager.agentInstanceOnline(resourceId)) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Agent" << resourceId << "is offline, skipping"; + } else if (agentManager.agentInstanceStatus(resourceId) > 2) { // 2 == Broken, 3 == Not Configured + qCDebug(AKONADISERVER_SEARCH_LOG) << "Agent" << resourceId << "is broken or not configured"; + } else { + const qint64 collectionId = query.value(0).toLongLong(); + qCDebug(AKONADISERVER_SEARCH_LOG) << "Enqueued search query (" << resourceId << ", " << collectionId << ")"; + task->queries << qMakePair(resourceId, collectionId); + } + } while (query.next()); + mInstancesLock.unlock(); + + QMutexLocker locker(&mLock); + mTasklist.append(task); + mWait.wakeAll(); +} + +void SearchTaskManager::pushResults(const QByteArray &searchId, const QSet &ids, Connection *connection) +{ + Q_UNUSED(searchId) + + const auto resourceName = connection->context().resource().name(); + qCDebug(AKONADISERVER_SEARCH_LOG) << ids.count() << "results for search" << searchId << "pushed from" << resourceName; + + QMutexLocker locker(&mLock); + ResourceTask *task = mRunningTasks.take(resourceName); + if (!task) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "No running task for" << resourceName << " - maybe it has timed out?"; + return; + } + + if (task->parentTask->id != searchId) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Received results for different search - maybe the original task has timed out?"; + qCDebug(AKONADISERVER_SEARCH_LOG) << "Search is" << searchId << ", but task is" << task->parentTask->id; + return; + } + + task->results = ids; + mPendingResults.append(task); + + mWait.wakeAll(); +} + +bool SearchTaskManager::allResourceTasksCompleted(SearchTask *agentSearchTask) const +{ + // Check for queries pending to be dispatched + if (!agentSearchTask->queries.isEmpty()) { + return false; + } + + // Check for running queries + QMap::const_iterator it = mRunningTasks.begin(); + QMap::const_iterator end = mRunningTasks.end(); + for (; it != end; ++it) { + if (it.value()->parentTask == agentSearchTask) { + return false; + } + } + + return true; +} + +SearchTaskManager::TasksMap::Iterator SearchTaskManager::cancelRunningTask(TasksMap::Iterator &iter) +{ + ResourceTask *task = iter.value(); + SearchTask *parentTask = task->parentTask; + QMutexLocker locker(&parentTask->sharedLock); + // erase the task before allResourceTasksCompleted + SearchTaskManager::TasksMap::Iterator it = mRunningTasks.erase(iter); + // We're not clearing the results since we don't want to clear successful results from other resources + parentTask->complete = allResourceTasksCompleted(parentTask); + parentTask->notifier.wakeAll(); + delete task; + + return it; +} + +void SearchTaskManager::searchLoop() +{ + qint64 timeout = ULONG_MAX; + + QMutexLocker locker(&mLock); + + Q_FOREVER { + qCDebug(AKONADISERVER_SEARCH_LOG) << "Search loop is waiting, will wake again in" << timeout << "ms"; + mWait.wait(&mLock, QDeadlineTimer(QDeadlineTimer::Forever)); + if (mShouldStop) { + Q_FOREACH (SearchTask *task, mTasklist) { + QMutexLocker locker(&task->sharedLock); + task->queries.clear(); + task->notifier.wakeAll(); + } + + QMap::Iterator it = mRunningTasks.begin(); + for (; it != mRunningTasks.end();) { + if (mTasklist.contains(it.value()->parentTask)) { + delete it.value(); + it = mRunningTasks.erase(it); + continue; + } + it = cancelRunningTask(it); + } + + break; + } + + // First notify about available results + while (!mPendingResults.isEmpty()) { + ResourceTask *finishedTask = mPendingResults.first(); + mPendingResults.remove(0); + qCDebug(AKONADISERVER_SEARCH_LOG) << "Pending results from" << finishedTask->resourceId << "for collection" << finishedTask->collectionId + << "for search" << finishedTask->parentTask->id << "available!"; + SearchTask *parentTask = finishedTask->parentTask; + QMutexLocker locker(&parentTask->sharedLock); + // We need to append, this agent search task is shared + parentTask->pendingResults += finishedTask->results; + parentTask->complete = allResourceTasksCompleted(parentTask); + parentTask->notifier.wakeAll(); + delete finishedTask; + } + + // No check whether there are any tasks running longer than 1 minute and kill them + QMap::Iterator it = mRunningTasks.begin(); + const qint64 now = QDateTime::currentMSecsSinceEpoch(); + for (; it != mRunningTasks.end();) { + ResourceTask *task = it.value(); + if (now - task->timestamp > 60 * 1000) { + // Remove the task - and signal to parent task that it has "finished" without results + qCDebug(AKONADISERVER_SEARCH_LOG) << "Resource task" << task->resourceId << "for search" << task->parentTask->id << "timed out!"; + it = cancelRunningTask(it); + } else { + ++it; + } + } + + if (!mTasklist.isEmpty()) { + SearchTask *task = mTasklist.first(); + qCDebug(AKONADISERVER_SEARCH_LOG) << "Search task" << task->id << "available!"; + if (task->queries.isEmpty()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "nothing to do for task"; + QMutexLocker locker(&task->sharedLock); + // After this the AgentSearchTask will be destroyed + task->complete = true; + task->notifier.wakeAll(); + mTasklist.remove(0); + continue; + } + + for (auto it = task->queries.begin(); it != task->queries.end();) { + if (!mRunningTasks.contains(it->first)) { + const auto &[resource, colId] = *it; + qCDebug(AKONADISERVER_SEARCH_LOG) << "\t Sending query for collection" << colId << "to resource" << resource; + auto rTask = new ResourceTask; + rTask->resourceId = resource; + rTask->collectionId = colId; + rTask->parentTask = task; + rTask->timestamp = QDateTime::currentMSecsSinceEpoch(); + mRunningTasks.insert(resource, rTask); + + mInstancesLock.lock(); + AgentSearchInstance *instance = mInstances.value(resource); + if (!instance) { + mInstancesLock.unlock(); + // Resource disappeared in the meanwhile + continue; + } + + instance->search(task->id, task->query, colId); + mInstancesLock.unlock(); + + task->sharedLock.lock(); + it = task->queries.erase(it); + task->sharedLock.unlock(); + } else { + ++it; + } + } + // Yay! We managed to dispatch all requests! + if (task->queries.isEmpty()) { + qCDebug(AKONADISERVER_SEARCH_LOG) << "All queries from task" << task->id << "dispatched!"; + mTasklist.remove(0); + } + + timeout = 60 * 1000; // check whether all tasks have finished within a minute + } else { + if (mRunningTasks.isEmpty()) { + timeout = ULONG_MAX; + } + } + } +} diff --git a/src/server/search/searchtaskmanager.h b/src/server/search/searchtaskmanager.h new file mode 100644 index 0000000..6003635 --- /dev/null +++ b/src/server/search/searchtaskmanager.h @@ -0,0 +1,96 @@ +/* + SPDX-FileCopyrightText: 2013, 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akthread.h" + +#include "agentmanagerinterface.h" +#include "exception.h" +#include +#include +#include +#include +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class Connection; +class AgentSearchInstance; + +class SearchTask +{ +public: + QByteArray id; + QString query; + QStringList mimeTypes; + QVector collections; + bool complete; + + QMutex sharedLock; + QWaitCondition notifier; + + QVector> queries; + QSet pendingResults; +}; + +class SearchTaskManager : public AkThread +{ + Q_OBJECT + +public: + explicit SearchTaskManager(); + ~SearchTaskManager() override; + + void registerInstance(const QString &id); + void unregisterInstance(const QString &id); + + void addTask(SearchTask *task); + + void pushResults(const QByteArray &searchId, const QSet &ids, Connection *connection); + +private Q_SLOTS: + void searchLoop(); + +private: + class ResourceTask + { + public: + QString resourceId; + qint64 collectionId; + SearchTask *parentTask; + QSet results; + + qint64 timestamp; + }; + + using TasksMap = QMap; + + bool mShouldStop; + + TasksMap::Iterator cancelRunningTask(TasksMap::Iterator &iter); + bool allResourceTasksCompleted(SearchTask *agentSearchTask) const; + + QMap mInstances; + QMutex mInstancesLock; + + QWaitCondition mWait; + QMutex mLock; + + QVector mTasklist; + + QMap mRunningTasks; + QVector mPendingResults; +}; + +AKONADI_EXCEPTION_MAKE_INSTANCE(SearchException); + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/akonadi-mysql-client.sh b/src/server/storage/akonadi-mysql-client.sh new file mode 100755 index 0000000..ee2b81a --- /dev/null +++ b/src/server/storage/akonadi-mysql-client.sh @@ -0,0 +1,15 @@ +#! /bin/sh +# connect to mysqld started by akonadi +# useful for developing + +if [ -z "$1" ]; then + akonadisocket="$HOME/.local/share/akonadi/socket-`hostname`/mysql.socket" +else + if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + echo "Usage: $0 [instance identifier]" + exit 1; + fi + akonadisocket="$HOME/.local/share/akonadi/instance/$1/socket-`hostname`/mysql.socket" +fi + +mysql --socket=$akonadisocket akonadi diff --git a/src/server/storage/akonadi-mysql-server.sh b/src/server/storage/akonadi-mysql-server.sh new file mode 100755 index 0000000..adb4da1 --- /dev/null +++ b/src/server/storage/akonadi-mysql-server.sh @@ -0,0 +1,16 @@ +#! /bin/sh +# start mysqld as started by akonadi +# useful for developing + +akonadihome=$HOME/.local/share/akonadi +globalconfig=$KDEDIR/share/akonadi/mysql-global.conf +localconfig=$HOME/.config/akonadi/mysql-local.conf +if [ -f $globalconfig ]; then + cat $globalconfig $localconfig > $akonadihome/mysql.conf +fi + +/usr/sbin/mysqld \ + --defaults-file=$akonadihome/mysql.conf \ + --datadir=$akonadihome/db_data/ \ + "--socket=$akonadihome/socket-`hostname`/mysql.socket" + diff --git a/src/server/storage/akonadidb.qrc b/src/server/storage/akonadidb.qrc new file mode 100644 index 0000000..b1ab061 --- /dev/null +++ b/src/server/storage/akonadidb.qrc @@ -0,0 +1,5 @@ + + + dbupdate.xml + + diff --git a/src/server/storage/akonadidb.xml b/src/server/storage/akonadidb.xml new file mode 100644 index 0000000..50a83e8 --- /dev/null +++ b/src/server/storage/akonadidb.xml @@ -0,0 +1,245 @@ + + + + + + + + + Contains the schema version of the database. + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + +
+ + + + + + + + + + + create/modified time + + + read access time + + + Indicates that this item has unsaved changes. + + + + + + + + +
+ + + This meta data is stored inside akonadi to provide fast access. + + +
+ + + Table containing item part types. + + + Part name, without namespace. + + + Part namespace. + + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + + +
+ + + + + + + + + Specifies allowed MimeType for a Collection + + + + Used to associate items with search folders. + +
diff --git a/src/server/storage/akonadidb.xsd b/src/server/storage/akonadidb.xsd new file mode 100644 index 0000000..82fdb7b --- /dev/null +++ b/src/server/storage/akonadidb.xsd @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/server/storage/collectionqueryhelper.cpp b/src/server/storage/collectionqueryhelper.cpp new file mode 100644 index 0000000..b2df514 --- /dev/null +++ b/src/server/storage/collectionqueryhelper.cpp @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionqueryhelper.h" + +#include "connection.h" +#include "handler.h" +#include "queryhelper.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void CollectionQueryHelper::remoteIdToQuery(const QStringList &rids, const CommandContext &context, QueryBuilder &qb) +{ + if (rids.size() == 1) { + qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::Equals, rids.first()); + } else { + qb.addValueCondition(Collection::remoteIdFullColumnName(), Query::In, rids); + } + + if (context.resource().isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + } +} + +void CollectionQueryHelper::scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb) +{ + if (scope.scope() == Scope::Uid) { + QueryHelper::setToQuery(scope.uidSet(), Collection::idFullColumnName(), qb); + } else if (scope.scope() == Scope::Rid) { + if (context.collectionId() <= 0 && !context.resource().isValid()) { + throw HandlerException("Operations based on remote identifiers require a resource or collection context"); + } + CollectionQueryHelper::remoteIdToQuery(scope.ridSet(), context, qb); + } else if (scope.scope() == Scope::HierarchicalRid) { + if (!context.resource().isValid()) { + throw HandlerException("Operations based on hierarchical remote identifiers require a resource or collection context"); + } + const Collection c = CollectionQueryHelper::resolveHierarchicalRID(scope.hridChain(), context.resource().id()); + qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, c.id()); + } else { + throw HandlerException("WTF?"); + } +} + +bool CollectionQueryHelper::hasAllowedName(const Collection &collection, const QString &name, Collection::Id parent) +{ + Q_UNUSED(collection) + SelectQueryBuilder qb; + if (parent > 0) { + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, parent); + } else { + qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); + } + qb.addValueCondition(Collection::nameColumn(), Query::Equals, name); + if (!qb.exec()) { + return false; + } + const QVector result = qb.result(); + if (!result.isEmpty()) { + return result.first().id() == collection.id(); + } + return true; +} + +bool CollectionQueryHelper::canBeMovedTo(const Collection &collection, const Collection &_parent) +{ + if (_parent.isValid()) { + Collection parent = _parent; + Q_FOREVER { + if (parent.id() == collection.id()) { + return false; // target is child of source + } + if (parent.parentId() == 0) { + break; + } + parent = parent.parent(); + } + } + return hasAllowedName(collection, collection.name(), _parent.id()); +} + +Collection CollectionQueryHelper::resolveHierarchicalRID(const QVector &ridChain, Resource::Id resId) +{ + if (ridChain.size() < 2) { + throw HandlerException("Empty or incomplete hierarchical RID chain"); + } + if (!ridChain.last().isEmpty()) { + throw HandlerException("Hierarchical RID chain is not root-terminated"); + } + Collection::Id parentId = 0; + Collection result; + for (int i = ridChain.size() - 2; i >= 0; --i) { + SelectQueryBuilder qb; + if (parentId > 0) { + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, parentId); + } else { + qb.addValueCondition(Collection::parentIdColumn(), Query::Is, QVariant()); + } + qb.addValueCondition(Collection::remoteIdColumn(), Query::Equals, ridChain.at(i).remoteId); + qb.addValueCondition(Collection::resourceIdColumn(), Query::Equals, resId); + if (!qb.exec()) { + throw HandlerException("Unable to execute query"); + } + const Collection::List results = qb.result(); + const int resultSize = results.size(); + if (resultSize == 0) { + throw HandlerException("Hierarchical RID does not specify an existing collection"); + } else if (resultSize > 1) { + throw HandlerException("Hierarchical RID does not specify a unique collection"); + } + result = results.first(); + parentId = result.id(); + } + return result; +} + +Collection CollectionQueryHelper::singleCollectionFromScope(const Scope &scope, const CommandContext &context) +{ + // root + if (scope.scope() == Scope::Uid && scope.uidSet().intervals().count() == 1) { + const ImapInterval i = scope.uidSet().intervals().at(0); + if (!i.size()) { // ### why do we need this hack for 0, shouldn't that be size() == 1? + Collection root; + root.setId(0); + return root; + } + } + SelectQueryBuilder qb; + scopeToQuery(scope, context, qb); + if (!qb.exec()) { + throw HandlerException("Unable to execute query"); + } + const Collection::List cols = qb.result(); + if (cols.isEmpty()) { + throw HandlerException("No collection found"); + } else if (cols.size() > 1) { + throw HandlerException("Collection cannot be uniquely identified"); + } + return cols.first(); +} diff --git a/src/server/storage/collectionqueryhelper.h b/src/server/storage/collectionqueryhelper.h new file mode 100644 index 0000000..4f13b99 --- /dev/null +++ b/src/server/storage/collectionqueryhelper.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +class CommandContext; +class QueryBuilder; + +/** + Helper methods to generate WHERE clauses for collection queries based on a Scope object. +*/ +namespace CollectionQueryHelper +{ +/** + Add conditions to @p qb for the given remote identifier @p rid. + The rid context is taken from @p context. +*/ +void remoteIdToQuery(const QStringList &rids, const CommandContext &context, QueryBuilder &qb); + +/** + Add conditions to @p qb for the given collection operation scope @p scope. + The rid context is taken from @p context, if none is specified an exception is thrown. +*/ +void scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb); + +/** + Checks if a collection could exist in the given parent folder with the given name. +*/ +bool hasAllowedName(const Collection &collection, const QString &name, Collection::Id parent); + +/** + Checks if a collection could be moved from its current parent into the given one. +*/ +bool canBeMovedTo(const Collection &collection, const Collection &parent); + +/** + Retrieve the collection referred to by the given hierarchical RID chain. +*/ +Collection resolveHierarchicalRID(const QVector &hridChain, Resource::Id resId); + +/** + Returns an existing collection specified by the given scope. If that does not + specify exactly one valid collection, an exception is thrown. +*/ +Collection singleCollectionFromScope(const Scope &scope, const CommandContext &context); +} + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/collectionstatistics.cpp b/src/server/storage/collectionstatistics.cpp new file mode 100644 index 0000000..93fc368 --- /dev/null +++ b/src/server/storage/collectionstatistics.cpp @@ -0,0 +1,209 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * SPDX-FileCopyrightText: 2016 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "collectionstatistics.h" +#include "akonadiserver_debug.h" +#include "countquerybuilder.h" +#include "datastore.h" +#include "entities.h" +#include "querybuilder.h" + +#include + +using namespace Akonadi::Server; + +CollectionStatistics::CollectionStatistics(bool prefetch) +{ + if (prefetch) { + QMutexLocker lock(&mCacheLock); + + QList builders; + // This single query will give us statistics for all non-empty non-virtual + // Collections at much better speed than individual queries. + auto qb = prepareGenericQuery(); + qb.addColumn(PimItem::collectionIdFullColumnName()); + qb.addGroupColumn(PimItem::collectionIdFullColumnName()); + builders << qb; + + // This single query will give us statistics for all non-empty virtual + // Collections + qb = prepareGenericQuery(); + qb.addColumn(CollectionPimItemRelation::leftFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, + CollectionPimItemRelation::tableName(), + CollectionPimItemRelation::rightFullColumnName(), + PimItem::idFullColumnName()); + qb.addGroupColumn(CollectionPimItemRelation::leftFullColumnName()); + builders << qb; + + for (auto &qb : builders) { + if (!qb.exec()) { + return; + } + + auto query = qb.query(); + while (query.next()) { + mCache.insert(query.value(3).toLongLong(), {query.value(0).toLongLong(), query.value(1).toLongLong(), query.value(2).toLongLong()}); + } + } + + // Now quickly get all non-virtual enabled Collections and if they are + // not in mCache yet, insert them with empty statistics. + qb = QueryBuilder(Collection::tableName()); + qb.addColumn(Collection::idColumn()); + qb.addValueCondition(Collection::enabledColumn(), Query::Equals, true); + qb.addValueCondition(Collection::isVirtualColumn(), Query::Equals, false); + if (!qb.exec()) { + return; + } + + auto query = qb.query(); + while (query.next()) { + const auto colId = query.value(0).toLongLong(); + if (!mCache.contains(colId)) { + mCache.insert(colId, {0, 0, 0}); + } + } + } +} + +void CollectionStatistics::itemAdded(const Collection &col, qint64 size, bool seen) +{ + if (!col.isValid()) { + return; + } + + QMutexLocker lock(&mCacheLock); + auto stats = mCache.find(col.id()); + if (stats != mCache.end()) { + ++(stats->count); + stats->size += size; + stats->read += (seen ? 1 : 0); + } else { + mCache.insert(col.id(), calculateCollectionStatistics(col)); + } +} + +void CollectionStatistics::itemsSeenChanged(const Collection &col, qint64 seenCount) +{ + if (!col.isValid()) { + return; + } + + QMutexLocker lock(&mCacheLock); + auto stats = mCache.find(col.id()); + if (stats != mCache.end()) { + stats->read += seenCount; + } else { + mCache.insert(col.id(), calculateCollectionStatistics(col)); + } +} + +void CollectionStatistics::invalidateCollection(const Collection &col) +{ + if (!col.isValid()) { + return; + } + + QMutexLocker lock(&mCacheLock); + mCache.remove(col.id()); +} + +void CollectionStatistics::expireCache() +{ + QMutexLocker lock(&mCacheLock); + mCache.clear(); +} + +CollectionStatistics::Statistics CollectionStatistics::statistics(const Collection &col) +{ + QMutexLocker lock(&mCacheLock); + auto it = mCache.find(col.id()); + if (it == mCache.end()) { + it = mCache.insert(col.id(), calculateCollectionStatistics(col)); + } + return it.value(); +} + +QueryBuilder CollectionStatistics::prepareGenericQuery() +{ + static const QString SeenFlagsTableName = QStringLiteral("SeenFlags"); + static const QString IgnoredFlagsTableName = QStringLiteral("IgnoredFlags"); + +#define FLAGS_COLUMN(table, column) QStringLiteral("%1.%2").arg(table##TableName, PimItemFlagRelation::column()) + + // COUNT(DISTINCT PimItemTable.id) + CountQueryBuilder qb(PimItem::tableName(), PimItem::idFullColumnName(), CountQueryBuilder::Distinct); + // SUM(PimItemTable.size) + qb.addAggregation(PimItem::sizeFullColumnName(), QStringLiteral("sum")); + + // SUM(CASE WHEN SeenFlags.flag_id IS NOT NULL OR IgnoredFlags.flag_id IS NOT NULL THEN 1 ELSE 0 END) + // This allows us to get read messages count in a single query with the other + // statistics. It is much than doing two queries, because the database + // only has to calculate the JOINs once. + // + // Flag::retrieveByName() will hit the Entity cache, which allows us to avoid + // a second JOIN with FlagTable, which PostgreSQL seems to struggle to optimize. + Query::Condition cond(Query::Or); + cond.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), Query::IsNot, QVariant()); + cond.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), Query::IsNot, QVariant()); + + Query::Case caseStmt(cond, QStringLiteral("1"), QStringLiteral("0")); + qb.addAggregation(caseStmt, QStringLiteral("sum")); + + // We need to join PimItemFlagRelation table twice - once for \SEEN flag and once + // for $IGNORED flag, otherwise entries from PimItemTable get duplicated when an + // item has both flags and the SUM(CASE ...) above returns bogus values + { + Query::Condition seenCondition(Query::And); + seenCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(SeenFlags, leftColumn)); + seenCondition.addValueCondition(FLAGS_COLUMN(SeenFlags, rightColumn), + Query::Equals, + Flag::retrieveByNameOrCreate(QStringLiteral(AKONADI_FLAG_SEEN)).id()); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), SeenFlagsTableName), seenCondition); + } + { + Query::Condition ignoredCondition(Query::And); + ignoredCondition.addColumnCondition(PimItem::idFullColumnName(), Query::Equals, FLAGS_COLUMN(IgnoredFlags, leftColumn)); + ignoredCondition.addValueCondition(FLAGS_COLUMN(IgnoredFlags, rightColumn), + Query::Equals, + Flag::retrieveByNameOrCreate(QStringLiteral(AKONADI_FLAG_IGNORED)).id()); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral("%1 AS %2").arg(PimItemFlagRelation::tableName(), IgnoredFlagsTableName), ignoredCondition); + } + +#undef FLAGS_COLUMN + + return std::move(qb); +} + +CollectionStatistics::Statistics CollectionStatistics::calculateCollectionStatistics(const Collection &col) +{ + auto qb = prepareGenericQuery(); + + if (col.isVirtual()) { + qb.addJoin(QueryBuilder::InnerJoin, + CollectionPimItemRelation::tableName(), + CollectionPimItemRelation::rightFullColumnName(), + PimItem::idFullColumnName()); + qb.addValueCondition(CollectionPimItemRelation::leftFullColumnName(), Query::Equals, col.id()); + } else { + qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, col.id()); + } + + if (!qb.exec()) { + return {-1, -1, -1}; + } + if (!qb.query().next()) { + qCCritical(AKONADISERVER_LOG) << "Error during retrieving result of statistics query:" << qb.query().lastError().text(); + return {-1, -1, -1}; + } + + auto result = Statistics{qb.query().value(0).toLongLong(), qb.query().value(1).toLongLong(), qb.query().value(2).toLongLong()}; + qb.query().finish(); + return result; +} diff --git a/src/server/storage/collectionstatistics.h b/src/server/storage/collectionstatistics.h new file mode 100644 index 0000000..e7289c8 --- /dev/null +++ b/src/server/storage/collectionstatistics.h @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + + +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class QueryBuilder; +class Collection; + +/** + * Provides cache for collection statistics + * + * Collection statistics are requested very often, so to take some load from the + * database we cache the results until the statistics are invalidated (see + * NotificationCollector, which takes care for invalidating the statistics). + * + * The cache (together with optimization of the actual SQL query) seems to + * massively improve initial folder listing on system start (when IO and CPU loads + * are very high). + */ +class CollectionStatistics +{ +public: + struct Statistics { + qint64 count; + qint64 size; + qint64 read; + }; + + explicit CollectionStatistics(bool prefetch = true); + virtual ~CollectionStatistics() = default; + + Statistics statistics(const Collection &col); + + void itemAdded(const Collection &col, qint64 size, bool seen); + void itemsSeenChanged(const Collection &col, qint64 seenCount); + + void invalidateCollection(const Collection &col); + + void expireCache(); + +protected: + QueryBuilder prepareGenericQuery(); + + virtual Statistics calculateCollectionStatistics(const Collection &col); + + QMutex mCacheLock; + QHash mCache; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/collectiontreecache.cpp b/src/server/storage/collectiontreecache.cpp new file mode 100644 index 0000000..8e53363 --- /dev/null +++ b/src/server/storage/collectiontreecache.cpp @@ -0,0 +1,382 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiontreecache.h" +#include "akonadiserver_debug.h" +#include "commandcontext.h" +#include "selectquerybuilder.h" + +#include + +#include + +#include +#include +#include +#include + +using namespace Akonadi::Server; + +namespace +{ +enum Column { + IdColumn, + ParentIdColumn, + RIDColumn, + DisplayPrefColumn, + SyncPrefColumn, + IndexPrefColumn, + EnabledColumn, + ReferencedColumn, + ResourceNameColumn, +}; +} + +CollectionTreeCache::Node::Node() + : parent(nullptr) + , lruCounter(0) + , id(-1) +{ +} + +CollectionTreeCache::Node::Node(const Collection &col) + : parent(nullptr) + , id(col.id()) + , collection(col) + +{ +} + +CollectionTreeCache::Node::~Node() +{ + qDeleteAll(children); +} + +void CollectionTreeCache::Node::appendChild(Node *child) +{ + child->parent = this; + children.push_back(child); +} + +void CollectionTreeCache::Node::removeChild(Node *child) +{ + child->parent = nullptr; + children.removeOne(child); +} + +CollectionTreeCache::CollectionTreeCache() + : AkThread(QStringLiteral("CollectionTreeCache")) +{ +} + +CollectionTreeCache::~CollectionTreeCache() +{ + quitThread(); +} + +void CollectionTreeCache::init() +{ + AkThread::init(); + + QWriteLocker locker(&mLock); + + mRoot = new Node; + mRoot->id = 0; + mRoot->parent = nullptr; + mNodeLookup.insert(0, mRoot); + + SelectQueryBuilder qb; + qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending); + + if (!qb.exec()) { + qCCritical(AKONADISERVER_LOG) << "Failed to initialize Collection tree cache!"; + return; + } + + // std's reverse iterators makes processing pendingNodes much easier. + std::multimap pendingNodes; + const auto collections = qb.result(); + for (const auto &col : collections) { + auto parent = mNodeLookup.value(col.parentId(), nullptr); + + auto node = new Node(col); + + if (parent) { + parent->appendChild(node); + mNodeLookup.insert(node->id, node); + } else { + pendingNodes.insert({col.parentId(), node}); + } + } + + if (!pendingNodes.empty()) { + int inserts = 0; + + // Thanks to the SQL results being ordered by ID we already inserted most + // of the nodes in the loop above and here we only handle the rare cases + // when child has ID lower than its parent, i.e. moved collections. + // + // In theory we may need multiple passes to insert all nodes, but we can + // optimize by iterating the ordered map in reverse order. This way we find + // the parents with higher IDs first and their children later, thus needing + // fewer passes to process all the nodes. + auto it = pendingNodes.rbegin(); + while (!pendingNodes.empty()) { + auto parent = mNodeLookup.value(it->first, nullptr); + if (!parent) { + // Parent of this node is still somewhere in pendingNodes, let's skip + // this one for now and try again in the next iteration + ++it; + } else { + auto node = it->second; + parent->appendChild(node); + mNodeLookup.insert(node->id, node); + pendingNodes.erase((++it).base()); + ++inserts; + } + + if (it == pendingNodes.rend()) { + if (Q_UNLIKELY(inserts == 0)) { + // This means we iterated through the entire pendingNodes but did + // not manage to insert any collection to the node tree. That + // means that there is an unreferenced collection in the database + // that points to an invalid parent (or has a parent which points + // to an invalid parent etc.). This should not happen + // anymore with DB constraints, but who knows... + qCWarning(AKONADISERVER_LOG) << "Found unreferenced Collections!"; + auto unref = pendingNodes.begin(); + while (unref != pendingNodes.end()) { + qCWarning(AKONADISERVER_LOG) << "\tCollection" << unref->second->id << "references an invalid parent" << it->first; + // Remove the unreferenced collection from the map + delete unref->second; + unref = pendingNodes.erase(unref); + } + qCWarning(AKONADISERVER_LOG) << "Please run \"akonadictl fsck\" to correct the inconsistencies!"; + // pendingNodes should be empty now so break the loop here + break; + } + + it = pendingNodes.rbegin(); + inserts = 0; + } + } + } + + Q_ASSERT(pendingNodes.empty()); + Q_ASSERT(mNodeLookup.size() == collections.count() + 1 /* root */); + // Now we should have a complete tree built, yay! +} + +void CollectionTreeCache::quit() +{ + delete mRoot; + + AkThread::quit(); +} + +void CollectionTreeCache::collectionAdded(const Collection &col) +{ + QWriteLocker locker(&mLock); + + auto parent = mNodeLookup.value(col.parentId(), nullptr); + if (!parent) { + qCWarning(AKONADISERVER_LOG) << "Received a new collection (" << col.id() << ") with unknown parent (" << col.parentId() << ")"; + return; + } + + auto node = new Node(col); + parent->appendChild(node); + mNodeLookup.insert(node->id, node); +} + +void CollectionTreeCache::collectionChanged(const Collection &col) +{ + QWriteLocker locker(&mLock); + + auto node = mNodeLookup.value(col.id(), nullptr); + if (!node) { + qCWarning(AKONADISERVER_LOG) << "Received an unknown changed collection (" << col.id() << ")"; + return; + } + + // Only update non-expired nodes + if (node->collection.isValid()) { + node->collection = col; + } +} + +void CollectionTreeCache::collectionMoved(const Collection &col) +{ + QWriteLocker locker(&mLock); + + auto node = mNodeLookup.value(col.id(), nullptr); + if (!node) { + qCWarning(AKONADISERVER_LOG) << "Received an unknown moved collection (" << col.id() << ")"; + return; + } + auto oldParent = node->parent; + + auto newParent = mNodeLookup.value(col.parentId(), nullptr); + if (!newParent) { + qCWarning(AKONADISERVER_LOG) << "Received a moved collection (" << col.id() << ") with an unknown move destination (" << col.parentId() << ")"; + return; + } + + oldParent->removeChild(node); + newParent->appendChild(node); + if (node->collection.isValid()) { + node->collection = col; + } +} + +void CollectionTreeCache::collectionRemoved(const Collection &col) +{ + QWriteLocker locker(&mLock); + + auto node = mNodeLookup.value(col.id(), nullptr); + if (!node) { + qCWarning(AKONADISERVER_LOG) << "Received unknown removed collection (" << col.id() << ")"; + return; + } + + auto parent = node->parent; + parent->removeChild(node); + mNodeLookup.remove(node->id); + delete node; +} + +CollectionTreeCache::Node *CollectionTreeCache::findNode(const QString &rid, const QString &resource) const +{ + QReadLocker locker(&mLock); + + // Find a subtree that belongs to the respective resource + auto root = std::find_if(mRoot->children.cbegin(), mRoot->children.cend(), [resource](Node *node) { + // resource().name() may seem expensive, but really + // there are only few resources and they are all cached + // in memory. + return node->collection.resource().name() == resource; + }); + if (root == mRoot->children.cend()) { + return nullptr; + } + + return findNode((*root), [rid](Node *node) { + return node->collection.remoteId() == rid; + }); +} + +QVector CollectionTreeCache::retrieveCollections(CollectionTreeCache::Node *root, int depth, int ancestorDepth) const +{ + QReadLocker locker(&mLock); + + QVector nodes; + // Get all ancestors for root + Node *parent = root->parent; + for (int i = 0; i < ancestorDepth && parent != nullptr; ++i) { + nodes.push_back(parent); + parent = parent->parent; + } + + struct StackTuple { + Node *node; + int depth; + }; + QStack stack; + stack.push({root, 0}); + while (!stack.isEmpty()) { + auto c = stack.pop(); + if (c.depth > depth) { + break; + } + + if (c.node->id > 0) { // skip root + nodes.push_back(c.node); + } + + for (auto child : std::as_const(c.node->children)) { + stack.push({child, c.depth + 1}); + } + } + + QVector cols; + QVector missing; + for (auto node : nodes) { + if (node->collection.isValid()) { + cols.push_back(node->collection); + } else { + missing.push_back(node); + } + } + + if (!missing.isEmpty()) { + // TODO: Check if no-one else is currently retrieving the same collections + SelectQueryBuilder qb; + Query::Condition cond(Query::Or); + for (auto node : std::as_const(missing)) { + cond.addValueCondition(Collection::idFullColumnName(), Query::Equals, node->id); + } + qb.addCondition(cond); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to retrieve collections from the database"; + return {}; + } + + const auto results = qb.result(); + if (results.size() != missing.size()) { + qCWarning(AKONADISERVER_LOG) << "Could not obtain all missing collections! Node tree refers to a non-existent collection"; + } + + cols += results; + + // Relock for write + // TODO: Needs a better lock-upgrade mechanism + locker.unlock(); + QWriteLocker wLocker(&mLock); + for (auto node : std::as_const(missing)) { + auto it = std::find_if(results.cbegin(), results.cend(), [node](const Collection &col) { + return node->id == col.id(); + }); + if (Q_UNLIKELY(it == results.cend())) { + continue; + } + + node->collection = *it; + } + } + + return cols; +} + +QVector +CollectionTreeCache::retrieveCollections(const Scope &scope, int depth, int ancestorDepth, const QString &resource, CommandContext *context) const +{ + if (scope.isEmpty()) { + return retrieveCollections(mRoot, depth, ancestorDepth); + } else if (scope.scope() == Scope::Rid) { + // Caller must ensure! + Q_ASSERT(!resource.isEmpty() || (context && context->resource().isValid())); + + Node *node = nullptr; + if (!resource.isEmpty()) { + node = findNode(scope.rid(), resource); + } else if (context && context->resource().isValid()) { + node = findNode(scope.rid(), context->resource().name()); + } else { + return {}; + } + + if (Q_LIKELY(node)) { + return retrieveCollections(node, depth, ancestorDepth); + } + } else if (scope.scope() == Scope::Uid) { + Node *node = mNodeLookup.value(scope.uid()); + if (Q_LIKELY(node)) { + return retrieveCollections(node, depth, ancestorDepth); + } + } + + return {}; +} diff --git a/src/server/storage/collectiontreecache.h b/src/server/storage/collectiontreecache.h new file mode 100644 index 0000000..dd43f2d --- /dev/null +++ b/src/server/storage/collectiontreecache.h @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2017 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akthread.h" +#include "entities.h" + +#include + +#include +#include +#include + +namespace Akonadi +{ +class Scope; + +namespace Server +{ +class CommandContext; + +class CollectionTreeCache : public AkThread +{ + Q_OBJECT + +protected: + class Node + { + public: + explicit Node(); + explicit Node(const Collection &query); + + ~Node(); + + void appendChild(Node *child); + void removeChild(Node *child); + + Node *parent = nullptr; + QVector children; + QAtomicInt lruCounter; + qint64 id; + + Collection collection; + }; + +public: + explicit CollectionTreeCache(); + ~CollectionTreeCache() override; + + QVector + retrieveCollections(const Scope &scope, int depth, int ancestorDepth, const QString &resource = QString(), CommandContext *context = nullptr) const; + +public Q_SLOTS: + void collectionAdded(const Collection &col); + void collectionChanged(const Collection &col); + void collectionMoved(const Collection &col); + void collectionRemoved(const Collection &col); + +protected: + void init() override; + void quit() override; + + Node *findNode(const QString &rid, const QString &resource) const; + + template Node *findNode(Node *root, Predicate pred) const; + + QVector retrieveCollections(Node *root, int depth, int ancestorDepth) const; + +protected: + mutable QReadWriteLock mLock; + + Node *mRoot = nullptr; + + QHash mNodeLookup; +}; + +// Non-recursive depth-first tree traversal, looking for first Node matching the predicate +template CollectionTreeCache::Node *CollectionTreeCache::findNode(Node *root, Predicate pred) const +{ + QList toVisit = {root}; + // We expect a single subtree to not contain more than 1/4 of all collections, + // which is an arbitrary guess, but should be good enough for most cases. + toVisit.reserve(mNodeLookup.size() / 4); + while (!toVisit.isEmpty()) { + auto node = toVisit.takeFirst(); + if (pred(node)) { + return node; + } + + for (auto child : std::as_const(node->children)) { + toVisit.prepend(child); + } + } + + return nullptr; +} + +} // namespace Server +} // namespace Akonadi diff --git a/src/server/storage/countquerybuilder.h b/src/server/storage/countquerybuilder.h new file mode 100644 index 0000000..2b4b045 --- /dev/null +++ b/src/server/storage/countquerybuilder.h @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef AKONADI_COUNTQUERYBUILDER_H +#define AKONADI_COUNTQUERYBUILDER_H + +#include "akonadiserver_debug.h" +#include "storage/querybuilder.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +/** + Helper class for creating queries to count elements in a database. +*/ +class CountQueryBuilder : public QueryBuilder +{ +public: + enum CountMode { + All, + Distinct, + }; + + /** + Creates a new query builder that counts all entries in @p table. + */ + explicit inline CountQueryBuilder(const QString &table) + : QueryBuilder(table, Select) + { + addColumn(QStringLiteral("count(*)")); + } + + /** + * Creates a new query builder that counts entries in @p column of @p table. + * If @p mode is set to @c Distinct, duplicate entries in that column are ignored. + */ + inline CountQueryBuilder(const QString &table, const QString &column, CountMode mode) + : QueryBuilder(table, Select) + { + Q_ASSERT(!table.isEmpty()); + Q_ASSERT(!column.isEmpty()); + QString s = QStringLiteral("count("); + if (mode == Distinct) { + s += QLatin1String("DISTINCT "); + } + s += column; + s += QLatin1Char(')'); + addColumn(s); + } + + /** + Returns the result of this query. + @returns -1 on error. + */ + inline int result() + { + if (!query().next()) { + qCDebug(AKONADISERVER_LOG) << "Error during retrieving result of query:" << query().lastError().text(); + return -1; + } + const auto result = query().value(0).toInt(); + query().finish(); + return result; + } +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/datastore.cpp b/src/server/storage/datastore.cpp new file mode 100644 index 0000000..a68ed12 --- /dev/null +++ b/src/server/storage/datastore.cpp @@ -0,0 +1,1471 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Andreas Gungl * + * SPDX-FileCopyrightText: 2007 Robert Zwerus * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "datastore.h" + +#include "akonadi.h" +#include "akonadischema.h" +#include "akonadiserver_debug.h" +#include "collectionqueryhelper.h" +#include "collectionstatistics.h" +#include "countquerybuilder.h" +#include "dbconfig.h" +#include "dbinitializer.h" +#include "dbupdater.h" +#include "handler.h" +#include "handlerhelper.h" +#include "notificationmanager.h" +#include "parthelper.h" +#include "parttypehelper.h" +#include "querycache.h" +#include "queryhelper.h" +#include "selectquerybuilder.h" +#include "storagedebugger.h" +#include "tracer.h" +#include "transaction.h" +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +bool DataStore::s_hasForeignKeyConstraints = false; +QMutex DataStore::sTransactionMutex = {}; + +static QThreadStorage sInstances; + +#define TRANSACTION_MUTEX_LOCK \ + if (DbType::isSystemSQLite(m_database)) \ + sTransactionMutex.lock() +#define TRANSACTION_MUTEX_UNLOCK \ + if (DbType::isSystemSQLite(m_database)) \ + sTransactionMutex.unlock() + +#define setBoolPtr(ptr, val) \ + { \ + if ((ptr)) { \ + *(ptr) = (val); \ + } \ + } + +std::unique_ptr DataStore::sFactory; + +void DataStore::setFactory(std::unique_ptr factory) +{ + sFactory = std::move(factory); +} + +/*************************************************************************** + * DataStore * + ***************************************************************************/ +DataStore::DataStore(AkonadiServer &akonadi) + : m_akonadi(akonadi) + , m_dbOpened(false) + , m_transactionLevel(0) + , m_keepAliveTimer(nullptr) +{ + if (DbConfig::configuredDatabase()->driverName() == QLatin1String("QMYSQL")) { + // Send a dummy query to MySQL every 1 hour to keep the connection alive, + // otherwise MySQL just drops the connection and our subsequent queries fail + // without properly reporting the error + m_keepAliveTimer = new QTimer(this); + m_keepAliveTimer->setInterval(3600 * 1000); + QObject::connect(m_keepAliveTimer, &QTimer::timeout, this, &DataStore::sendKeepAliveQuery); + } +} + +DataStore::~DataStore() +{ + Q_ASSERT_X(!m_dbOpened, "DataStore", "Attempting to destroy DataStore with opened DB connection. Call close() first!"); +} + +void DataStore::open() +{ + m_connectionName = QUuid::createUuid().toString() + QString::number(reinterpret_cast(QThread::currentThread())); + Q_ASSERT(!QSqlDatabase::contains(m_connectionName)); + + m_database = QSqlDatabase::addDatabase(DbConfig::configuredDatabase()->driverName(), m_connectionName); + DbConfig::configuredDatabase()->apply(m_database); + + if (!m_database.isValid()) { + m_dbOpened = false; + return; + } + m_dbOpened = m_database.open(); + + if (!m_dbOpened) { + debugLastDbError("Cannot open database."); + } else { + qCDebug(AKONADISERVER_LOG) << "Database" << m_database.databaseName() << "opened using driver" << m_database.driverName(); + } + + StorageDebugger::instance()->addConnection(reinterpret_cast(this), QThread::currentThread()->objectName()); + connect(QThread::currentThread(), &QThread::objectNameChanged, this, [this](const QString &name) { + if (!name.isEmpty()) { + StorageDebugger::instance()->changeConnection(reinterpret_cast(this), name); + } + }); + + DbConfig::configuredDatabase()->initSession(m_database); + + if (m_keepAliveTimer) { + m_keepAliveTimer->start(); + } +} + +QSqlDatabase DataStore::database() +{ + if (!m_dbOpened) { + open(); + } + return m_database; +} + +void DataStore::close() +{ + if (m_keepAliveTimer) { + m_keepAliveTimer->stop(); + } + + if (!m_dbOpened) { + return; + } + + if (inTransaction()) { + // By setting m_transactionLevel to '1' here, we skip all nested transactions + // and rollback the outermost transaction. + m_transactionLevel = 1; + rollbackTransaction(); + } + + QueryCache::clear(); + m_database.close(); + m_database = QSqlDatabase(); + QSqlDatabase::removeDatabase(m_connectionName); + + StorageDebugger::instance()->removeConnection(reinterpret_cast(this)); + + m_dbOpened = false; +} + +bool DataStore::init() +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + + AkonadiSchema schema; + DbInitializer::Ptr initializer = DbInitializer::createInstance(database(), &schema); + if (!initializer->run()) { + qCCritical(AKONADISERVER_LOG) << initializer->errorMsg(); + return false; + } + s_hasForeignKeyConstraints = initializer->hasForeignKeyConstraints(); + + if (QFile::exists(QStringLiteral(":dbupdate.xml"))) { + DbUpdater updater(database(), QStringLiteral(":dbupdate.xml")); + if (!updater.run()) { + return false; + } + } else { + qCWarning(AKONADISERVER_LOG) << "Warning: dbupdate.xml not found, skipping updates"; + } + + if (!initializer->updateIndexesAndConstraints()) { + qCCritical(AKONADISERVER_LOG) << initializer->errorMsg(); + return false; + } + + // enable caching for some tables + MimeType::enableCache(true); + Flag::enableCache(true); + Resource::enableCache(true); + Collection::enableCache(true); + PartType::enableCache(true); + + return true; +} + +NotificationCollector *DataStore::notificationCollector() +{ + if (!mNotificationCollector) { + mNotificationCollector = std::make_unique(m_akonadi, this); + } + + return mNotificationCollector.get(); +} + +DataStore *DataStore::self() +{ + if (!sInstances.hasLocalData()) { + sInstances.setLocalData(sFactory->createStore()); + } + return sInstances.localData(); +} + +bool DataStore::hasDataStore() +{ + return sInstances.hasLocalData(); +} + +/* --- ItemFlags ----------------------------------------------------- */ + +bool DataStore::setItemsFlags(const PimItem::List &items, + const QVector *currentFlags, + const QVector &newFlags, + bool *flagsChanged, + const Collection &col_, + bool silent) +{ + QSet removedFlags; + QSet addedFlags; + QVariantList insIds; + QVariantList insFlags; + Query::Condition delConds(Query::Or); + Collection col = col_; + + setBoolPtr(flagsChanged, false); + + for (const PimItem &item : items) { + const Flag::List itemFlags = currentFlags ? *currentFlags : item.flags(); // optimization + for (const Flag &flag : itemFlags) { + if (!newFlags.contains(flag)) { + removedFlags << flag.name(); + Query::Condition cond; + cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::Equals, item.id()); + cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::Equals, flag.id()); + delConds.addCondition(cond); + } + } + + for (const Flag &flag : newFlags) { + if (!itemFlags.contains(flag)) { + addedFlags << flag.name(); + insIds << item.id(); + insFlags << flag.id(); + } + } + + if (col.id() == -1) { + col.setId(item.collectionId()); + } else if (col.id() != item.collectionId()) { + col.setId(-2); + } + } + + if (!removedFlags.empty()) { + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); + qb.addCondition(delConds); + if (!qb.exec()) { + return false; + } + } + + if (!addedFlags.empty()) { + QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemFlagRelation::leftColumn(), insIds); + qb2.setColumnValue(PimItemFlagRelation::rightColumn(), insFlags); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + return false; + } + } + + if (!silent && (!addedFlags.isEmpty() || !removedFlags.isEmpty())) { + QSet addedFlagsBa; + QSet removedFlagsBa; + for (const auto &addedFlag : std::as_const(addedFlags)) { + addedFlagsBa.insert(addedFlag.toLatin1()); + } + for (const auto &removedFlag : std::as_const(removedFlags)) { + removedFlagsBa.insert(removedFlag.toLatin1()); + } + notificationCollector()->itemsFlagsChanged(items, addedFlagsBa, removedFlagsBa, col); + } + + setBoolPtr(flagsChanged, (addedFlags != removedFlags)); + + return true; +} + +bool DataStore::doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet &existing, const Collection &col_, bool silent) +{ + Collection col = col_; + QVariantList flagIds; + QVariantList appendIds; + PimItem::List appendItems; + for (const PimItem &item : items) { + if (existing.contains(item.id())) { + continue; + } + + flagIds << flag.id(); + appendIds << item.id(); + appendItems << item; + + if (col.id() == -1) { + col.setId(item.collectionId()); + } else if (col.id() != item.collectionId()) { + col.setId(-2); + } + } + + if (appendItems.isEmpty()) { + return true; // all items have the desired flags already + } + + QueryBuilder qb2(PimItemFlagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemFlagRelation::leftColumn(), appendIds); + qb2.setColumnValue(PimItemFlagRelation::rightColumn(), flagIds); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append flag" << flag.name() << "to Items" << appendIds; + return false; + } + + if (!silent) { + notificationCollector()->itemsFlagsChanged(appendItems, {flag.name().toLatin1()}, {}, col); + } + + return true; +} + +bool DataStore::appendItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged, + bool checkIfExists, + const Collection &col, + bool silent) +{ + QVariantList itemsIds; + itemsIds.reserve(items.count()); + for (const PimItem &item : items) { + itemsIds.append(item.id()); + } + + setBoolPtr(flagsChanged, false); + + for (const Flag &flag : flags) { + QSet existing; + if (checkIfExists) { + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Select); + Query::Condition cond; + cond.addValueCondition(PimItemFlagRelation::rightColumn(), Query::Equals, flag.id()); + cond.addValueCondition(PimItemFlagRelation::leftColumn(), Query::In, itemsIds); + qb.addColumn(PimItemFlagRelation::leftColumn()); + qb.addCondition(cond); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing flags for Items " << itemsIds; + return false; + } + + QSqlQuery query = qb.query(); + if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { + // The query size feature is not supported by the sqllite driver + if (query.size() == items.count()) { + continue; + } + setBoolPtr(flagsChanged, true); + } + + while (query.next()) { + existing << query.value(0).value(); + } + if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) { + if (existing.size() != items.count()) { + setBoolPtr(flagsChanged, true); + } + } + query.finish(); + } + + if (!doAppendItemsFlag(items, flag, existing, col, silent)) { + return false; + } + } + + return true; +} + +bool DataStore::removeItemsFlags(const PimItem::List &items, const QVector &flags, bool *flagsChanged, const Collection &col_, bool silent) +{ + Collection col = col_; + QSet removedFlags; + QVariantList itemsIds; + QVariantList flagsIds; + + setBoolPtr(flagsChanged, false); + itemsIds.reserve(items.count()); + + for (const PimItem &item : items) { + itemsIds << item.id(); + if (col.id() == -1) { + col.setId(item.collectionId()); + } else if (col.id() != item.collectionId()) { + col.setId(-2); + } + for (int i = 0; i < flags.count(); ++i) { + const QString flagName = flags[i].name(); + if (!removedFlags.contains(flagName)) { + flagsIds << flags[i].id(); + removedFlags << flagName; + } + } + } + + // Delete all given flags from all given items in one go + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); + Query::Condition cond(Query::And); + cond.addValueCondition(PimItemFlagRelation::rightFullColumnName(), Query::In, flagsIds); + cond.addValueCondition(PimItemFlagRelation::leftFullColumnName(), Query::In, itemsIds); + qb.addCondition(cond); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove flags" << flags << "from Items" << itemsIds; + return false; + } + + if (qb.query().numRowsAffected() != 0) { + setBoolPtr(flagsChanged, true); + if (!silent) { + QSet removedFlagsBa; + for (const auto &remoteFlag : std::as_const(removedFlags)) { + removedFlagsBa.insert(remoteFlag.toLatin1()); + } + notificationCollector()->itemsFlagsChanged(items, {}, removedFlagsBa, col); + } + } + + return true; +} + +/* --- ItemTags ----------------------------------------------------- */ + +bool DataStore::setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) +{ + QSet removedTags; + QSet addedTags; + QVariantList insIds; + QVariantList insTags; + Query::Condition delConds(Query::Or); + + setBoolPtr(tagsChanged, false); + + for (const PimItem &item : items) { + const Tag::List itemTags = item.tags(); + for (const Tag &tag : itemTags) { + if (!tags.contains(tag)) { + // Remove tags from items that had it set + removedTags << tag.id(); + Query::Condition cond; + cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::Equals, item.id()); + cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, tag.id()); + delConds.addCondition(cond); + } + } + + for (const Tag &tag : tags) { + if (!itemTags.contains(tag)) { + // Add tags to items that did not have the tag + addedTags << tag.id(); + insIds << item.id(); + insTags << tag.id(); + } + } + } + + if (!removedTags.empty()) { + QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); + qb.addCondition(delConds); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTags << "from Items"; + return false; + } + } + + if (!addedTags.empty()) { + QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemTagRelation::leftColumn(), insIds); + qb2.setColumnValue(PimItemTagRelation::rightColumn(), insTags); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to add tags" << addedTags << "to Items"; + return false; + } + } + + if (!silent && (!addedTags.empty() || !removedTags.empty())) { + notificationCollector()->itemsTagsChanged(items, addedTags, removedTags); + } + + setBoolPtr(tagsChanged, (addedTags != removedTags)); + + return true; +} + +bool DataStore::doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet &existing, const Collection &col, bool silent) +{ + QVariantList tagIds; + QVariantList appendIds; + PimItem::List appendItems; + for (const PimItem &item : items) { + if (existing.contains(item.id())) { + continue; + } + + tagIds << tag.id(); + appendIds << item.id(); + appendItems << item; + } + + if (appendItems.isEmpty()) { + return true; // all items have the desired tags already + } + + QueryBuilder qb2(PimItemTagRelation::tableName(), QueryBuilder::Insert); + qb2.setColumnValue(PimItemTagRelation::leftColumn(), appendIds); + qb2.setColumnValue(PimItemTagRelation::rightColumn(), tagIds); + qb2.setIdentificationColumn(QString()); + if (!qb2.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append tag" << tag << "to Items" << appendItems; + return false; + } + + if (!silent) { + notificationCollector()->itemsTagsChanged(appendItems, {tag.id()}, {}, col); + } + + return true; +} + +bool DataStore::appendItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool checkIfExists, const Collection &col, bool silent) +{ + QVariantList itemsIds; + itemsIds.reserve(items.count()); + for (const PimItem &item : items) { + itemsIds.append(item.id()); + } + + setBoolPtr(tagsChanged, false); + + for (const Tag &tag : tags) { + QSet existing; + if (checkIfExists) { + QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Select); + Query::Condition cond; + cond.addValueCondition(PimItemTagRelation::rightColumn(), Query::Equals, tag.id()); + cond.addValueCondition(PimItemTagRelation::leftColumn(), Query::In, itemsIds); + qb.addColumn(PimItemTagRelation::leftColumn()); + qb.addCondition(cond); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to retrieve existing tag" << tag << "for Items" << itemsIds; + return false; + } + + QSqlQuery query = qb.query(); + if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { + if (query.size() == items.count()) { + continue; + } + setBoolPtr(tagsChanged, true); + } + + while (query.next()) { + existing << query.value(0).value(); + } + if (!query.driver()->hasFeature(QSqlDriver::QuerySize)) { + if (existing.size() != items.count()) { + setBoolPtr(tagsChanged, true); + } + } + query.finish(); + } + + if (!doAppendItemsTag(items, tag, existing, col, silent)) { + return false; + } + } + + return true; +} + +bool DataStore::removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged, bool silent) +{ + QSet removedTags; + QVariantList itemsIds; + QVariantList tagsIds; + + setBoolPtr(tagsChanged, false); + itemsIds.reserve(items.count()); + + for (const PimItem &item : items) { + itemsIds << item.id(); + for (int i = 0; i < tags.count(); ++i) { + const qint64 tagId = tags[i].id(); + if (!removedTags.contains(tagId)) { + tagsIds << tagId; + removedTags << tagId; + } + } + } + + // Delete all given tags from all given items in one go + QueryBuilder qb(PimItemTagRelation::tableName(), QueryBuilder::Delete); + Query::Condition cond(Query::And); + cond.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, tagsIds); + cond.addValueCondition(PimItemTagRelation::leftFullColumnName(), Query::In, itemsIds); + qb.addCondition(cond); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << tagsIds << "from Items" << itemsIds; + return false; + } + + if (qb.query().numRowsAffected() != 0) { + setBoolPtr(tagsChanged, true); + if (!silent) { + notificationCollector()->itemsTagsChanged(items, QSet(), removedTags); + } + } + + return true; +} + +bool DataStore::removeTags(const Tag::List &tags, bool silent) +{ + // Currently the "silent" argument is only for API symmetry + Q_UNUSED(silent) + + QVariantList removedTagsIds; + QSet removedTags; + removedTagsIds.reserve(tags.count()); + removedTags.reserve(tags.count()); + for (const Tag &tag : tags) { + removedTagsIds << tag.id(); + removedTags << tag.id(); + } + + // Get all PIM items that we will untag + SelectQueryBuilder itemsQuery; + itemsQuery.addJoin(QueryBuilder::LeftJoin, PimItemTagRelation::tableName(), PimItemTagRelation::leftFullColumnName(), PimItem::idFullColumnName()); + itemsQuery.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::In, removedTagsIds); + + if (!itemsQuery.exec()) { + qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to query Items for given tags" << removedTagsIds; + return false; + } + const PimItem::List items = itemsQuery.result(); + + if (!items.isEmpty()) { + notificationCollector()->itemsTagsChanged(items, QSet(), removedTags); + } + + for (const Tag &tag : tags) { + // Emit special tagRemoved notification for each resource that owns the tag + QueryBuilder qb(TagRemoteIdResourceRelation::tableName(), QueryBuilder::Select); + qb.addColumn(TagRemoteIdResourceRelation::remoteIdFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, Resource::tableName(), TagRemoteIdResourceRelation::resourceIdFullColumnName(), Resource::idFullColumnName()); + qb.addColumn(Resource::nameFullColumnName()); + qb.addValueCondition(TagRemoteIdResourceRelation::tagIdFullColumnName(), Query::Equals, tag.id()); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Removing tags failed: failed to retrieve RIDs for tag" << tag.id(); + return false; + } + + // Emit specialized notifications for each resource + QSqlQuery query = qb.query(); + while (query.next()) { + const QString rid = query.value(0).toString(); + const QByteArray resource = query.value(1).toByteArray(); + + notificationCollector()->tagRemoved(tag, resource, rid); + } + query.finish(); + + // And one for clients - without RID + notificationCollector()->tagRemoved(tag, QByteArray(), QString()); + } + + // Just remove the tags, table constraints will take care of the rest + QueryBuilder qb(Tag::tableName(), QueryBuilder::Delete); + qb.addValueCondition(Tag::idColumn(), Query::In, removedTagsIds); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove tags" << removedTagsIds; + return false; + } + + return true; +} + +/* --- ItemParts ----------------------------------------------------- */ + +bool DataStore::removeItemParts(const PimItem &item, const QSet &parts) +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); + qb.addCondition(PartTypeHelper::conditionFromFqNames(parts)); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Removing item parts failed: failed to query parts" << parts << "from Item " << item.id(); + return false; + } + + const Part::List existingParts = qb.result(); + for (Part part : std::as_const(existingParts)) { + if (!PartHelper::remove(&part)) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name() + << ") from Item" << item.id(); + return false; + } + } + + notificationCollector()->itemChanged(item, parts); + return true; +} + +bool DataStore::invalidateItemCache(const PimItem &item) +{ + // find all payload item parts + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PartType::tableName(), Part::partTypeIdFullColumnName(), PartType::idFullColumnName()); + qb.addValueCondition(Part::pimItemIdFullColumnName(), Query::Equals, item.id()); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + qb.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QLatin1String("PLD")); + qb.addValueCondition(PimItem::dirtyFullColumnName(), Query::Equals, false); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to invalidate cache for Item" << item.id(); + return false; + } + + const Part::List parts = qb.result(); + // clear data field + for (Part part : parts) { + if (!PartHelper::truncate(part)) { + qCWarning(AKONADISERVER_LOG) << "Failed to truncate payload part" << part.id() << "(" << part.partType().ns() << ":" << part.partType().name() + << ") of Item" << item.id(); + return false; + } + } + + return true; +} + +/* --- Collection ------------------------------------------------------ */ +bool DataStore::appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap &attributes) +{ + // no need to check for already existing collection with the same name, + // a unique index on parent + name prevents that in the database + if (!collection.insert()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append Collection" << collection.name() << "in resource" << collection.resource().name(); + return false; + } + + if (!appendMimeTypeForCollection(collection.id(), mimeTypes)) { + qCWarning(AKONADISERVER_LOG) << "Failed to append mimetypes" << mimeTypes << "to new collection" << collection.name() << "(ID" << collection.id() + << ") in resource" << collection.resource().name(); + return false; + } + + for (auto it = attributes.cbegin(), end = attributes.cend(); it != end; ++it) { + if (!addCollectionAttribute(collection, it.key(), it.value(), true)) { + qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << it.key() << "to new collection" << collection.name() << "(ID" << collection.id() + << ") in resource" << collection.resource().name(); + return false; + } + } + + notificationCollector()->collectionAdded(collection); + return true; +} + +bool DataStore::cleanupCollection(Collection &collection) +{ + if (!s_hasForeignKeyConstraints) { + return cleanupCollection_slow(collection); + } + + // db will do most of the work for us, we just deal with notifications and external payload parts here + Q_ASSERT(s_hasForeignKeyConstraints); + + // collect item deletion notifications + const PimItem::List items = collection.items(); + const QByteArray resource = collection.resource().name().toLatin1(); + + // generate the notification before actually removing the data + // TODO: we should try to get rid of this, requires client side changes to resources and Monitor though + notificationCollector()->itemsRemoved(items, collection, resource); + + // remove all external payload parts + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::dataFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addValueCondition(Collection::idFullColumnName(), Query::Equals, collection.id()); + qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to cleanup collection" << collection.name() << "(ID" << collection.id() << "):" + << "Failed to query existing payload parts"; + return false; + } + + try { + while (qb.query().next()) { + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(qb.query().value(0).toByteArray())); + } + } catch (const PartHelperException &e) { + qb.query().finish(); + qCWarning(AKONADISERVER_LOG) << "PartHelperException while cleaning up collection" << collection.name() << "(ID" << collection.id() << "):" << e.what(); + return false; + } + qb.query().finish(); + + // delete the collection itself, referential actions will do the rest + notificationCollector()->collectionRemoved(collection); + return collection.remove(); +} + +bool DataStore::cleanupCollection_slow(Collection &collection) +{ + Q_ASSERT(!s_hasForeignKeyConstraints); + + // delete the content + const PimItem::List items = collection.items(); + const QByteArray resource = collection.resource().name().toLatin1(); + notificationCollector()->itemsRemoved(items, collection, resource); + + for (const PimItem &item : items) { + if (!item.clearFlags()) { // TODO: move out of loop and use only a single query + qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() << ")" + << "failed: error clearing items flags"; + return false; + } + if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { // TODO: reduce to single query + qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() << ")" + << "failed: error clearing item payload parts"; + + return false; + } + + if (!PimItem::remove(PimItem::idColumn(), item.id())) { // TODO: move into single querya + qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() << ")" + << "failed: error clearing items"; + return false; + } + + if (!Entity::clearRelation(item.id(), Entity::Right)) { // TODO: move into single query + qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() << ")" + << "failed: error clearing linked items"; + return false; + } + } + + // delete collection mimetypes + collection.clearMimeTypes(); + Collection::clearPimItems(collection.id()); + + // delete attributes + Q_FOREACH (CollectionAttribute attr, collection.attributes()) { // krazy:exclude=foreach + if (!attr.remove()) { + qCWarning(AKONADISERVER_LOG) << "Slow cleanup of collection" << collection.name() << "(ID" << collection.id() << ")" + << "failed: error clearing attribute" << attr.type(); + return false; + } + } + + // delete the collection itself + notificationCollector()->collectionRemoved(collection); + return collection.remove(); +} + +static bool recursiveSetResourceId(const Collection &collection, qint64 resourceId) +{ + Transaction transaction(DataStore::self(), QStringLiteral("RECURSIVE SET RESOURCEID")); + + QueryBuilder qb(Collection::tableName(), QueryBuilder::Update); + qb.addValueCondition(Collection::parentIdColumn(), Query::Equals, collection.id()); + qb.setColumnValue(Collection::resourceIdColumn(), resourceId); + qb.setColumnValue(Collection::remoteIdColumn(), QVariant()); + qb.setColumnValue(Collection::remoteRevisionColumn(), QVariant()); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to set resource ID" << resourceId << "to collection" << collection.name() << "(ID" << collection.id() << ")"; + return false; + } + + // this is a cross-resource move, so also reset any resource-specific data (RID, RREV, etc) + // as well as mark the items dirty to prevent cache purging before they have been written back + qb = QueryBuilder(PimItem::tableName(), QueryBuilder::Update); + qb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, collection.id()); + qb.setColumnValue(PimItem::remoteIdColumn(), QVariant()); + qb.setColumnValue(PimItem::remoteRevisionColumn(), QVariant()); + const QDateTime now = QDateTime::currentDateTimeUtc(); + qb.setColumnValue(PimItem::datetimeColumn(), now); + qb.setColumnValue(PimItem::atimeColumn(), now); + qb.setColumnValue(PimItem::dirtyColumn(), true); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed reset RID/RREV for PimItems in Collection" << collection.name() << "(ID" << collection.id() << ")"; + return false; + } + + transaction.commit(); + + Q_FOREACH (const Collection &col, collection.children()) { + if (!recursiveSetResourceId(col, resourceId)) { + return false; + } + } + return true; +} + +bool DataStore::moveCollection(Collection &collection, const Collection &newParent) +{ + if (collection.parentId() == newParent.id()) { + return true; + } + + if (!m_dbOpened) { + return false; + } + + if (!newParent.isValid()) { + qCWarning(AKONADISERVER_LOG) << "Failed to move collection" << collection.name() << "(ID" << collection.id() << "): invalid destination"; + return false; + } + + const QByteArray oldResource = collection.resource().name().toLatin1(); + + int resourceId = collection.resourceId(); + const Collection source = collection.parent(); + if (newParent.id() > 0) { // not root + resourceId = newParent.resourceId(); + } + if (!CollectionQueryHelper::canBeMovedTo(collection, newParent)) { + return false; + } + + collection.setParentId(newParent.id()); + if (collection.resourceId() != resourceId) { + collection.setResourceId(resourceId); + collection.setRemoteId(QString()); + collection.setRemoteRevision(QString()); + if (!recursiveSetResourceId(collection, resourceId)) { + return false; + } + } + + if (!collection.update()) { + qCWarning(AKONADISERVER_LOG) << "Failed to move Collection" << collection.name() << "(ID" << collection.id() << ")" + << "into Collection" << collection.name() << "(ID" << collection.id() << ")"; + return false; + } + + notificationCollector()->collectionMoved(collection, source, oldResource, newParent.resource().name().toLatin1()); + return true; +} + +bool DataStore::appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes) +{ + if (mimeTypes.isEmpty()) { + return true; + } + + for (const QString &mimeType : mimeTypes) { + const auto &mt = MimeType::retrieveByNameOrCreate(mimeType); + if (!mt.isValid()) { + return false; + } + if (!Collection::addMimeType(collectionId, mt.id())) { + qCWarning(AKONADISERVER_LOG) << "Failed to append mimetype" << mt.name() << "to Collection" << collectionId; + return false; + } + } + + return true; +} + +void DataStore::activeCachePolicy(Collection &col) +{ + if (!col.cachePolicyInherit()) { + return; + } + + Collection parent = col; + while (parent.parentId() != 0) { + parent = parent.parent(); + if (!parent.cachePolicyInherit()) { + col.setCachePolicyCheckInterval(parent.cachePolicyCheckInterval()); + col.setCachePolicyCacheTimeout(parent.cachePolicyCacheTimeout()); + col.setCachePolicySyncOnDemand(parent.cachePolicySyncOnDemand()); + col.setCachePolicyLocalParts(parent.cachePolicyLocalParts()); + return; + } + } + + // ### system default + col.setCachePolicyCheckInterval(-1); + col.setCachePolicyCacheTimeout(-1); + col.setCachePolicySyncOnDemand(false); + col.setCachePolicyLocalParts(QStringLiteral("ALL")); +} + +QVector DataStore::virtualCollections(const PimItem &item) +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); + qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, item.id()); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to query virtual collections which PimItem" << item.id() << "belongs into"; + return QVector(); + } + + return qb.result(); +} + +QMap> DataStore::virtualCollections(const PimItem::List &items) +{ + QueryBuilder qb(CollectionPimItemRelation::tableName(), QueryBuilder::Select); + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), Collection::idFullColumnName(), CollectionPimItemRelation::leftFullColumnName()); + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), PimItem::idFullColumnName(), CollectionPimItemRelation::rightFullColumnName()); + qb.addColumn(Collection::idFullColumnName()); + qb.addColumns(QStringList() << PimItem::idFullColumnName() << PimItem::remoteIdFullColumnName() << PimItem::remoteRevisionFullColumnName() + << PimItem::mimeTypeIdFullColumnName()); + qb.addSortColumn(Collection::idFullColumnName(), Query::Ascending); + + if (items.count() == 1) { + qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::Equals, items.first().id()); + } else { + QVariantList ids; + ids.reserve(items.count()); + for (const PimItem &item : items) { + ids << item.id(); + } + qb.addValueCondition(CollectionPimItemRelation::rightFullColumnName(), Query::In, ids); + } + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to query virtual Collections which PimItems" << items << "belong into"; + return QMap>(); + } + + QSqlQuery query = qb.query(); + QMap> map; + query.next(); + while (query.isValid()) { + const qlonglong collectionId = query.value(0).toLongLong(); + QList &pimItems = map[collectionId]; + do { + PimItem item; + item.setId(query.value(1).toLongLong()); + item.setRemoteId(query.value(2).toString()); + item.setRemoteRevision(query.value(3).toString()); + item.setMimeTypeId(query.value(4).toLongLong()); + pimItems << item; + } while (query.next() && query.value(0).toLongLong() == collectionId); + } + query.finish(); + + return map; +} + +/* --- PimItem ------------------------------------------------------- */ +bool DataStore::appendPimItem(QVector &parts, + const QVector &flags, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem) +{ + pimItem.setMimeTypeId(mimetype.id()); + pimItem.setCollectionId(collection.id()); + if (dateTime.isValid()) { + pimItem.setDatetime(dateTime); + } + if (remote_id.isEmpty()) { + // from application + pimItem.setDirty(true); + } else { + // from resource + pimItem.setRemoteId(remote_id); + pimItem.setDirty(false); + } + pimItem.setRemoteRevision(remoteRevision); + pimItem.setGid(gid); + pimItem.setAtime(QDateTime::currentDateTimeUtc()); + + if (!pimItem.insert()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append new PimItem into Collection" << collection.name() << "(ID" << collection.id() << ")"; + return false; + } + + // insert every part + if (!parts.isEmpty()) { + // don't use foreach, the caller depends on knowing the part has changed, see the Append handler + for (QVector::iterator it = parts.begin(); it != parts.end(); ++it) { + (*it).setPimItemId(pimItem.id()); + if ((*it).datasize() < (*it).data().size()) { + (*it).setDatasize((*it).data().size()); + } + + // qCDebug(AKONADISERVER_LOG) << "Insert from DataStore::appendPimItem"; + if (!PartHelper::insert(&(*it))) { + qCWarning(AKONADISERVER_LOG) << "Failed to add part" << it->partType().name() << "to new PimItem" << pimItem.id(); + return false; + } + } + } + + bool seen = false; + for (const Flag &flag : flags) { + seen |= (flag.name() == QLatin1String(AKONADI_FLAG_SEEN) || flag.name() == QLatin1String(AKONADI_FLAG_IGNORED)); + if (!pimItem.addFlag(flag)) { + qCWarning(AKONADISERVER_LOG) << "Failed to add flag" << flag.name() << "to new PimItem" << pimItem.id(); + return false; + } + } + + // qCDebug(AKONADISERVER_LOG) << "appendPimItem: " << pimItem; + + notificationCollector()->itemAdded(pimItem, seen, collection); + return true; +} + +bool DataStore::unhidePimItem(PimItem &pimItem) +{ + if (!m_dbOpened) { + return false; + } + + qCDebug(AKONADISERVER_LOG) << "DataStore::unhidePimItem(" << pimItem << ")"; + + // FIXME: This is inefficient. Using a bit on the PimItemTable record would probably be some orders of magnitude faster... + return removeItemParts(pimItem, {AKONADI_ATTRIBUTE_HIDDEN}); +} + +bool DataStore::unhideAllPimItems() +{ + if (!m_dbOpened) { + return false; + } + + qCDebug(AKONADISERVER_LOG) << "DataStore::unhideAllPimItems()"; + + try { + return PartHelper::remove(Part::partTypeIdFullColumnName(), PartTypeHelper::fromFqName(QStringLiteral("ATR"), QStringLiteral("HIDDEN")).id()); + } catch (...) { + } // we can live with this failing + + return false; +} + +bool DataStore::cleanupPimItems(const PimItem::List &items, bool silent) +{ + // generate relation removed notifications + if (!silent) { + for (const PimItem &item : items) { + SelectQueryBuilder relationQuery; + relationQuery.addValueCondition(Relation::leftIdFullColumnName(), Query::Equals, item.id()); + relationQuery.addValueCondition(Relation::rightIdFullColumnName(), Query::Equals, item.id()); + relationQuery.setSubQueryMode(Query::Or); + + if (!relationQuery.exec()) { + throw HandlerException("Failed to obtain relations"); + } + const Relation::List relations = relationQuery.result(); + for (const Relation &relation : relations) { + notificationCollector()->relationRemoved(relation); + } + } + + // generate the notification before actually removing the data + notificationCollector()->itemsRemoved(items); + } + + // FIXME: Create a single query to do this + for (const auto &item : items) { + if (!item.clearFlags()) { + qCWarning(AKONADISERVER_LOG) << "Failed to clean up flags from PimItem" << item.id(); + return false; + } + if (!PartHelper::remove(Part::pimItemIdColumn(), item.id())) { + qCWarning(AKONADISERVER_LOG) << "Failed to clean up parts from PimItem" << item.id(); + return false; + } + if (!PimItem::remove(PimItem::idColumn(), item.id())) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id(); + return false; + } + + if (!Entity::clearRelation(item.id(), Entity::Right)) { + qCWarning(AKONADISERVER_LOG) << "Failed to remove PimItem" << item.id() << "from linked collections"; + return false; + } + } + + return true; +} + +bool DataStore::addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent) +{ + SelectQueryBuilder qb; + qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); + qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() + << "): Failed to query existing attribute"; + return false; + } + + if (!qb.result().isEmpty()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() + << "): Attribute already exists"; + return false; + } + + CollectionAttribute attr; + attr.setCollectionId(col.id()); + attr.setType(key); + attr.setValue(value); + + if (!attr.insert()) { + qCWarning(AKONADISERVER_LOG) << "Failed to append attribute" << key << "to Collection" << col.name() << "(ID" << col.id() << ")"; + return false; + } + + if (!silent) { + notificationCollector()->collectionChanged(col, QList() << key); + } + return true; +} + +bool DataStore::removeCollectionAttribute(const Collection &col, const QByteArray &key) +{ + SelectQueryBuilder qb; + qb.addValueCondition(CollectionAttribute::collectionIdColumn(), Query::Equals, col.id()); + qb.addValueCondition(CollectionAttribute::typeColumn(), Query::Equals, key); + if (!qb.exec()) { + throw HandlerException("Unable to query for collection attribute"); + } + + const QVector result = qb.result(); + for (CollectionAttribute attr : result) { + if (!attr.remove()) { + throw HandlerException("Unable to remove collection attribute"); + } + } + + if (!result.isEmpty()) { + notificationCollector()->collectionChanged(col, QList() << key); + return true; + } + return false; +} + +void DataStore::debugLastDbError(const char *actionDescription) const +{ + qCCritical(AKONADISERVER_LOG) << "Database error:" << actionDescription; + qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText(); + qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText(); + + m_akonadi.tracer().error("DataStore (Database Error)", + QStringLiteral("%1\nDriver said: %2\nDatabase said:%3") + .arg(QString::fromLatin1(actionDescription), m_database.lastError().driverText(), m_database.lastError().databaseText())); +} + +void DataStore::debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const +{ + qCCritical(AKONADISERVER_LOG) << "Query error:" << actionDescription; + qCCritical(AKONADISERVER_LOG) << " Last error message:" << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << " Last driver error:" << m_database.lastError().driverText(); + qCCritical(AKONADISERVER_LOG) << " Last database error:" << m_database.lastError().databaseText(); + + m_akonadi.tracer().error("DataStore (Database Query Error)", + QStringLiteral("%1: %2").arg(QString::fromLatin1(actionDescription), query.lastError().text())); +} + +// static +QString DataStore::dateTimeFromQDateTime(const QDateTime &dateTime) +{ + QDateTime utcDateTime = dateTime; + if (utcDateTime.timeSpec() != Qt::UTC) { + utcDateTime = utcDateTime.toUTC(); + } + return utcDateTime.toString(QStringLiteral("yyyy-MM-dd hh:mm:ss")); +} + +// static +QDateTime DataStore::dateTimeToQDateTime(const QByteArray &dateTime) +{ + return QDateTime::fromString(QString::fromLatin1(dateTime), QStringLiteral("yyyy-MM-dd hh:mm:ss")); +} + +bool DataStore::doRollback() +{ + QSqlDriver *driver = m_database.driver(); + QElapsedTimer timer; + timer.start(); + driver->rollbackTransaction(); + StorageDebugger::instance()->removeTransaction(reinterpret_cast(this), false, timer.elapsed(), m_database.lastError().text()); + if (m_database.lastError().isValid()) { + TRANSACTION_MUTEX_UNLOCK; + debugLastDbError("DataStore::rollbackTransaction"); + return false; + } + TRANSACTION_MUTEX_UNLOCK; + return true; +} + +void DataStore::transactionKilledByDB() +{ + m_transactionKilledByDB = true; + cleanupAfterRollback(); + Q_EMIT transactionRolledBack(); +} + +bool DataStore::beginTransaction(const QString &name) +{ + if (!m_dbOpened) { + return false; + } + + if (m_transactionLevel == 0 || m_transactionKilledByDB) { + m_transactionKilledByDB = false; + QElapsedTimer timer; + timer.start(); + TRANSACTION_MUTEX_LOCK; + if (DbType::type(m_database) == DbType::Sqlite) { + m_database.exec(QStringLiteral("BEGIN IMMEDIATE TRANSACTION")); + StorageDebugger::instance()->addTransaction(reinterpret_cast(this), name, timer.elapsed(), m_database.lastError().text()); + if (m_database.lastError().isValid()) { + debugLastDbError("DataStore::beginTransaction (SQLITE)"); + TRANSACTION_MUTEX_UNLOCK; + return false; + } + } else { + m_database.driver()->beginTransaction(); + StorageDebugger::instance()->addTransaction(reinterpret_cast(this), name, timer.elapsed(), m_database.lastError().text()); + if (m_database.lastError().isValid()) { + debugLastDbError("DataStore::beginTransaction"); + TRANSACTION_MUTEX_UNLOCK; + return false; + } + } + + if (DbType::type(m_database) == DbType::PostgreSQL) { + // Make constraints check deferred in PostgreSQL. Allows for + // INSERT INTO mimetypetable (name) VALUES ('foo') RETURNING id; + // INSERT INTO collectionmimetyperelation (collection_id, mimetype_id) VALUES (x, y) + // where "y" refers to the newly inserted mimetype + m_database.exec(QStringLiteral("SET CONSTRAINTS ALL DEFERRED")); + } + } + + ++m_transactionLevel; + + return true; +} + +bool DataStore::rollbackTransaction() +{ + if (!m_dbOpened) { + return false; + } + + if (m_transactionLevel == 0) { + qCWarning(AKONADISERVER_LOG) << "DataStore::rollbackTransaction(): No transaction in progress!"; + return false; + } + + --m_transactionLevel; + + if (m_transactionLevel == 0 && !m_transactionKilledByDB) { + doRollback(); + cleanupAfterRollback(); + Q_EMIT transactionRolledBack(); + } + + return true; +} + +bool DataStore::commitTransaction() +{ + if (!m_dbOpened) { + return false; + } + + if (m_transactionLevel == 0) { + qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): No transaction in progress!"; + return false; + } + + if (m_transactionLevel == 1) { + if (m_transactionKilledByDB) { + qCWarning(AKONADISERVER_LOG) << "DataStore::commitTransaction(): Cannot commit, transaction was killed by mysql deadlock handling!"; + return false; + } + QSqlDriver *driver = m_database.driver(); + QElapsedTimer timer; + timer.start(); + driver->commitTransaction(); + StorageDebugger::instance()->removeTransaction(reinterpret_cast(this), true, timer.elapsed(), m_database.lastError().text()); + if (m_database.lastError().isValid()) { + debugLastDbError("DataStore::commitTransaction"); + rollbackTransaction(); + return false; + } else { + TRANSACTION_MUTEX_UNLOCK; + m_transactionLevel--; + Q_EMIT transactionCommitted(); + } + } else { + m_transactionLevel--; + } + return true; +} + +bool DataStore::inTransaction() const +{ + return m_transactionLevel > 0; +} + +void DataStore::sendKeepAliveQuery() +{ + if (m_database.isOpen()) { + QSqlQuery query(m_database); + query.exec(QStringLiteral("SELECT 1")); + } +} + +void DataStore::cleanupAfterRollback() +{ + MimeType::invalidateCompleteCache(); + Flag::invalidateCompleteCache(); + Resource::invalidateCompleteCache(); + Collection::invalidateCompleteCache(); + PartType::invalidateCompleteCache(); + m_akonadi.collectionStatistics().expireCache(); + QueryCache::clear(); +} diff --git a/src/server/storage/datastore.h b/src/server/storage/datastore.h new file mode 100644 index 0000000..1a33905 --- /dev/null +++ b/src/server/storage/datastore.h @@ -0,0 +1,371 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Andreas Gungl * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class QSqlQuery; +class QTimer; + +#include "entities.h" +#include "notificationcollector.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +class DataStore; +class DataStoreFactory +{ +public: + virtual ~DataStoreFactory() = default; + virtual DataStore *createStore() = 0; + +protected: + explicit DataStoreFactory() = default; + +private: + Q_DISABLE_COPY_MOVE(DataStoreFactory) +}; + +class NotificationCollector; + +/** + This class handles all the database access. + +

Database configuration

+ + You can select between various database backends during runtime using the + @c $HOME/.config/akonadi/akonadiserverrc configuration file. + + Example: +@verbatim +[%General] +Driver=QMYSQL + +[QMYSQL_EMBEDDED] +Name=akonadi +Options=SERVER_DATADIR=/home/foo/.local/share/akonadi/db_data + +[QMYSQL] +Name=akonadi +Host=localhost +User=foo +Password=***** +#Options=UNIX_SOCKET=/home/foo/.local/share/akonadi/socket-bar/mysql.socket +StartServer=true +ServerPath=/usr/sbin/mysqld + +[QSQLITE] +Name=/home/foo/.local/share/akonadi/akonadi.db +@endverbatim + + Use @c General/Driver to select the QSql driver to use for database + access. The following drivers are currently supported, other might work + but are untested: + + - QMYSQL + - QMYSQL_EMBEDDED + - QSQLITE + + The options for each driver are read from the corresponding group. + The following options are supported, dependent on the driver not all of them + might have an effect: + + - Name: Database name, for sqlite that's the file name of the database. + - Host: Hostname of the database server + - User: Username for the database server + - Password: Password for the database server + - Options: Additional options, format is driver-dependent + - StartServer: Start the database locally just for Akonadi instead of using an existing one + - ServerPath: Path to the server executable +*/ +class DataStore : public QObject +{ + Q_OBJECT +public: + const constexpr static bool Silent = true; + + static void setFactory(std::unique_ptr factory); + + /** + Closes the database connection and destroys the DataStore object. + */ + ~DataStore() override; + + /** + Opens the database connection. + */ + virtual void open(); + + /** + Closes the database connection. + */ + void close(); + + /** + Initializes the database. Should be called during startup by the main thread. + */ + virtual bool init(); + + /** + Per thread singleton. + */ + static DataStore *self(); + + /** + * Returns whether per thread DataStore has been created. + */ + static bool hasDataStore(); + + /* --- ItemFlags ----------------------------------------------------- */ + virtual bool setItemsFlags(const PimItem::List &items, + const QVector *currentFlags, + const QVector &newFlags, + bool *flagsChanged = nullptr, + const Collection &col = Collection(), + bool silent = false); + virtual bool appendItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *flagsChanged = nullptr, + bool checkIfExists = true, + const Collection &col = Collection(), + bool silent = false); + virtual bool removeItemsFlags(const PimItem::List &items, + const QVector &flags, + bool *tagsChanged = nullptr, + const Collection &collection = Collection(), + bool silent = false); + + /* --- ItemTags ----------------------------------------------------- */ + virtual bool setItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool silent = false); + virtual bool appendItemsTags(const PimItem::List &items, + const Tag::List &tags, + bool *tagsChanged = nullptr, + bool checkIfExists = true, + const Collection &col = Collection(), + bool silent = false); + virtual bool removeItemsTags(const PimItem::List &items, const Tag::List &tags, bool *tagsChanged = nullptr, bool silent = false); + virtual bool removeTags(const Tag::List &tags, bool silent = false); + + /* --- ItemParts ----------------------------------------------------- */ + virtual bool removeItemParts(const PimItem &item, const QSet &parts); + + // removes all payload parts for this item. + virtual bool invalidateItemCache(const PimItem &item); + + /* --- Collection ------------------------------------------------------ */ + virtual bool appendCollection(Collection &collection, const QStringList &mimeTypes, const QMap &attributes); + + /// removes the given collection and all its content + virtual bool cleanupCollection(Collection &collection); + /// same as the above but for database backends without working referential actions on foreign keys + virtual bool cleanupCollection_slow(Collection &collection); + + /// moves the collection @p collection to @p newParent. + virtual bool moveCollection(Collection &collection, const Collection &newParent); + + virtual bool appendMimeTypeForCollection(qint64 collectionId, const QStringList &mimeTypes); + + static QString collectionDelimiter() + { + return QStringLiteral("/"); + } + + /** + Determines the active cache policy for this Collection. + The active cache policy is set in the corresponding Collection fields. + */ + virtual void activeCachePolicy(Collection &col); + + /// Returns all virtual collections the @p item is linked to + QVector virtualCollections(const PimItem &item); + + QMap> virtualCollections(const Akonadi::Server::PimItem::List &items); + + /* --- PimItem ------------------------------------------------------- */ + virtual bool appendPimItem(QVector &parts, + const QVector &flags, + const MimeType &mimetype, + const Collection &collection, + const QDateTime &dateTime, + const QString &remote_id, + const QString &remoteRevision, + const QString &gid, + PimItem &pimItem); + /** + * Removes the pim item and all referenced data ( e.g. flags ) + */ + virtual bool cleanupPimItems(const PimItem::List &items, bool silent = false); + + /** + * Unhides the specified PimItem. Emits the itemAdded() notification as + * the hidden flag is assumed to have been set by appendPimItem() before + * pushing the item to the preprocessor chain. The hidden item had his + * notifications disabled until now (so for the clients the "unhide" operation + * is actually a new item arrival). + * + * This function does NOT verify if the item was *really* hidden: this is + * responsibility of the caller. + */ + virtual bool unhidePimItem(PimItem &pimItem); + + /** + * Unhides all the items which have the "hidden" flag set. + * This function doesn't emit any notification about the items + * being unhidden so it's meant to be called only in rare circumstances. + * The most notable call to this function is at server startup + * when we attempt to restore a clean state of the database. + */ + virtual bool unhideAllPimItems(); + + /* --- Collection attributes ------------------------------------------ */ + virtual bool addCollectionAttribute(const Collection &col, const QByteArray &key, const QByteArray &value, bool silent = false); + /** + * Removes the given collection attribute for @p col. + * @throws HandlerException on database errors + * @returns @c true if the attribute existed, @c false otherwise + */ + virtual bool removeCollectionAttribute(const Collection &col, const QByteArray &key); + + /* --- Helper functions ---------------------------------------------- */ + + /** + Begins a transaction. No changes will be written to the database and + no notification signal will be emitted unless you call commitTransaction(). + @return @c true if successful. + */ + virtual bool beginTransaction(const QString &name); + + /** + Reverts all changes within the current transaction. + */ + virtual bool rollbackTransaction(); + + /** + Commits all changes within the current transaction and emits all + collected notfication signals. If committing fails, the transaction + will be rolled back. + */ + virtual bool commitTransaction(); + + /** + Returns true if there is a transaction in progress. + */ + bool inTransaction() const; + + /** + Returns the notification collector of this DataStore object. + Use this to listen to change notification signals. + */ + NotificationCollector *notificationCollector(); + + /** + Returns the QSqlDatabase object. Use this for generating queries yourself. + + Will [re-]open the database, if it is closed. + */ + QSqlDatabase database(); + + /** + Sets the current session id. + */ + void setSessionId(const QByteArray &sessionId) + { + mSessionId = sessionId; + } + + /** + Returns if the database is currently open + */ + bool isOpened() const + { + return m_dbOpened; + } + + bool doRollback(); + void transactionKilledByDB(); + +Q_SIGNALS: + /** + Emitted if a transaction has been successfully committed. + */ + void transactionCommitted(); + /** + Emitted if a transaction has been aborted. + */ + void transactionRolledBack(); + +protected: + /** + Creates a new DataStore object and opens it. + */ + DataStore(AkonadiServer &akonadi); + + void debugLastDbError(const char *actionDescription) const; + void debugLastQueryError(const QSqlQuery &query, const char *actionDescription) const; + +private: + bool doAppendItemsFlag(const PimItem::List &items, const Flag &flag, const QSet &existing, const Collection &col, bool silent); + + bool doAppendItemsTag(const PimItem::List &items, const Tag &tag, const QSet &existing, const Collection &col, bool silent); + + /** Converts the given date/time to the database format, i.e. + "YYYY-MM-DD HH:MM:SS". + @param dateTime the date/time in UTC + @return the date/time in database format + @see dateTimeToQDateTime + */ + static QString dateTimeFromQDateTime(const QDateTime &dateTime); + + /** Converts the given date/time from database format to QDateTime. + @param dateTime the date/time in database format + @return the date/time as QDateTime + @see dateTimeFromQDateTime + */ + static QDateTime dateTimeToQDateTime(const QByteArray &dateTime); + +private Q_SLOTS: + void sendKeepAliveQuery(); + +protected: + static std::unique_ptr sFactory; + std::unique_ptr mNotificationCollector; + AkonadiServer &m_akonadi; + +private: + Q_DISABLE_COPY_MOVE(DataStore) + + void cleanupAfterRollback(); + QString m_connectionName; + QSqlDatabase m_database; + bool m_dbOpened; + bool m_transactionKilledByDB = false; + uint m_transactionLevel; + struct TransactionQuery { + QString query; + QVector boundValues; + bool isBatch; + }; + QByteArray mSessionId; + QTimer *m_keepAliveTimer = nullptr; + static bool s_hasForeignKeyConstraints; + static QMutex sTransactionMutex; + + friend class DataStoreFactory; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbconfig.cpp b/src/server/storage/dbconfig.cpp new file mode 100644 index 0000000..32f00e6 --- /dev/null +++ b/src/server/storage/dbconfig.cpp @@ -0,0 +1,151 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbconfig.h" + +#include "akonadiserver_debug.h" +#include "dbconfigmysql.h" +#include "dbconfigpostgresql.h" +#include "dbconfigsqlite.h" + +#include + +#include +#include + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +// TODO: make me Q_GLOBAL_STATIC +static DbConfig *s_DbConfigInstance = nullptr; + +DbConfig::DbConfig() +{ + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + + mSizeThreshold = 4096; + const QVariant value = settings.value(QStringLiteral("General/SizeThreshold"), mSizeThreshold); + if (value.canConvert()) { + mSizeThreshold = value.value(); + if (mSizeThreshold < 0) { + mSizeThreshold = 0; + } + } else { + mSizeThreshold = 0; + } +} + +DbConfig::~DbConfig() +{ +} + +bool DbConfig::isConfigured() +{ + return s_DbConfigInstance; +} + +QString DbConfig::defaultAvailableDatabaseBackend(QSettings &settings) +{ + QString driverName = QStringLiteral(AKONADI_DATABASE_BACKEND); + + std::unique_ptr dbConfigFallbackTest; + if (driverName == QLatin1String("QMYSQL")) { + dbConfigFallbackTest = std::make_unique(); + } else if (driverName == QLatin1String("QPSQL")) { + dbConfigFallbackTest = std::make_unique(); + } + + if (dbConfigFallbackTest && !dbConfigFallbackTest->isAvailable(settings) && DbConfigSqlite(DbConfigSqlite::Custom).isAvailable(settings)) { + qCWarning(AKONADISERVER_LOG) << driverName << " requirements not available. Falling back to using QSQLITE3."; + driverName = QStringLiteral("QSQLITE3"); + } + + return driverName; +} + +DbConfig *DbConfig::configuredDatabase() +{ + if (!s_DbConfigInstance) { + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + QSettings settings(serverConfigFile, QSettings::IniFormat); + + // determine driver to use + QString driverName = settings.value(QStringLiteral("General/Driver")).toString(); + if (driverName.isEmpty()) { + driverName = defaultAvailableDatabaseBackend(settings); + + // when using the default, write it explicitly, in case the default changes later + settings.setValue(QStringLiteral("General/Driver"), driverName); + settings.sync(); + } + + if (driverName == QLatin1String("QMYSQL")) { + s_DbConfigInstance = new DbConfigMysql; + } else if (driverName == QLatin1String("QSQLITE")) { + s_DbConfigInstance = new DbConfigSqlite(DbConfigSqlite::Default); + } else if (driverName == QLatin1String("QSQLITE3")) { + s_DbConfigInstance = new DbConfigSqlite(DbConfigSqlite::Custom); + } else if (driverName == QLatin1String("QPSQL")) { + s_DbConfigInstance = new DbConfigPostgresql; + } else { + qCCritical(AKONADISERVER_LOG) << "Unknown database driver: " << driverName; + qCCritical(AKONADISERVER_LOG) << "Available drivers are: " << QSqlDatabase::drivers(); + return nullptr; + } + + if (!s_DbConfigInstance->init(settings)) { + delete s_DbConfigInstance; + s_DbConfigInstance = nullptr; + } + } + + return s_DbConfigInstance; +} + +bool DbConfig::startInternalServer() +{ + // do nothing + return true; +} + +void DbConfig::stopInternalServer() +{ + // do nothing +} + +void DbConfig::setup() +{ + // do nothing +} + +qint64 DbConfig::sizeThreshold() const +{ + return mSizeThreshold; +} + +QString DbConfig::defaultDatabaseName() +{ + if (!Instance::hasIdentifier()) { + return QStringLiteral("akonadi"); + } + // dash is not allowed in PSQL + return QLatin1String("akonadi_") % Instance::identifier().replace(QLatin1Char('-'), QLatin1Char('_')); +} + +void DbConfig::initSession(const QSqlDatabase &database) +{ + Q_UNUSED(database) +} + +int DbConfig::execute(const QString &cmd, const QStringList &args) const +{ + qCDebug(AKONADISERVER_LOG) << "Executing: " << cmd << args.join(QLatin1Char(' ')); + return QProcess::execute(cmd, args); +} diff --git a/src/server/storage/dbconfig.h b/src/server/storage/dbconfig.h new file mode 100644 index 0000000..dbe2581 --- /dev/null +++ b/src/server/storage/dbconfig.h @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace Akonadi +{ +namespace Server +{ +/** + * A base class that provides an unique access layer to configuration + * and initialization of different database backends. + */ +class DbConfig +{ +public: + virtual ~DbConfig(); + + /** + * Returns whether database have been configured. + */ + static bool isConfigured(); + + /** + * Returns the DbConfig instance for the database the user has + * configured. + */ + static DbConfig *configuredDatabase(); + + /** + * Returns the name of the used driver. + */ + virtual QString driverName() const = 0; + + /** + * Returns the database name. + */ + virtual QString databaseName() const = 0; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back if @p storeSettings is true. + */ + virtual bool init(QSettings &settings, bool storeSettings = true) = 0; + + /** + * This method checks if the requirements for this database connection are met + * in the system (i.e. QMYSQL driver is available, mysqld binary is found, etc.). + */ + virtual bool isAvailable(QSettings &settings) = 0; + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + virtual void apply(QSqlDatabase &database) = 0; + + /** + * Do session setup/initialization work on @p database. + * An example would be to run some SQL commands on every new session, + * typically stuff like setting encodings, transaction isolation levels, etc. + */ + virtual void initSession(const QSqlDatabase &database); + + /** + * Returns whether an internal server needs to be used. + */ + virtual bool useInternalServer() const = 0; + + /** + * This method is called to start an external server. + */ + virtual bool startInternalServer(); + + /** + * This method is called to stop the external server. + */ + virtual void stopInternalServer(); + + /** + * Payload data bigger than this value will be stored in separate files, instead of the database. Valid + * + * @return the size threshold in bytes, defaults to 4096. + */ + virtual qint64 sizeThreshold() const; + + /** + * This method is called to setup initial database settings after a connection is established. + */ + virtual void setup(); + +protected: + DbConfig(); + + /** + * Returns the suggested default database name, if none is specified in the configuration already. + * This includes instance namespaces, so usually this is not necessary to use in combination + * with internal databases (in process or using our own server instance). + */ + static QString defaultDatabaseName(); + + /* + * Returns the Database backend we should use by default. Usually it should be the same value + * configured as AKONADI_DATABASE_BACKEND at build time, but this method checks if that + * backend is really available and if it's not, it falls back to returning "QSQLITE3". + */ + static QString defaultAvailableDatabaseBackend(QSettings &settings); + + /** + * Calls QProcess::execute() and also prints the command and arguments via qCDebug() + */ + int execute(const QString &cmd, const QStringList &args) const; + +private: + Q_DISABLE_COPY(DbConfig) + + qint64 mSizeThreshold; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbconfigmysql.cpp b/src/server/storage/dbconfigmysql.cpp new file mode 100644 index 0000000..1a437ac --- /dev/null +++ b/src/server/storage/dbconfigmysql.cpp @@ -0,0 +1,636 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbconfigmysql.h" +#include "akonadiserver_debug.h" +#include "utils.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +#define MYSQL_MIN_MAJOR 5 +#define MYSQL_MIN_MINOR 1 + +#define MYSQL_VERSION_CHECK(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) + +static const QString s_mysqlSocketFileName = QStringLiteral("mysql.socket"); + +DbConfigMysql::DbConfigMysql() + : mInternalServer(true) + , mDatabaseProcess(nullptr) +{ +} + +QString DbConfigMysql::driverName() const +{ + return QStringLiteral("QMYSQL"); +} + +QString DbConfigMysql::databaseName() const +{ + return mDatabaseName; +} + +static QString findExecutable(const QString &bin) +{ + static const QStringList mysqldSearchPath = { +#ifdef MYSQLD_SCRIPTS_PATH + QStringLiteral(MYSQLD_SCRIPTS_PATH), +#endif + QStringLiteral("/usr/bin"), + QStringLiteral("/usr/sbin"), + QStringLiteral("/usr/local/sbin"), + QStringLiteral("/usr/local/libexec"), + QStringLiteral("/usr/libexec"), + QStringLiteral("/opt/mysql/libexec"), + QStringLiteral("/opt/local/lib/mysql5/bin"), + QStringLiteral("/opt/mysql/sbin"), + }; + QString path = QStandardPaths::findExecutable(bin); + if (path.isEmpty()) { // No results in PATH; fall back to hardcoded list. + path = QStandardPaths::findExecutable(bin, mysqldSearchPath); + } + return path; +} + +bool DbConfigMysql::init(QSettings &settings, bool storeSettings) +{ + // determine default settings depending on the driver + QString defaultHostName; + QString defaultOptions; + QString defaultServerPath; + QString defaultCleanShutdownCommand; + +#ifndef Q_OS_WIN + const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); +#endif + + const bool defaultInternalServer = true; +#ifdef MYSQLD_EXECUTABLE + if (QFile::exists(QStringLiteral(MYSQLD_EXECUTABLE))) { + defaultServerPath = QStringLiteral(MYSQLD_EXECUTABLE); + } +#endif + if (defaultServerPath.isEmpty()) { + defaultServerPath = findExecutable(QStringLiteral("mysqld")); + } + + const QString mysqladminPath = findExecutable(QStringLiteral("mysqladmin")); + if (!mysqladminPath.isEmpty()) { +#ifndef Q_OS_WIN + defaultCleanShutdownCommand = QStringLiteral("%1 --defaults-file=%2/mysql.conf --socket=%3/%4 shutdown") + .arg(mysqladminPath, StandardDirs::saveDir("data"), socketDirectory, s_mysqlSocketFileName); +#else + defaultCleanShutdownCommand = QString::fromLatin1("%1 shutdown --shared-memory").arg(mysqladminPath); +#endif + } + + mMysqlInstallDbPath = findExecutable(QStringLiteral("mysql_install_db")); + qCDebug(AKONADISERVER_LOG) << "Found mysql_install_db: " << mMysqlInstallDbPath; + + mMysqlCheckPath = findExecutable(QStringLiteral("mysqlcheck")); + qCDebug(AKONADISERVER_LOG) << "Found mysqlcheck: " << mMysqlCheckPath; + + mInternalServer = settings.value(QStringLiteral("QMYSQL/StartServer"), defaultInternalServer).toBool(); +#ifndef Q_OS_WIN + if (mInternalServer) { + defaultOptions = QStringLiteral("UNIX_SOCKET=%1/%2").arg(socketDirectory, s_mysqlSocketFileName); + } +#endif + + // read settings for current driver + settings.beginGroup(driverName()); + mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString(); + mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString(); + mUserName = settings.value(QStringLiteral("User")).toString(); + mPassword = settings.value(QStringLiteral("Password")).toString(); + mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString(); + mMysqldPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString(); + mCleanServerShutdownCommand = settings.value(QStringLiteral("CleanServerShutdownCommand"), defaultCleanShutdownCommand).toString(); + settings.endGroup(); + + // verify settings and apply permanent changes (written out below) + if (mInternalServer) { + mConnectionOptions = defaultOptions; + // intentionally not namespaced as we are the only one in this db instance when using internal mode + mDatabaseName = QStringLiteral("akonadi"); + } + if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) { + mMysqldPath = defaultServerPath; + } + + qCDebug(AKONADISERVER_LOG) << "Using mysqld:" << mMysqldPath; + + if (storeSettings) { + // store back the default values + settings.beginGroup(driverName()); + settings.setValue(QStringLiteral("Name"), mDatabaseName); + settings.setValue(QStringLiteral("Host"), mHostName); + settings.setValue(QStringLiteral("Options"), mConnectionOptions); + if (!mMysqldPath.isEmpty()) { + settings.setValue(QStringLiteral("ServerPath"), mMysqldPath); + } + settings.setValue(QStringLiteral("StartServer"), mInternalServer); + settings.endGroup(); + settings.sync(); + } + + // apply temporary changes to the settings + if (mInternalServer) { + mHostName.clear(); + mUserName.clear(); + mPassword.clear(); + } + + return true; +} + +bool DbConfigMysql::isAvailable(QSettings &settings) +{ + if (!QSqlDatabase::drivers().contains(driverName())) { + return false; + } + + if (!init(settings, false)) { + return false; + } + + if (mInternalServer && (mMysqldPath.isEmpty() || !QFile::exists(mMysqldPath))) { + return false; + } + + return true; +} + +void DbConfigMysql::apply(QSqlDatabase &database) +{ + if (!mDatabaseName.isEmpty()) { + database.setDatabaseName(mDatabaseName); + } + if (!mHostName.isEmpty()) { + database.setHostName(mHostName); + } + if (!mUserName.isEmpty()) { + database.setUserName(mUserName); + } + if (!mPassword.isEmpty()) { + database.setPassword(mPassword); + } + + database.setConnectOptions(mConnectionOptions); + + // can we check that during init() already? + Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); +} + +bool DbConfigMysql::useInternalServer() const +{ + return mInternalServer; +} + +bool DbConfigMysql::startInternalServer() +{ + bool success = true; + + const QString akDir = StandardDirs::saveDir("data"); + const QString dataDir = StandardDirs::saveDir("data", QStringLiteral("db_data")); +#ifndef Q_OS_WIN + const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); + const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName); + const QString pidFileName = QStringLiteral("%1/mysql.pid").arg(socketDirectory); +#endif + + // generate config file + const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); + const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf")); + const QString actualConfig = StandardDirs::saveDir("data") + QLatin1String("/mysql.conf"); + if (globalConfig.isEmpty()) { + qCCritical(AKONADISERVER_LOG) << "Did not find MySQL server default configuration (mysql-global.conf)"; + return false; + } + +#ifdef Q_OS_LINUX + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. Disabling CoW only has effect on empty directory (since + // it affects only new files), so we check whether MySQL has not yet been initialized. + QDir dir(dataDir + QDir::separator() + QLatin1String("mysql")); + if (!dir.exists()) { + if (Utils::getDirectoryFileSystem(dataDir) == QLatin1String("btrfs")) { + Utils::disableCoW(dataDir); + } + } +#endif + + if (mMysqldPath.isEmpty()) { + qCCritical(AKONADISERVER_LOG) << "mysqld not found. Please verify your installation"; + return false; + } + + // Get the version of the mysqld server that we'll be using. + // MySQL (but not MariaDB) deprecates and removes command line options in + // patch version releases, so we need to adjust the command line options accordingly + // when running the helper utilities or starting the server + const unsigned int localVersion = parseCommandLineToolsVersion(); + if (localVersion == 0x000000) { + qCCritical(AKONADISERVER_LOG) << "Failed to detect mysqld version!"; + } + // TODO: Parse "MariaDB" or "MySQL" from the version string instead of relying + // on the version numbers + const bool isMariaDB = localVersion >= MYSQL_VERSION_CHECK(10, 0, 0); + qCDebug(AKONADISERVER_LOG).nospace() << "mysqld reports version " << (localVersion >> 16) << "." << ((localVersion >> 8) & 0x0000FF) << "." + << (localVersion & 0x0000FF) << " (" << (isMariaDB ? "MariaDB" : "Oracle MySQL") << ")"; + + bool confUpdate = false; + QFile actualFile(actualConfig); + // update conf only if either global (or local) is newer than actual + if ((QFileInfo(globalConfig).lastModified() > QFileInfo(actualFile).lastModified()) + || (QFileInfo(localConfig).lastModified() > QFileInfo(actualFile).lastModified())) { + QFile globalFile(globalConfig); + QFile localFile(localConfig); + if (globalFile.open(QFile::ReadOnly) && actualFile.open(QFile::WriteOnly)) { + actualFile.write(globalFile.readAll()); + if (!localConfig.isEmpty()) { + if (localFile.open(QFile::ReadOnly)) { + actualFile.write(localFile.readAll()); + localFile.close(); + } + } + globalFile.close(); + actualFile.close(); + confUpdate = true; + } else { + qCCritical(AKONADISERVER_LOG) << "Unable to create MySQL server configuration file."; + qCCritical(AKONADISERVER_LOG) << "This means that either the default configuration file (mysql-global.conf) was not readable"; + qCCritical(AKONADISERVER_LOG) << "or the target file (mysql.conf) could not be written."; + return false; + } + } + + // MySQL doesn't like world writeable config files (which makes sense), but + // our config file somehow ends up being world-writable on some systems for no + // apparent reason nevertheless, so fix that + const QFile::Permissions allowedPerms = + actualFile.permissions() & (QFile::ReadOwner | QFile::WriteOwner | QFile::ReadGroup | QFile::WriteGroup | QFile::ReadOther); + if (allowedPerms != actualFile.permissions()) { + actualFile.setPermissions(allowedPerms); + } + + if (dataDir.isEmpty()) { + qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database data directory"; + return false; + } + + if (akDir.isEmpty()) { + qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database log directory"; + return false; + } + +#ifndef Q_OS_WIN + if (socketDirectory.isEmpty()) { + qCCritical(AKONADISERVER_LOG) << "Akonadi server was not able to create database misc directory"; + return false; + } + + // the socket path must not exceed 103 characters, so check for max dir length right away + if (socketDirectory.length() >= 90) { + qCCritical(AKONADISERVER_LOG) << "MySQL cannot deal with a socket path this long. Path was: " << socketDirectory; + return false; + } + + // If mysql socket file exists, check if also the server process is still running, + // else we can safely remove the socket file (cleanup after a system crash, etc.) + QFile pidFile(pidFileName); + if (QFile::exists(socketFile) && pidFile.open(QIODevice::ReadOnly)) { + qCDebug(AKONADISERVER_LOG) << "Found a mysqld pid file, checking whether the server is still running..."; + QByteArray pid = pidFile.readLine().trimmed(); + QFile proc(QString::fromLatin1("/proc/" + pid + "/stat")); + // Check whether the process with the PID from pidfile still exists and whether + // it's actually still mysqld or, whether the PID has been recycled in the meanwhile. + bool serverIsRunning = false; + if (proc.open(QIODevice::ReadOnly)) { + const QByteArray stat = proc.readAll(); + const QList stats = stat.split(' '); + if (stats.count() > 1) { + // Make sure the PID actually belongs to mysql process + + // Linux trims executable name in /proc filesystem to 15 characters + const QString expectedProcName = QFileInfo(mMysqldPath).fileName().left(15); + if (QString::fromLatin1(stats[1]) == QStringLiteral("(%1)").arg(expectedProcName)) { + // Yup, our mysqld is actually running, so pretend we started the server + // and try to connect to it + qCWarning(AKONADISERVER_LOG) << "mysqld for Akonadi is already running, trying to connect to it."; + serverIsRunning = true; + } + } + proc.close(); + } + + if (!serverIsRunning) { + qCDebug(AKONADISERVER_LOG) << "No mysqld process with specified PID is running. Removing the pidfile and starting a new instance..."; + pidFile.close(); + pidFile.remove(); + QFile::remove(socketFile); + } + } +#endif + + // synthesize the mysqld command + QStringList arguments; + arguments << QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir); + arguments << QStringLiteral("--datadir=%1/").arg(dataDir); +#ifndef Q_OS_WIN + arguments << QStringLiteral("--socket=%1").arg(socketFile); + arguments << QStringLiteral("--pid-file=%1").arg(pidFileName); +#else + arguments << QString::fromLatin1("--shared-memory"); +#endif + +#ifndef Q_OS_WIN + // If mysql socket file does not exists, then we must start the server, + // otherwise we reconnect to it + if (!QFile::exists(socketFile)) { + // move mysql error log file out of the way + const QFileInfo errorLog(dataDir + QDir::separator() + QLatin1String("mysql.err")); + if (errorLog.exists()) { + QFile logFile(errorLog.absoluteFilePath()); + QFile oldLogFile(dataDir + QDir::separator() + QLatin1String("mysql.err.old")); + if (logFile.open(QFile::ReadOnly) && oldLogFile.open(QFile::Append)) { + oldLogFile.write(logFile.readAll()); + oldLogFile.close(); + logFile.close(); + logFile.remove(); + } else { + qCCritical(AKONADISERVER_LOG) << "Failed to open MySQL error log."; + } + } + + // first run, some MySQL versions need a mysql_install_db run for that + const QString confFile = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); + if (QDir(dataDir).entryList(QDir::NoDotAndDotDot | QDir::AllEntries).isEmpty()) { + if (isMariaDB) { + initializeMariaDBDatabase(confFile, dataDir); + } else if (localVersion >= MYSQL_VERSION_CHECK(5, 7, 6)) { + initializeMySQL5_7_6Database(confFile, dataDir); + } else { + initializeMySQLDatabase(confFile, dataDir); + } + } + + // clear mysql ib_logfile's in case innodb_log_file_size option changed in last confUpdate + if (confUpdate) { + QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile0")).remove(); + QFile(dataDir + QDir::separator() + QLatin1String("ib_logfile1")).remove(); + } + + qCDebug(AKONADISERVER_LOG) << "Executing:" << mMysqldPath << arguments.join(QLatin1Char(' ')); + mDatabaseProcess = new QProcess; + mDatabaseProcess->start(mMysqldPath, arguments); + if (!mDatabaseProcess->waitForStarted()) { + qCCritical(AKONADISERVER_LOG) << "Could not start database server!"; + qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath; + qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; + qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString(); + return false; + } + + connect(mDatabaseProcess, QOverload::of(&QProcess::finished), this, &DbConfigMysql::processFinished); + + // wait until mysqld has created the socket file (workaround for QTBUG-47475 in Qt5.5.0) + int counter = 50; // avoid an endless loop in case mysqld terminated + while ((counter-- > 0) && !QFileInfo::exists(socketFile)) { + QThread::msleep(100); + } + } else { + qCDebug(AKONADISERVER_LOG) << "Found " << qPrintable(s_mysqlSocketFileName) << " file, reconnecting to the database"; + } +#endif + + const QLatin1String initCon("initConnection"); + { + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QMYSQL"), initCon); + apply(db); + + db.setDatabaseName(QString()); // might not exist yet, then connecting to the actual db will fail + if (!db.isValid()) { + qCCritical(AKONADISERVER_LOG) << "Invalid database object during database server startup"; + return false; + } + + bool opened = false; + for (int i = 0; i < 120; ++i) { + opened = db.open(); + if (opened) { + break; + } + if (mDatabaseProcess && mDatabaseProcess->waitForFinished(500)) { + qCCritical(AKONADISERVER_LOG) << "Database process exited unexpectedly during initial connection!"; + qCCritical(AKONADISERVER_LOG) << "executable:" << mMysqldPath; + qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; + qCCritical(AKONADISERVER_LOG) << "stdout:" << mDatabaseProcess->readAllStandardOutput(); + qCCritical(AKONADISERVER_LOG) << "stderr:" << mDatabaseProcess->readAllStandardError(); + qCCritical(AKONADISERVER_LOG) << "exit code:" << mDatabaseProcess->exitCode(); + qCCritical(AKONADISERVER_LOG) << "process error:" << mDatabaseProcess->errorString(); + return false; + } + } + + if (opened) { + if (!mMysqlCheckPath.isEmpty()) { + execute(mMysqlCheckPath, + {QStringLiteral("--defaults-file=%1/mysql.conf").arg(akDir), + QStringLiteral("--check-upgrade"), + QStringLiteral("--auto-repair"), +#ifndef Q_OS_WIN + QStringLiteral("--socket=%1/%2").arg(socketDirectory, s_mysqlSocketFileName), +#endif + mDatabaseName}); + } + + // Verify MySQL version + { + QSqlQuery query(db); + if (!query.exec(QStringLiteral("SELECT VERSION()")) || !query.first()) { + qCCritical(AKONADISERVER_LOG) << "Failed to verify database server version"; + qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + return false; + } + + const QString version = query.value(0).toString(); + const QStringList versions = version.split(QLatin1Char('.'), Qt::SkipEmptyParts); + if (versions.count() < 3) { + qCCritical(AKONADISERVER_LOG) << "Invalid database server version: " << version; + return false; + } + + if (versions[0].toInt() < MYSQL_MIN_MAJOR || (versions[0].toInt() == MYSQL_MIN_MAJOR && versions[1].toInt() < MYSQL_MIN_MINOR)) { + qCCritical(AKONADISERVER_LOG) << "Unsupported MySQL version:"; + qCCritical(AKONADISERVER_LOG) << "Current version:" << QStringLiteral("%1.%2").arg(versions[0], versions[1]); + qCCritical(AKONADISERVER_LOG) << "Minimum required version:" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR); + qCCritical(AKONADISERVER_LOG) << "Please update your MySQL database server"; + return false; + } else { + qCDebug(AKONADISERVER_LOG) << "MySQL version OK" + << "(required" << QStringLiteral("%1.%2").arg(MYSQL_MIN_MAJOR).arg(MYSQL_MIN_MINOR) << ", available" + << QStringLiteral("%1.%2").arg(versions[0], versions[1]) << ")"; + } + } + + { + QSqlQuery query(db); + if (!query.exec(QStringLiteral("USE %1").arg(mDatabaseName))) { + qCDebug(AKONADISERVER_LOG) << "Failed to use database" << mDatabaseName; + qCDebug(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); + qCDebug(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + qCDebug(AKONADISERVER_LOG) << "Trying to create database now..."; + if (!query.exec(QStringLiteral("CREATE DATABASE akonadi"))) { + qCCritical(AKONADISERVER_LOG) << "Failed to create database"; + qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + success = false; + } + } + } // make sure query is destroyed before we close the db + db.close(); + } else { + qCCritical(AKONADISERVER_LOG) << "Failed to connect to database!"; + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + success = false; + } + } + + return success; +} + +void DbConfigMysql::processFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + Q_UNUSED(exitCode) + Q_UNUSED(exitStatus) + + qCCritical(AKONADISERVER_LOG) << "database server stopped unexpectedly"; + +#ifndef Q_OS_WIN + // when the server stopped unexpectedly, make sure to remove the stale socket file since otherwise + // it can not be started again + const QString socketDirectory = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc")), s_mysqlSocketFileName.length()); + const QString socketFile = QStringLiteral("%1/%2").arg(socketDirectory, s_mysqlSocketFileName); + QFile::remove(socketFile); +#endif + + QCoreApplication::quit(); +} + +void DbConfigMysql::stopInternalServer() +{ + if (!mDatabaseProcess) { + return; + } + + // closing initConnection this late to work around QTBUG-63108 + QSqlDatabase::removeDatabase(QStringLiteral("initConnection")); + + disconnect(mDatabaseProcess, static_cast(&QProcess::finished), this, &DbConfigMysql::processFinished); + + // first, try the nicest approach + if (!mCleanServerShutdownCommand.isEmpty()) { + QProcess::execute(mCleanServerShutdownCommand, QStringList()); + if (mDatabaseProcess->waitForFinished(3000)) { + return; + } + } + + mDatabaseProcess->terminate(); + const bool result = mDatabaseProcess->waitForFinished(3000); + // We've waited nicely for 3 seconds, to no avail, let's be rude. + if (!result) { + mDatabaseProcess->kill(); + } +} + +void DbConfigMysql::initSession(const QSqlDatabase &database) +{ + QSqlQuery query(database); + query.exec(QStringLiteral("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")); +} + +int DbConfigMysql::parseCommandLineToolsVersion() const +{ + QProcess mysqldProcess; + mysqldProcess.start(mMysqldPath, {QStringLiteral("--version")}); + mysqldProcess.waitForFinished(10000 /* 10 secs */); + + const QString out = QString::fromLocal8Bit(mysqldProcess.readAllStandardOutput()); + QRegularExpression regexp(QStringLiteral("Ver ([0-9]+)\\.([0-9]+)\\.([0-9]+)")); + auto match = regexp.match(out); + if (!match.hasMatch()) { + return 0; + } + + return (match.capturedRef(1).toInt() << 16) | (match.capturedRef(2).toInt() << 8) | match.capturedRef(3).toInt(); +} + +bool DbConfigMysql::initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const +{ + // KDE Neon (and possible others) don't ship mysql_install_db, but it seems + // that MariaDB can initialize itself automatically on first start, it only + // needs that the datadir directory exists + if (mMysqlInstallDbPath.isEmpty()) { + return QDir().mkpath(dataDir); + } + + QFileInfo fi(mMysqlInstallDbPath); + QDir dir = fi.dir(); + dir.cdUp(); + const QString baseDir = dir.absolutePath(); + return 0 + == execute(mMysqlInstallDbPath, + {QStringLiteral("--defaults-file=%1").arg(confFile), + QStringLiteral("--force"), + QStringLiteral("--basedir=%1").arg(baseDir), + QStringLiteral("--datadir=%1/").arg(dataDir)}); +} + +/** + * As of MySQL 5.7.6 mysql_install_db is deprecated and mysqld --initailize should be used instead + * See MySQL Reference Manual section 2.10.1.1 (Initializing the Data Directory Manually Using mysqld) + */ +bool DbConfigMysql::initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const +{ + return 0 + == execute(mMysqldPath, + {QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--initialize"), QStringLiteral("--datadir=%1/").arg(dataDir)}); +} + +bool DbConfigMysql::initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const +{ + // On FreeBSD MySQL 5.6 is also installed without mysql_install_db, so this + // might do the trick there as well. + if (mMysqlInstallDbPath.isEmpty()) { + return QDir().mkpath(dataDir); + } + + QFileInfo fi(mMysqlInstallDbPath); + QDir dir = fi.dir(); + dir.cdUp(); + const QString baseDir = dir.absolutePath(); + + // Don't use --force, it has been removed in MySQL 5.7.5 + return 0 + == execute( + mMysqlInstallDbPath, + {QStringLiteral("--defaults-file=%1").arg(confFile), QStringLiteral("--basedir=%1").arg(baseDir), QStringLiteral("--datadir=%1/").arg(dataDir)}); +} diff --git a/src/server/storage/dbconfigmysql.h b/src/server/storage/dbconfigmysql.h new file mode 100644 index 0000000..353236e --- /dev/null +++ b/src/server/storage/dbconfigmysql.h @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "dbconfig.h" +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class DbConfigMysql : public QObject, public DbConfig +{ + Q_OBJECT + +public: + DbConfigMysql(); + + /** + * Returns the name of the used driver. + */ + QString driverName() const override; + + /** + * Returns the database name. + */ + QString databaseName() const override; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back if @p storeSettings is true. + */ + bool init(QSettings &settings, bool storeSettings = true) override; + + /** + * This method checks if the requirements for this database connection are met + * in the system (QMYSQL driver is available, mysqld binary is found, etc.). + */ + bool isAvailable(QSettings &settings) override; + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + void apply(QSqlDatabase &database) override; + + /** + * Returns whether an internal server needs to be used. + */ + bool useInternalServer() const override; + + /** + * This method is called to start an external server. + */ + bool startInternalServer() override; + + /** + * This method is called to stop the external server. + */ + void stopInternalServer() override; + + /// reimpl + void initSession(const QSqlDatabase &database) override; + +private Q_SLOTS: + void processFinished(int exitCode, QProcess::ExitStatus exitStatus); + +private: + int parseCommandLineToolsVersion() const; + + bool initializeMariaDBDatabase(const QString &confFile, const QString &dataDir) const; + bool initializeMySQL5_7_6Database(const QString &confFile, const QString &dataDir) const; + bool initializeMySQLDatabase(const QString &confFile, const QString &dataDir) const; + + QString mDatabaseName; + QString mHostName; + QString mUserName; + QString mPassword; + QString mConnectionOptions; + QString mMysqldPath; + QString mCleanServerShutdownCommand; + QString mMysqlInstallDbPath; + QString mMysqlCheckPath; + bool mInternalServer; + QProcess *mDatabaseProcess = nullptr; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbconfigpostgresql.cpp b/src/server/storage/dbconfigpostgresql.cpp new file mode 100644 index 0000000..38364e3 --- /dev/null +++ b/src/server/storage/dbconfigpostgresql.cpp @@ -0,0 +1,647 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbconfigpostgresql.h" +#include "akonadiserver_debug.h" +#include "utils.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#ifdef HAVE_UNISTD_H +#include +#endif +#include + +using namespace std::chrono_literals; + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +DbConfigPostgresql::DbConfigPostgresql() + : mHostPort(0) + , mInternalServer(true) +{ +} + +QString DbConfigPostgresql::driverName() const +{ + return QStringLiteral("QPSQL"); +} + +QString DbConfigPostgresql::databaseName() const +{ + return mDatabaseName; +} + +namespace +{ +struct VersionCompare { + bool operator()(const QFileInfo &lhsFi, const QFileInfo &rhsFi) const + { + const auto lhs = parseVersion(lhsFi.fileName()); + if (!lhs.has_value()) { + return false; + } + const auto rhs = parseVersion(rhsFi.fileName()); + if (!rhs.has_value()) { + return true; + } + + return std::tie(lhs->major, lhs->minor) < std::tie(rhs->major, rhs->minor); + } + +private: + struct Version { + int major; + int minor; + }; + std::optional parseVersion(const QString &name) const + { + const auto dotIdx = name.indexOf(QLatin1Char('.')); + if (dotIdx == -1) { + return {}; + } + bool ok = false; +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + const auto major = QStringView(name).left(dotIdx).toInt(&ok); +#else + const auto major = name.leftRef(dotIdx).toInt(&ok); +#endif + if (!ok) { + return {}; + } +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 2) + const auto minor = QStringView(name).mid(dotIdx + 1).toInt(&ok); +#else + const auto minor = name.midRef(dotIdx + 1).toInt(&ok); +#endif + if (!ok) { + return {}; + } + return Version{major, minor}; + } +}; + +} // namespace + +QStringList DbConfigPostgresql::postgresSearchPaths(const QString &versionedPath) const +{ + QStringList paths; + +#ifdef POSTGRES_PATH + const QString dir(QStringLiteral(POSTGRES_PATH)); + if (QDir(dir).exists()) { + paths.push_back(QStringLiteral(POSTGRES_PATH)); + } +#endif + paths << QStringLiteral("/usr/bin") << QStringLiteral("/usr/sbin") << QStringLiteral("/usr/local/sbin"); + + // Locate all versions in /usr/lib/postgresql (i.e. /usr/lib/postgresql/X.Y) in reversed + // sorted order, so we search from the newest one to the oldest. + QDir versionedDir(versionedPath); + if (versionedDir.exists()) { + auto versionedDirs = versionedDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort); + qDebug() << versionedDirs; + std::sort(versionedDirs.begin(), versionedDirs.end(), VersionCompare()); + std::reverse(versionedDirs.begin(), versionedDirs.end()); + paths += versionedDirs | Views::transform([](const auto &dir) -> QString { + return dir.absoluteFilePath() + QStringLiteral("/bin"); + }) + | Actions::toQList; + } + + return paths; +} + +bool DbConfigPostgresql::init(QSettings &settings, bool storeSettings) +{ + // determine default settings depending on the driver + QString defaultHostName; + QString defaultOptions; + QString defaultServerPath; + QString defaultInitDbPath; + QString defaultPgUpgradePath; + QString defaultPgData; + +#ifndef Q_WS_WIN // We assume that PostgreSQL is running as service on Windows + const bool defaultInternalServer = true; +#else + const bool defaultInternalServer = false; +#endif + + mInternalServer = settings.value(QStringLiteral("QPSQL/StartServer"), defaultInternalServer).toBool(); + if (mInternalServer) { + const auto paths = postgresSearchPaths(QStringLiteral("/usr/lib/postgresql")); + + defaultServerPath = QStandardPaths::findExecutable(QStringLiteral("pg_ctl"), paths); + defaultInitDbPath = QStandardPaths::findExecutable(QStringLiteral("initdb"), paths); + defaultHostName = Utils::preferredSocketDirectory(StandardDirs::saveDir("data", QStringLiteral("db_misc"))); + defaultPgUpgradePath = QStandardPaths::findExecutable(QStringLiteral("pg_upgrade"), paths); + defaultPgData = StandardDirs::saveDir("data", QStringLiteral("db_data")); + } + + // read settings for current driver + settings.beginGroup(driverName()); + mDatabaseName = settings.value(QStringLiteral("Name"), defaultDatabaseName()).toString(); + if (mDatabaseName.isEmpty()) { + mDatabaseName = defaultDatabaseName(); + } + mHostName = settings.value(QStringLiteral("Host"), defaultHostName).toString(); + if (mHostName.isEmpty()) { + mHostName = defaultHostName; + } + mHostPort = settings.value(QStringLiteral("Port")).toInt(); + // User, password and Options can be empty and still valid, so don't override them + mUserName = settings.value(QStringLiteral("User")).toString(); + mPassword = settings.value(QStringLiteral("Password")).toString(); + mConnectionOptions = settings.value(QStringLiteral("Options"), defaultOptions).toString(); + mServerPath = settings.value(QStringLiteral("ServerPath"), defaultServerPath).toString(); + if (mInternalServer && mServerPath.isEmpty()) { + mServerPath = defaultServerPath; + } + qCDebug(AKONADISERVER_LOG) << "Found pg_ctl:" << mServerPath; + mInitDbPath = settings.value(QStringLiteral("InitDbPath"), defaultInitDbPath).toString(); + if (mInternalServer && mInitDbPath.isEmpty()) { + mInitDbPath = defaultInitDbPath; + } + qCDebug(AKONADISERVER_LOG) << "Found initdb:" << mServerPath; + mPgUpgradePath = settings.value(QStringLiteral("UpgradePath"), defaultPgUpgradePath).toString(); + if (mInternalServer && mPgUpgradePath.isEmpty()) { + mPgUpgradePath = defaultPgUpgradePath; + } + qCDebug(AKONADISERVER_LOG) << "Found pg_upgrade:" << mPgUpgradePath; + mPgData = settings.value(QStringLiteral("PgData"), defaultPgData).toString(); + if (mPgData.isEmpty()) { + mPgData = defaultPgData; + } + settings.endGroup(); + + if (storeSettings) { + // store back the default values + settings.beginGroup(driverName()); + settings.setValue(QStringLiteral("Name"), mDatabaseName); + settings.setValue(QStringLiteral("Host"), mHostName); + if (mHostPort) { + settings.setValue(QStringLiteral("Port"), mHostPort); + } + settings.setValue(QStringLiteral("Options"), mConnectionOptions); + settings.setValue(QStringLiteral("ServerPath"), mServerPath); + settings.setValue(QStringLiteral("InitDbPath"), mInitDbPath); + settings.setValue(QStringLiteral("StartServer"), mInternalServer); + settings.endGroup(); + settings.sync(); + } + + return true; +} + +bool DbConfigPostgresql::isAvailable(QSettings &settings) +{ + if (!QSqlDatabase::drivers().contains(driverName())) { + return false; + } + + if (!init(settings, false)) { + return false; + } + + if (mInternalServer) { + if (mInitDbPath.isEmpty() || !QFile::exists(mInitDbPath)) { + return false; + } + + if (mServerPath.isEmpty() || !QFile::exists(mServerPath)) { + return false; + } + } + + return true; +} + +void DbConfigPostgresql::apply(QSqlDatabase &database) +{ + if (!mDatabaseName.isEmpty()) { + database.setDatabaseName(mDatabaseName); + } + if (!mHostName.isEmpty()) { + database.setHostName(mHostName); + } + if (mHostPort > 0 && mHostPort < 65535) { + database.setPort(mHostPort); + } + if (!mUserName.isEmpty()) { + database.setUserName(mUserName); + } + if (!mPassword.isEmpty()) { + database.setPassword(mPassword); + } + + database.setConnectOptions(mConnectionOptions); + + // can we check that during init() already? + Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); +} + +bool DbConfigPostgresql::useInternalServer() const +{ + return mInternalServer; +} + +std::optional DbConfigPostgresql::checkPgVersion() const +{ + // Contains major version of Postgres that creted the cluster + QFile pgVersionFile(QStringLiteral("%1/PG_VERSION").arg(mPgData)); + if (!pgVersionFile.open(QIODevice::ReadOnly)) { + return std::nullopt; + } + const auto clusterVersion = pgVersionFile.readAll().toInt(); + + QProcess pgctl; + pgctl.start(mServerPath, {QStringLiteral("--version")}, QIODevice::ReadOnly); + if (!pgctl.waitForFinished()) { + return std::nullopt; + } + // Looks like "pg_ctl (PostgreSQL) 11.2" + const auto output = QString::fromUtf8(pgctl.readAll()); + + // Get the major version from major.minor + QRegularExpression re(QStringLiteral("\\(PostgreSQL\\) ([0-9]+).[0-9]+")); + const auto match = re.match(output); + if (!match.hasMatch()) { + return std::nullopt; + } + const auto serverVersion = match.captured(1).toInt(); + + qDebug(AKONADISERVER_LOG) << "Detected psql versions - cluster:" << clusterVersion << ", server:" << serverVersion; + return {{clusterVersion, serverVersion}}; +} + +bool DbConfigPostgresql::runInitDb(const QString &newDbPath) +{ + // Make sure the cluster directory exists + if (!QDir(newDbPath).exists()) { + if (!QDir().mkpath(newDbPath)) { + return false; + } + } + +#ifdef Q_OS_LINUX + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. This only has effect when done on empty directory, + // so we only call this before calling initdb + if (Utils::getDirectoryFileSystem(newDbPath) == QLatin1String("btrfs")) { + Utils::disableCoW(newDbPath); + } +#endif + + // call 'initdb --pgdata=/home/user/.local/share/akonadi/data_db' + return execute(mInitDbPath, {QStringLiteral("--pgdata=%1").arg(newDbPath), QStringLiteral("--encoding=UTF8"), QStringLiteral("--no-locale")}) == 0; +} + +namespace +{ +std::optional findBinPathForVersion(int version) +{ + // First we need to find where the previous PostgreSQL version binaries are available + const auto oldBinSearchPaths = { + QStringLiteral("/usr/lib64/pgsql/postgresql-%1/bin").arg(version), // Fedora & friends + QStringLiteral("/usr/lib/pgsql/postgresql-%1/bin").arg(version), + QStringLiteral("/usr/lib/postgresql/%1/bin").arg(version), // Debian-based + QStringLiteral("/opt/pgsql-%1/bin").arg(version), // Arch Linux + // TODO: Check other distros as well, they might do things differently. + }; + + for (const auto &path : oldBinSearchPaths) { + if (QDir(path).exists()) { + return path; + } + } + + return std::nullopt; +} + +bool checkAndRemoveTmpCluster(const QDir &baseDir, const QString &clusterName) +{ + if (baseDir.exists(clusterName)) { + qCInfo(AKONADISERVER_LOG) << "Postgres cluster update:" << clusterName << "cluster already exists, trying to remove it first"; + if (!QDir(baseDir.path() + QDir::separator() + clusterName).removeRecursively()) { + qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to remove" << clusterName + << "cluster from some previous run, not performing auto-upgrade"; + return false; + } + } + return true; +} + +bool runPgUpgrade(const QString &pgUpgrade, + const QDir &baseDir, + const QString &oldBinPath, + const QString &newBinPath, + const QString &oldDbData, + const QString &newDbData) +{ + QProcess process; + const QStringList args = {QString(QStringLiteral("--old-bindir=%1").arg(oldBinPath)), + QString(QStringLiteral("--new-bindir=%1").arg(newBinPath)), + QString(QStringLiteral("--old-datadir=%1").arg(oldDbData)), + QString(QStringLiteral("--new-datadir=%1").arg(newDbData))}; + qCInfo(AKONADISERVER_LOG) << "Postgres cluster update: starting pg_upgrade to upgrade your Akonadi DB cluster"; + qCDebug(AKONADISERVER_LOG) << "Executing pg_upgrade" << QStringList(args); + process.setWorkingDirectory(baseDir.path()); + process.start(pgUpgrade, args); + process.waitForFinished(std::chrono::milliseconds(1h).count()); + if (process.exitCode() != 0) { + qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: pg_upgrade finished with exit code" << process.exitCode() + << ", please run migration manually."; + return false; + } + + qCDebug(AKONADISERVER_LOG) << "Postgres cluster update: pg_upgrade finished successfully."; + return true; +} + +bool swapClusters(QDir &baseDir, const QString &oldDbDataCluster, const QString &newDbDataCluster) +{ + // If everything went fine, swap the old and new clusters + if (!baseDir.rename(QStringLiteral("db_data"), oldDbDataCluster)) { + qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to rename old db_data to" << oldDbDataCluster; + return false; + } + if (!baseDir.rename(newDbDataCluster, QStringLiteral("db_data"))) { + qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to rename" << newDbDataCluster << "to db_data, rolling back"; + if (!baseDir.rename(oldDbDataCluster, QStringLiteral("db_data"))) { + qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to roll back from" << oldDbDataCluster << "to db_data."; + return false; + } + qCDebug(AKONADISERVER_LOG) << "Postgres cluster update: rollback successful."; + return false; + } + + return true; +} + +} // namespace + +bool DbConfigPostgresql::upgradeCluster(int clusterVersion) +{ + const auto oldDbDataCluster = QStringLiteral("old_db_data"); + const auto newDbDataCluster = QStringLiteral("new_db_data"); + + QDir baseDir(mPgData); // db_data + baseDir.cdUp(); // move to its parent folder + + const auto oldBinPath = findBinPathForVersion(clusterVersion); + if (!oldBinPath.has_value()) { + qCDebug(AKONADISERVER_LOG) << "Postgres cluster update: failed to find Postgres server for version" << clusterVersion; + return false; + } + const auto newBinPath = QFileInfo(mServerPath).path(); + + if (!checkAndRemoveTmpCluster(baseDir, oldDbDataCluster)) { + return false; + } + if (!checkAndRemoveTmpCluster(baseDir, newDbDataCluster)) { + return false; + } + + // Next, initialize a new cluster + const QString newDbData = baseDir.path() + QDir::separator() + newDbDataCluster; + qCInfo(AKONADISERVER_LOG) << "Postgres cluster upgrade: creating a new cluster for current Postgres server"; + if (!runInitDb(newDbData)) { + qCWarning(AKONADISERVER_LOG) << "Postgres cluster update: failed to initialize new db cluster"; + return false; + } + + // Now migrate the old cluster from the old version into the new cluster + if (!runPgUpgrade(mPgUpgradePath, baseDir, *oldBinPath, newBinPath, mPgData, newDbData)) { + return false; + } + + if (!swapClusters(baseDir, oldDbDataCluster, newDbDataCluster)) { + return false; + } + + // Drop the old cluster + if (!QDir(baseDir.path() + QDir::separator() + oldDbDataCluster).removeRecursively()) { + qCInfo(AKONADISERVER_LOG) << "Postgres cluster update: failed to remove" << oldDbDataCluster << "cluster (not an issue, continuing)"; + } + + return true; +} + +bool DbConfigPostgresql::startInternalServer() +{ + // We defined the mHostName to the socket directory, during init + const QString socketDir = mHostName; + bool success = true; + + // Make sure the path exists, otherwise pg_ctl fails + if (!QFile::exists(socketDir)) { + QDir().mkpath(socketDir); + } + +// TODO Windows support +#ifndef Q_WS_WIN + // If postmaster.pid exists, check whether the postgres process still exists too, + // because normally we shouldn't be able to get this far if Akonadi is already + // running. If postgres is not running, then the pidfile was left after a system + // crash or something similar and we can remove it (otherwise pg_ctl won't start) + QFile postmaster(QStringLiteral("%1/postmaster.pid").arg(mPgData)); + if (postmaster.exists() && postmaster.open(QIODevice::ReadOnly)) { + qCDebug(AKONADISERVER_LOG) << "Found a postmaster.pid pidfile, checking whether the server is still running..."; + QByteArray pid = postmaster.readLine(); + // Remove newline character + pid.chop(1); + QFile proc(QString::fromLatin1("/proc/" + pid + "/stat")); + // Check whether the process with the PID from pidfile still exists and whether + // it's actually still postgres or, whether the PID has been recycled in the + // meanwhile. + if (proc.open(QIODevice::ReadOnly)) { + const QByteArray stat = proc.readAll(); + const QList stats = stat.split(' '); + if (stats.count() > 1) { + // Make sure the PID actually belongs to postgres process + if (stats[1] == "(postgres)") { + // Yup, our PostgreSQL is actually running, so pretend we started the server + // and try to connect to it + qCWarning(AKONADISERVER_LOG) << "PostgreSQL for Akonadi is already running, trying to connect to it."; + return true; + } + } + proc.close(); + } + + qCDebug(AKONADISERVER_LOG) << "No postgres process with specified PID is running. Removing the pidfile and starting a new Postgres instance..."; + postmaster.close(); + postmaster.remove(); + } +#endif + + // postgres data directory not initialized yet, so call initdb on it + if (!QFile::exists(QStringLiteral("%1/PG_VERSION").arg(mPgData))) { +#ifdef Q_OS_LINUX + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. This only has effect when done on an empty directory, + // so we call this before calling initdb. + if (Utils::getDirectoryFileSystem(mPgData) == QLatin1String("btrfs")) { + Utils::disableCoW(mPgData); + } +#endif + // call 'initdb --pgdata=/home/user/.local/share/akonadi/db_data' + execute(mInitDbPath, {QStringLiteral("--pgdata=%1").arg(mPgData), QStringLiteral("--encoding=UTF8"), QStringLiteral("--no-locale")}); + } else { + const auto versions = checkPgVersion(); + if (versions.has_value() && (versions->clusterVersion < versions->pgServerVersion)) { + qCInfo(AKONADISERVER_LOG) << "Cluster PG_VERSION is" << versions->clusterVersion << ", PostgreSQL server is version " << versions->pgServerVersion + << ", will attempt to upgrade the cluster"; + if (upgradeCluster(versions->clusterVersion)) { + qCInfo(AKONADISERVER_LOG) << "Successfully upgraded db cluster from Postgres" << versions->clusterVersion << "to" << versions->pgServerVersion; + } else { + qCWarning(AKONADISERVER_LOG) << "Postgres db cluster upgrade failed, Akonadi will fail to start. Sorry."; + } + } + } + + // synthesize the postgres command + QStringList arguments; + arguments << QStringLiteral("start") << QStringLiteral("-w") << QStringLiteral("--timeout=10") // default is 60 seconds. + << QStringLiteral("--pgdata=%1").arg(mPgData) + // These options are passed to postgres + // -k - directory for unix domain socket communication + // -h - disable listening for TCP/IP + << QStringLiteral("-o \"-k%1\" -h ''").arg(socketDir); + + qCDebug(AKONADISERVER_LOG) << "Executing:" << mServerPath << arguments.join(QLatin1Char(' ')); + QProcess pgCtl; + pgCtl.start(mServerPath, arguments); + if (!pgCtl.waitForStarted()) { + qCCritical(AKONADISERVER_LOG) << "Could not start database server!"; + qCCritical(AKONADISERVER_LOG) << "executable:" << mServerPath; + qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; + qCCritical(AKONADISERVER_LOG) << "process error:" << pgCtl.errorString(); + return false; + } + + const QLatin1String initCon("initConnection"); + { + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL"), initCon); + apply(db); + + // use the default database that is always available + db.setDatabaseName(QStringLiteral("postgres")); + + if (!db.isValid()) { + qCCritical(AKONADISERVER_LOG) << "Invalid database object during database server startup"; + return false; + } + + bool opened = false; + for (int i = 0; i < 120; ++i) { + opened = db.open(); + if (opened) { + break; + } + + if (pgCtl.waitForFinished(500) && pgCtl.exitCode()) { + qCCritical(AKONADISERVER_LOG) << "Database process exited unexpectedly during initial connection!"; + qCCritical(AKONADISERVER_LOG) << "executable:" << mServerPath; + qCCritical(AKONADISERVER_LOG) << "arguments:" << arguments; + qCCritical(AKONADISERVER_LOG) << "stdout:" << pgCtl.readAllStandardOutput(); + qCCritical(AKONADISERVER_LOG) << "stderr:" << pgCtl.readAllStandardError(); + qCCritical(AKONADISERVER_LOG) << "exit code:" << pgCtl.exitCode(); + qCCritical(AKONADISERVER_LOG) << "process error:" << pgCtl.errorString(); + return false; + } + } + + if (opened) { + { + QSqlQuery query(db); + + // check if the 'akonadi' database already exists + query.exec(QStringLiteral("SELECT 1 FROM pg_catalog.pg_database WHERE datname = '%1'").arg(mDatabaseName)); + + // if not, create it + if (!query.first()) { + if (!query.exec(QStringLiteral("CREATE DATABASE %1").arg(mDatabaseName))) { + qCCritical(AKONADISERVER_LOG) << "Failed to create database"; + qCCritical(AKONADISERVER_LOG) << "Query error:" << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error:" << db.lastError().text(); + success = false; + } + } + } // make sure query is destroyed before we close the db + db.close(); + } + } + // Make sure pg_ctl has returned + pgCtl.waitForFinished(); + + QSqlDatabase::removeDatabase(initCon); + return success; +} + +void DbConfigPostgresql::stopInternalServer() +{ + if (!checkServerIsRunning()) { + qCDebug(AKONADISERVER_LOG) << "Database is no longer running"; + return; + } + + // first, try a FAST shutdown + execute(mServerPath, {QStringLiteral("stop"), QStringLiteral("--pgdata=%1").arg(mPgData), QStringLiteral("--mode=fast")}); + if (!checkServerIsRunning()) { + return; + } + + // second, try an IMMEDIATE shutdown + execute(mServerPath, {QStringLiteral("stop"), QStringLiteral("--pgdata=%1").arg(mPgData), QStringLiteral("--mode=immediate")}); + if (!checkServerIsRunning()) { + return; + } + + // third, pg_ctl couldn't terminate all the postgres processes, we have to + // kill the master one. We don't want to do that, but we've passed the last + // call. pg_ctl is used to send the kill signal (safe when kill is not + // supported by OS) + const QString pidFileName = QStringLiteral("%1/postmaster.pid").arg(mPgData); + QFile pidFile(pidFileName); + if (pidFile.open(QIODevice::ReadOnly)) { + QString postmasterPid = QString::fromUtf8(pidFile.readLine(0).trimmed()); + qCCritical(AKONADISERVER_LOG) << "The postmaster is still running. Killing it."; + + execute(mServerPath, {QStringLiteral("kill"), QStringLiteral("ABRT"), postmasterPid}); + } +} + +bool DbConfigPostgresql::checkServerIsRunning() +{ + const QString command = mServerPath; + QStringList arguments; + arguments << QStringLiteral("status") << QStringLiteral("--pgdata=%1").arg(mPgData); + + QProcess pgCtl; + pgCtl.start(command, arguments, QIODevice::ReadOnly); + if (!pgCtl.waitForFinished(3000)) { + // Error? + return false; + } + + // "pg_ctl status" exits with 0 when server is running and a non-zero code when not. + return pgCtl.exitCode() == 0; +} diff --git a/src/server/storage/dbconfigpostgresql.h b/src/server/storage/dbconfigpostgresql.h new file mode 100644 index 0000000..ae383c4 --- /dev/null +++ b/src/server/storage/dbconfigpostgresql.h @@ -0,0 +1,98 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "dbconfig.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +class DbConfigPostgresql : public DbConfig +{ +public: + DbConfigPostgresql(); + + /** + * Returns the name of the used driver. + */ + QString driverName() const override; + + /** + * Returns the database name. + */ + QString databaseName() const override; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back if @p storeSettings is true. + */ + bool init(QSettings &settings, bool storeSettings = true) override; + + /** + * This method checks if the requirements for this database connection are + * met in the system (QPOSTGRESQL driver is available, postgresql binary is + * found, etc.). + */ + bool isAvailable(QSettings &settings) override; + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + void apply(QSqlDatabase &database) override; + + /** + * Returns whether an internal server needs to be used. + */ + bool useInternalServer() const override; + + /** + * This method is called to start an external server. + */ + bool startInternalServer() override; + + /** + * This method is called to stop the external server. + */ + void stopInternalServer() override; + +protected: + QStringList postgresSearchPaths(const QString &versionedPath) const; + +private: + struct Versions { + int clusterVersion = 0; + int pgServerVersion = 0; + }; + std::optional checkPgVersion() const; + bool upgradeCluster(int clusterVersion); + bool runInitDb(const QString &dbDataPath); + + bool checkServerIsRunning(); + + QString mDatabaseName; + QString mHostName; + int mHostPort; + QString mUserName; + QString mPassword; + QString mConnectionOptions; + QString mServerPath; + QString mInitDbPath; + QString mPgData; + QString mPgUpgradePath; + bool mInternalServer; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbconfigsqlite.cpp b/src/server/storage/dbconfigsqlite.cpp new file mode 100644 index 0000000..ff8dd23 --- /dev/null +++ b/src/server/storage/dbconfigsqlite.cpp @@ -0,0 +1,279 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbconfigsqlite.h" +#include "akonadiserver_debug.h" +#include "utils.h" + +#include + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +static QString dataDir() +{ + QString akonadiHomeDir = StandardDirs::saveDir("data"); + if (!QDir(akonadiHomeDir).exists()) { + if (!QDir().mkpath(akonadiHomeDir)) { + qCCritical(AKONADISERVER_LOG) << "Unable to create" << akonadiHomeDir << "during database initialization"; + return QString(); + } + } + + akonadiHomeDir += QDir::separator(); + + return akonadiHomeDir; +} + +static QString sqliteDataFile() +{ + const QString dir = dataDir(); + if (dir.isEmpty()) { + return QString(); + } + const QString akonadiPath = dir + QLatin1String("akonadi.db"); + if (!QFile::exists(akonadiPath)) { + QFile file(akonadiPath); + if (!file.open(QIODevice::WriteOnly)) { + qCCritical(AKONADISERVER_LOG) << "Unable to create file" << akonadiPath << "during database initialization."; + return QString(); + } + file.close(); + } + + return akonadiPath; +} + +DbConfigSqlite::DbConfigSqlite(Version driverVersion) + : mDriverVersion(driverVersion) +{ +} + +QString DbConfigSqlite::driverName() const +{ + if (mDriverVersion == Default) { + return QStringLiteral("QSQLITE"); + } else { + return QStringLiteral("QSQLITE3"); + } +} + +QString DbConfigSqlite::databaseName() const +{ + return mDatabaseName; +} + +bool DbConfigSqlite::init(QSettings &settings, bool storeSettings) +{ + // determine default settings depending on the driver + const QString defaultDbName = sqliteDataFile(); + if (defaultDbName.isEmpty()) { + return false; + } + + // read settings for current driver + settings.beginGroup(driverName()); + mDatabaseName = settings.value(QStringLiteral("Name"), defaultDbName).toString(); + mHostName = settings.value(QStringLiteral("Host")).toString(); + mUserName = settings.value(QStringLiteral("User")).toString(); + mPassword = settings.value(QStringLiteral("Password")).toString(); + mConnectionOptions = settings.value(QStringLiteral("Options")).toString(); + settings.endGroup(); + + if (storeSettings) { + // store back the default values + settings.beginGroup(driverName()); + settings.setValue(QStringLiteral("Name"), mDatabaseName); + settings.endGroup(); + settings.sync(); + } + + return true; +} + +bool DbConfigSqlite::isAvailable(QSettings &settings) +{ + if (!QSqlDatabase::drivers().contains(driverName())) { + return false; + } + + if (!init(settings, false)) { + return false; + } + + return true; +} + +void DbConfigSqlite::apply(QSqlDatabase &database) +{ + if (!mDatabaseName.isEmpty()) { + database.setDatabaseName(mDatabaseName); + } + if (!mHostName.isEmpty()) { + database.setHostName(mHostName); + } + if (!mUserName.isEmpty()) { + database.setUserName(mUserName); + } + if (!mPassword.isEmpty()) { + database.setPassword(mPassword); + } + + if (driverName() == QLatin1String("QSQLITE3") && !mConnectionOptions.contains(QLatin1String("SQLITE_ENABLE_SHARED_CACHE"))) { + mConnectionOptions += QLatin1String(";QSQLITE_ENABLE_SHARED_CACHE"); + } + database.setConnectOptions(mConnectionOptions); + + // can we check that during init() already? + Q_ASSERT(database.driver()->hasFeature(QSqlDriver::LastInsertId)); +} + +bool DbConfigSqlite::useInternalServer() const +{ + return false; +} + +bool DbConfigSqlite::setPragma(QSqlDatabase &db, QSqlQuery &query, const QString &pragma) +{ + if (!query.exec(QStringLiteral("PRAGMA %1").arg(pragma))) { + qCCritical(AKONADISERVER_LOG) << "Could not set sqlite PRAGMA " << pragma; + qCCritical(AKONADISERVER_LOG) << "Database: " << mDatabaseName; + qCCritical(AKONADISERVER_LOG) << "Query error: " << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error: " << db.lastError().text(); + return false; + } + return true; +} + +void DbConfigSqlite::setup() +{ + const QLatin1String connectionName("initConnection"); + + { + QSqlDatabase db = QSqlDatabase::addDatabase(driverName(), connectionName); + + if (!db.isValid()) { + qCCritical(AKONADISERVER_LOG) << "Invalid database for" << mDatabaseName << "with driver" << driverName(); + return; + } + + QFileInfo finfo(mDatabaseName); + if (!finfo.dir().exists()) { + QDir dir; + dir.mkpath(finfo.path()); + } + +#ifdef Q_OS_LINUX + QFile dbFile(mDatabaseName); + // It is recommended to disable CoW feature when running on Btrfs to improve + // database performance. It does not have any effect on non-empty files, so + // we check, whether the database has not yet been initialized. + if (dbFile.size() == 0) { + if (Utils::getDirectoryFileSystem(mDatabaseName) == QLatin1String("btrfs")) { + Utils::disableCoW(mDatabaseName); + } + } +#endif + + db.setDatabaseName(mDatabaseName); + if (!db.open()) { + qCCritical(AKONADISERVER_LOG) << "Could not open sqlite database" << mDatabaseName << "with driver" << driverName() << "for initialization"; + db.close(); + return; + } + + apply(db); + + QSqlQuery query(db); + if (!query.exec(QStringLiteral("SELECT sqlite_version()"))) { + qCCritical(AKONADISERVER_LOG) << "Could not query sqlite version"; + qCCritical(AKONADISERVER_LOG) << "Database: " << mDatabaseName; + qCCritical(AKONADISERVER_LOG) << "Query error: " << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + if (!query.next()) { // should never occur + qCCritical(AKONADISERVER_LOG) << "Could not query sqlite version"; + qCCritical(AKONADISERVER_LOG) << "Database: " << mDatabaseName; + qCCritical(AKONADISERVER_LOG) << "Query error: " << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + const QString sqliteVersion = query.value(0).toString(); + qCDebug(AKONADISERVER_LOG) << "sqlite version is " << sqliteVersion; + + const QStringList list = sqliteVersion.split(QLatin1Char('.')); + const int sqliteVersionMajor = list[0].toInt(); + const int sqliteVersionMinor = list[1].toInt(); + + // set synchronous mode to NORMAL; see http://www.sqlite.org/pragma.html#pragma_synchronous + if (!setPragma(db, query, QStringLiteral("synchronous=1"))) { + db.close(); + return; + } + + if (sqliteVersionMajor < 3 && sqliteVersionMinor < 7) { + // wal mode is only supported with >= sqlite 3.7.0 + db.close(); + return; + } + + // set write-ahead-log mode; see http://www.sqlite.org/wal.html + if (!setPragma(db, query, QStringLiteral("journal_mode=wal"))) { + db.close(); + return; + } + + if (!query.next()) { // should never occur + qCCritical(AKONADISERVER_LOG) << "Could not query sqlite journal mode"; + qCCritical(AKONADISERVER_LOG) << "Database: " << mDatabaseName; + qCCritical(AKONADISERVER_LOG) << "Query error: " << query.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Database error: " << db.lastError().text(); + db.close(); + return; + } + + const QString journalMode = query.value(0).toString(); + qCDebug(AKONADISERVER_LOG) << "sqlite journal mode is " << journalMode; + + // as of sqlite 3.12 this is default, previously was 1024. + if (!setPragma(db, query, QStringLiteral("page_size=4096"))) { + db.close(); + return; + } + + // set cache_size to 100000 pages; see https://www.sqlite.org/pragma.html#pragma_cache_size + if (!setPragma(db, query, QStringLiteral("cache_size=100000"))) { + db.close(); + return; + } + + // construct temporary tables in memory; see https://www.sqlite.org/pragma.html#pragma_temp_store + if (!setPragma(db, query, QStringLiteral("temp_store=MEMORY"))) { + db.close(); + return; + } + + // enable foreign key support; see https://www.sqlite.org/pragma.html#pragma_foreign_keys + if (!setPragma(db, query, QStringLiteral("foreign_keys=ON"))) { + db.close(); + return; + } + + db.close(); + } + + QSqlDatabase::removeDatabase(connectionName); +} diff --git a/src/server/storage/dbconfigsqlite.h b/src/server/storage/dbconfigsqlite.h new file mode 100644 index 0000000..20202e8 --- /dev/null +++ b/src/server/storage/dbconfigsqlite.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "dbconfig.h" + +namespace Akonadi +{ +namespace Server +{ +class DbConfigSqlite : public DbConfig +{ +public: + enum Version { + Default, /** Uses the Qt sqlite driver */ + Custom /** Uses the custom qsqlite driver from akonadi/qsqlite */ + }; + +public: + explicit DbConfigSqlite(Version driver); + + /** + * Returns the name of the used driver. + */ + QString driverName() const override; + + /** + * Returns the database name. + */ + QString databaseName() const override; + + /** + * This method is called whenever the Akonadi server is started + * and before the initial database connection is set up. + * + * At this point the default settings should be determined, merged + * with the given @p settings and written back if @p storeSettings is true. + */ + bool init(QSettings &settings, bool storeSettings = true) override; + + /** + * This method checks if the requirements for this database connection are met + * in the system (QSQLITE/QSQLITE3 driver is available, object can be initialized, etc.). + */ + bool isAvailable(QSettings &settings) override; + + /** + * This method applies the configured settings to the QtSql @p database + * instance. + */ + void apply(QSqlDatabase &database) override; + + /** + * Returns whether an internal server needs to be used. + */ + bool useInternalServer() const override; + + /** + * Sets sqlite journal mode to WAL and synchronous mode to NORMAL + */ + void setup() override; + +private: + bool setPragma(QSqlDatabase &db, QSqlQuery &query, const QString &pragma); + + Version mDriverVersion; + QString mDatabaseName; + QString mHostName; + QString mUserName; + QString mPassword; + QString mConnectionOptions; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbdeadlockcatcher.h b/src/server/storage/dbdeadlockcatcher.h new file mode 100644 index 0000000..451a220 --- /dev/null +++ b/src/server/storage/dbdeadlockcatcher.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2019 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "dbexception.h" + +namespace Akonadi +{ +namespace Server +{ +/** + This class catches DbDeadlockException (as emitted by QueryBuilder) + and retries execution of the method when it happens, as required by + SQL databases. +*/ +class DbDeadlockCatcher +{ +public: + template explicit DbDeadlockCatcher(Func &&func) + { + callFunc(func, 0); + } + +private: + static const int MaxRecursion = 5; + template void callFunc(Func &&func, int recursionCounter) + { + try { + func(); + } catch (const DbDeadlockException &) { + if (recursionCounter == MaxRecursion) { + throw; + } else { + callFunc(func, ++recursionCounter); // recurse + } + } + } +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbexception.cpp b/src/server/storage/dbexception.cpp new file mode 100644 index 0000000..1d167e8 --- /dev/null +++ b/src/server/storage/dbexception.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbexception.h" + +#include +#include + +using namespace Akonadi::Server; + +DbException::DbException(const QSqlQuery &query, const char *what) + : Exception(what) +{ + mWhat += "\nSql error: " + query.lastError().text().toUtf8(); + mWhat += "\nQuery: " + query.lastQuery().toUtf8(); +} + +const char *DbException::type() const throw() +{ + return "Database Exception"; +} + +DbDeadlockException::DbDeadlockException(const QSqlQuery &query) + : DbException(query, "Database deadlock, unsuccessful after multiple retries") +{ +} diff --git a/src/server/storage/dbexception.h b/src/server/storage/dbexception.h new file mode 100644 index 0000000..9c5a764 --- /dev/null +++ b/src/server/storage/dbexception.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "exception.h" + +class QSqlQuery; + +namespace Akonadi +{ +namespace Server +{ +/** Exception for reporting SQL errors. */ +class DbException : public Exception +{ +public: + explicit DbException(const QSqlQuery &query, const char *what = nullptr); + const char *type() const throw() override; +}; + +class DbDeadlockException : public DbException +{ +public: + explicit DbDeadlockException(const QSqlQuery &query); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbinitializer.cpp b/src/server/storage/dbinitializer.cpp new file mode 100644 index 0000000..ea5e646 --- /dev/null +++ b/src/server/storage/dbinitializer.cpp @@ -0,0 +1,397 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2012 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "dbinitializer.h" +#include "akonadiserver_debug.h" +#include "dbexception.h" +#include "dbinitializer_p.h" +#include "dbtype.h" +#include "entities.h" +#include "schema.h" + +#include +#include +#include + +#include + +#include + +using namespace Akonadi::Server; + +DbInitializer::Ptr DbInitializer::createInstance(const QSqlDatabase &database, Schema *schema) +{ + DbInitializer::Ptr i; + switch (DbType::type(database)) { + case DbType::MySQL: + i.reset(new DbInitializerMySql(database)); + break; + case DbType::Sqlite: + i.reset(new DbInitializerSqlite(database)); + break; + case DbType::PostgreSQL: + i.reset(new DbInitializerPostgreSql(database)); + break; + case DbType::Unknown: + qCCritical(AKONADISERVER_LOG) << database.driverName() << "backend not supported"; + break; + } + i->mSchema = schema; + return i; +} + +DbInitializer::DbInitializer(const QSqlDatabase &database) + : mDatabase(database) + , mSchema(nullptr) + , mTestInterface(nullptr) +{ + m_introspector = DbIntrospector::createInstance(mDatabase); +} + +DbInitializer::~DbInitializer() +{ +} + +bool DbInitializer::run() +{ + try { + qCInfo(AKONADISERVER_LOG) << "Running DB initializer"; + + const auto tables = mSchema->tables(); + for (const TableDescription &table : tables) { + if (!checkTable(table)) { + return false; + } + } + + const auto relations = mSchema->relations(); + for (const RelationDescription &relation : relations) { + if (!checkRelation(relation)) { + return false; + } + } + +#ifndef DBINITIALIZER_UNITTEST + // Now finally check and set the generation identifier if necessary + SchemaVersion version = SchemaVersion::retrieveAll().at(0); + if (version.generation() == 0) { + version.setGeneration(QDateTime::currentDateTimeUtc().toTime_t()); + version.update(); + + qCDebug(AKONADISERVER_LOG) << "Generation:" << version.generation(); + } +#endif + + qCInfo(AKONADISERVER_LOG) << "DB initializer done"; + return true; + } catch (const DbException &e) { + mErrorMsg = QString::fromUtf8(e.what()); + } + return false; +} + +bool DbInitializer::checkTable(const TableDescription &tableDescription) +{ + qCDebug(AKONADISERVER_LOG) << "checking table " << tableDescription.name; + + if (!m_introspector->hasTable(tableDescription.name)) { + // Get the CREATE TABLE statement for the specific SQL dialect + const QString createTableStatement = buildCreateTableStatement(tableDescription); + qCDebug(AKONADISERVER_LOG) << createTableStatement; + execQuery(createTableStatement); + } else { + // Check for every column whether it exists, and add the missing ones + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + if (!m_introspector->hasColumn(tableDescription.name, columnDescription.name)) { + // Don't add the column on update, DbUpdater will add it + if (columnDescription.noUpdate) { + continue; + } + // Get the ADD COLUMN statement for the specific SQL dialect + const QString statement = buildAddColumnStatement(tableDescription, columnDescription); + qCDebug(AKONADISERVER_LOG) << statement; + execQuery(statement); + } + } + + // NOTE: we do intentionally not delete any columns here, we defer that to the updater, + // very likely previous columns contain data that needs to be moved to a new column first. + } + + // Add initial data if table is empty + if (tableDescription.data.isEmpty()) { + return true; + } + if (m_introspector->isTableEmpty(tableDescription.name)) { + Q_FOREACH (const DataDescription &dataDescription, tableDescription.data) { + // Get the INSERT VALUES statement for the specific SQL dialect + const QString statement = buildInsertValuesStatement(tableDescription, dataDescription); + qCDebug(AKONADISERVER_LOG) << statement; + execQuery(statement); + } + } + + return true; +} + +void DbInitializer::checkForeignKeys(const TableDescription &tableDescription) +{ + try { + const QVector existingForeignKeys = m_introspector->foreignKeyConstraints(tableDescription.name); + Q_FOREACH (const ColumnDescription &column, tableDescription.columns) { + DbIntrospector::ForeignKey existingForeignKey; + for (const DbIntrospector::ForeignKey &fk : existingForeignKeys) { + if (QString::compare(fk.column, column.name, Qt::CaseInsensitive) == 0) { + existingForeignKey = fk; + break; + } + } + + if (!column.refTable.isEmpty() && !column.refColumn.isEmpty()) { + if (!existingForeignKey.column.isEmpty()) { + // there's a constraint on this column, check if it's the correct one + if (QString::compare(existingForeignKey.refTable, column.refTable + QLatin1String("table"), Qt::CaseInsensitive) == 0 + && QString::compare(existingForeignKey.refColumn, column.refColumn, Qt::CaseInsensitive) == 0 + && QString::compare(existingForeignKey.onUpdate, referentialActionToString(column.onUpdate), Qt::CaseInsensitive) == 0 + && QString::compare(existingForeignKey.onDelete, referentialActionToString(column.onDelete), Qt::CaseInsensitive) == 0) { + continue; // all good + } + + const auto statements = buildRemoveForeignKeyConstraintStatements(existingForeignKey, tableDescription); + if (!statements.isEmpty()) { + qCDebug(AKONADISERVER_LOG) << "Found existing foreign constraint that doesn't match the schema:" << existingForeignKey.name + << existingForeignKey.column << existingForeignKey.refTable << existingForeignKey.refColumn; + m_removedForeignKeys << statements; + } + } + + const auto statements = buildAddForeignKeyConstraintStatements(tableDescription, column); + if (statements.isEmpty()) { // not supported + return; + } + + m_pendingForeignKeys << statements; + + } else if (!existingForeignKey.column.isEmpty()) { + // constraint exists but we don't want one here + const auto statements = buildRemoveForeignKeyConstraintStatements(existingForeignKey, tableDescription); + if (!statements.isEmpty()) { + qCDebug(AKONADISERVER_LOG) << "Found unexpected foreign key constraint:" << existingForeignKey.name << existingForeignKey.column + << existingForeignKey.refTable << existingForeignKey.refColumn; + m_removedForeignKeys << statements; + } + } + } + } catch (const DbException &e) { + qCDebug(AKONADISERVER_LOG) << "Fixing foreign key constraints failed:" << e.what(); + } +} + +void DbInitializer::checkIndexes(const TableDescription &tableDescription) +{ + // Add indices + Q_FOREACH (const IndexDescription &indexDescription, tableDescription.indexes) { + // sqlite3 needs unique index identifiers per db + const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name); + if (!m_introspector->hasIndex(tableDescription.name, indexName)) { + // Get the CREATE INDEX statement for the specific SQL dialect + m_pendingIndexes << buildCreateIndexStatement(tableDescription, indexDescription); + } + } +} + +bool DbInitializer::checkRelation(const RelationDescription &relationDescription) +{ + return checkTable(RelationTableDescription(relationDescription)); +} + +QString DbInitializer::errorMsg() const +{ + return mErrorMsg; +} + +bool DbInitializer::updateIndexesAndConstraints() +{ + Q_FOREACH (const TableDescription &table, mSchema->tables()) { + // Make sure the foreign key constraints are all there + checkForeignKeys(table); + checkIndexes(table); + } + Q_FOREACH (const RelationDescription &relation, mSchema->relations()) { + RelationTableDescription relTable(relation); + checkForeignKeys(relTable); + checkIndexes(relTable); + } + + try { + if (!m_pendingIndexes.isEmpty()) { + qCDebug(AKONADISERVER_LOG) << "Updating indexes"; + execPendingQueries(m_pendingIndexes); + m_pendingIndexes.clear(); + } + + if (!m_removedForeignKeys.isEmpty()) { + qCDebug(AKONADISERVER_LOG) << "Removing invalid foreign key constraints"; + execPendingQueries(m_removedForeignKeys); + m_removedForeignKeys.clear(); + } + + if (!m_pendingForeignKeys.isEmpty()) { + qCDebug(AKONADISERVER_LOG) << "Adding new foreign key constraints"; + execPendingQueries(m_pendingForeignKeys); + m_pendingForeignKeys.clear(); + } + } catch (const DbException &e) { + qCCritical(AKONADISERVER_LOG) << "Updating index failed: " << e.what(); + return false; + } + + qCDebug(AKONADISERVER_LOG) << "Indexes successfully created"; + return true; +} + +void DbInitializer::execPendingQueries(const QStringList &queries) +{ + for (const QString &statement : queries) { + qCDebug(AKONADISERVER_LOG) << statement; + execQuery(statement); + } +} + +QString DbInitializer::sqlType(const ColumnDescription &col, int size) const +{ + Q_UNUSED(size) + if (col.type == QLatin1String("int")) { + return QStringLiteral("INTEGER"); + } + if (col.type == QLatin1String("qint64")) { + return QStringLiteral("BIGINT"); + } + if (col.type == QLatin1String("QString")) { + return QStringLiteral("TEXT"); + } + if (col.type == QLatin1String("QByteArray")) { + return QStringLiteral("LONGBLOB"); + } + if (col.type == QLatin1String("QDateTime")) { + return QStringLiteral("TIMESTAMP"); + } + if (col.type == QLatin1String("bool")) { + return QStringLiteral("BOOL"); + } + if (col.isEnum) { + return QStringLiteral("TINYINT"); + } + + qCCritical(AKONADISERVER_LOG) << "Invalid type" << col.type; + Q_ASSERT(false); + return QString(); +} + +QString DbInitializer::sqlValue(const ColumnDescription &col, const QString &value) const +{ + if (col.type == QLatin1String("QDateTime") && value == QLatin1String("QDateTime::currentDateTimeUtc()")) { + return QStringLiteral("CURRENT_TIMESTAMP"); + } else if (col.isEnum) { + return QString::number(col.enumValueMap[value]); + } + return value; +} + +QString DbInitializer::buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const +{ + return QStringLiteral("ALTER TABLE %1 ADD COLUMN %2").arg(tableDescription.name, buildColumnStatement(columnDescription, tableDescription)); +} + +QString DbInitializer::buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const +{ + const QString indexName = QStringLiteral("%1_%2").arg(tableDescription.name, indexDescription.name); + QStringList columns; + if (indexDescription.sort.isEmpty()) { + columns = indexDescription.columns; + } else { + columns.reserve(indexDescription.columns.count()); + std::transform(indexDescription.columns.cbegin(), + indexDescription.columns.cend(), + std::back_insert_iterator(columns), + [&indexDescription](const QString &column) { + return QStringLiteral("%1 %2").arg(column, indexDescription.sort); + }); + } + + return QStringLiteral("CREATE %1 INDEX %2 ON %3 (%4)") + .arg(indexDescription.isUnique ? QStringLiteral("UNIQUE") : QString(), indexName, tableDescription.name, columns.join(QLatin1Char(','))); +} + +QStringList DbInitializer::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const +{ + Q_UNUSED(table) + Q_UNUSED(column) + return {}; +} + +QStringList DbInitializer::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const +{ + Q_UNUSED(fk) + Q_UNUSED(table) + return {}; +} + +QString DbInitializer::buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete) +{ + return QLatin1String("ON UPDATE ") + referentialActionToString(onUpdate) + QLatin1String(" ON DELETE ") + referentialActionToString(onDelete); +} + +QString DbInitializer::referentialActionToString(ColumnDescription::ReferentialAction action) +{ + switch (action) { + case ColumnDescription::Cascade: + return QStringLiteral("CASCADE"); + case ColumnDescription::Restrict: + return QStringLiteral("RESTRICT"); + case ColumnDescription::SetNull: + return QStringLiteral("SET NULL"); + } + + Q_ASSERT(!"invalid referential action enum!"); + return QString(); +} + +QString DbInitializer::buildPrimaryKeyStatement(const TableDescription &table) +{ + QStringList cols; + for (const ColumnDescription &column : std::as_const(table.columns)) { + if (column.isPrimaryKey) { + cols.push_back(column.name); + } + } + return QLatin1String("PRIMARY KEY (") + cols.join(QLatin1String(", ")) + QLatin1Char(')'); +} + +void DbInitializer::execQuery(const QString &queryString) +{ + // if ( Q_UNLIKELY( mTestInterface ) ) { Qt 4.7 has no Q_UNLIKELY yet + if (mTestInterface) { + mTestInterface->execStatement(queryString); + return; + } + + QSqlQuery query(mDatabase); + if (!query.exec(queryString)) { + throw DbException(query); + } +} + +void DbInitializer::setTestInterface(TestInterface *interface) +{ + mTestInterface = interface; +} + +void DbInitializer::setIntrospector(const DbIntrospector::Ptr &introspector) +{ + m_introspector = introspector; +} diff --git a/src/server/storage/dbinitializer.h b/src/server/storage/dbinitializer.h new file mode 100644 index 0000000..ca90c9b --- /dev/null +++ b/src/server/storage/dbinitializer.h @@ -0,0 +1,183 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "dbintrospector.h" +#include "schematypes.h" + +#include +#include +#include +#include + +class DbInitializerTest; + +namespace Akonadi +{ +namespace Server +{ +class Schema; +class DbUpdater; + +class TestInterface +{ +public: + virtual ~TestInterface() = default; + + virtual void execStatement(const QString &statement) = 0; + +protected: + explicit TestInterface() = default; + +private: + Q_DISABLE_COPY_MOVE(TestInterface) +}; + +/** + * A helper class which takes a reference to a database object and + * the file name of a template file and initializes the database + * according to the rules in the template file. + * + * TODO: Refactor this to be easily reusable for updater too + */ +class DbInitializer +{ + friend class DbUpdater; + +public: + using Ptr = QSharedPointer; + + /** + Returns an initializer instance for a given backend. + */ + static DbInitializer::Ptr createInstance(const QSqlDatabase &database, Schema *schema = nullptr); + + /** + * Destroys the database initializer. + */ + virtual ~DbInitializer(); + + /** + * Starts the initialization process. + * On success true is returned, false otherwise. + * + * If something went wrong @see errorMsg() can be used to retrieve more + * information. + */ + bool run(); + + /** + * Returns the textual description of an occurred error. + */ + QString errorMsg() const; + + /** + * Returns whether the database has working and complete foreign keys. + * This information can be used for query optimizations. + * @note Result is invalid before run() has been called. + */ + virtual bool hasForeignKeyConstraints() const = 0; + + /** + * Checks and creates missing indexes. + * + * This method is run after DbUpdater to ensure that data in new columns + * are populated and creation of indexes and foreign keys does not fail. + */ + bool updateIndexesAndConstraints(); + + /** + * Returns a backend-specific CREATE TABLE SQL query describing given table + */ + virtual QString buildCreateTableStatement(const TableDescription &tableDescription) const = 0; + +protected: + /** + * Creates a new database initializer. + * + * @param database The reference to the database. + */ + DbInitializer(const QSqlDatabase &database); + + /** + * Overwrite in backend-specific sub-classes to return the SQL type for a given C++ type. + * @param type Name of the C++ type. + * @param size Optional size hint for the column, if -1 use the default SQL type for @p type. + */ + virtual QString sqlType(const ColumnDescription &col, int size) const; + /** Overwrite in backend-specific sub-classes to return the SQL value for a given C++ value. */ + virtual QString sqlValue(const ColumnDescription &col, const QString &value) const; + + virtual QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const = 0; + virtual QString buildAddColumnStatement(const TableDescription &tableDescription, const ColumnDescription &columnDescription) const; + virtual QString buildCreateIndexStatement(const TableDescription &tableDescription, const IndexDescription &indexDescription) const; + virtual QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const = 0; + + /** + * Returns an SQL statements to add a foreign key constraint to an existing column @p column. + * The default implementation returns an empty string, so any backend supporting foreign key constraints + * must reimplement this. + */ + virtual QStringList buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const; + + /** + * Returns an SQL statements to remove the foreign key constraint @p fk from table @p table. + * The default implementation returns an empty string, so any backend supporting foreign key constraints + * must reimplement this. + */ + virtual QStringList buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const; + + static QString buildReferentialAction(ColumnDescription::ReferentialAction onUpdate, ColumnDescription::ReferentialAction onDelete); + /// Use for multi-column primary keys during table creation + static QString buildPrimaryKeyStatement(const TableDescription &table); + +private: + friend class ::DbInitializerTest; + Q_DISABLE_COPY_MOVE(DbInitializer) + + /** + * Sets the debug @p interface that shall be used on unit test run. + */ + void setTestInterface(TestInterface *interface); + + /** + * Sets a different DbIntrospector. This allows unit tests to simulate certain + * states of the database. + */ + void setIntrospector(const DbIntrospector::Ptr &introspector); + + /** Helper method for executing a query. + * If a debug interface is set for testing, that gets the queries instead. + * @throws DbException if something went wrong. + */ + void execQuery(const QString &queryString); + + bool checkTable(const TableDescription &tableDescription); + /** + * Checks foreign key constraints on table @p tableDescription and fixes them if necessary. + */ + void checkForeignKeys(const TableDescription &tableDescription); + void checkIndexes(const TableDescription &tableDescription); + bool checkRelation(const RelationDescription &relationDescription); + + static QString referentialActionToString(ColumnDescription::ReferentialAction action); + + void execPendingQueries(const QStringList &queries); + + QSqlDatabase mDatabase; + Schema *mSchema = nullptr; + QString mErrorMsg; + TestInterface *mTestInterface = nullptr; + DbIntrospector::Ptr m_introspector; + QStringList m_pendingIndexes; + QStringList m_pendingForeignKeys; + QStringList m_removedForeignKeys; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbinitializer_p.cpp b/src/server/storage/dbinitializer_p.cpp new file mode 100644 index 0000000..dd16c3c --- /dev/null +++ b/src/server/storage/dbinitializer_p.cpp @@ -0,0 +1,354 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "storage/dbinitializer_p.h" +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +// BEGIN MySQL + +DbInitializerMySql::DbInitializerMySql(const QSqlDatabase &database) + : DbInitializer(database) +{ +} + +bool DbInitializerMySql::hasForeignKeyConstraints() const +{ + return true; +} + +QString DbInitializerMySql::sqlType(const ColumnDescription &col, int size) const +{ + if (col.type == QLatin1String("QString")) { + return QLatin1String("VARBINARY(") + QString::number(size <= 0 ? 255 : size) + QLatin1String(")"); + } else { + return DbInitializer::sqlType(col, size); + } +} + +QString DbInitializerMySql::buildCreateTableStatement(const TableDescription &tableDescription) const +{ + QStringList columns; + QStringList references; + + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + columns.append(buildColumnStatement(columnDescription, tableDescription)); + + if (!columnDescription.refTable.isEmpty() && !columnDescription.refColumn.isEmpty()) { + references << QStringLiteral("FOREIGN KEY (%1) REFERENCES %2Table(%3) ") + .arg(columnDescription.name, columnDescription.refTable, columnDescription.refColumn) + + buildReferentialAction(columnDescription.onUpdate, columnDescription.onDelete); + } + } + + if (tableDescription.primaryKeyColumnCount() > 1) { + columns.push_back(buildPrimaryKeyStatement(tableDescription)); + } + columns << references; + + QString tableProperties = QStringLiteral(" COLLATE=utf8_general_ci DEFAULT CHARSET=utf8"); + if (tableDescription.columns | AkRanges::Actions::any([](const auto &col) { + return col.type == QLatin1String("QString") && col.size > 255; + })) { + tableProperties += QStringLiteral(" ROW_FORMAT=DYNAMIC"); + } + + return QStringLiteral("CREATE TABLE %1 (%2) %3").arg(tableDescription.name, columns.join(QStringLiteral(", ")), tableProperties); +} + +QString DbInitializerMySql::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const +{ + QString column = columnDescription.name; + + column += QLatin1Char(' ') + sqlType(columnDescription, columnDescription.size); + + if (!columnDescription.allowNull) { + column += QLatin1String(" NOT NULL"); + } + + if (columnDescription.isAutoIncrement) { + column += QLatin1String(" AUTO_INCREMENT"); + } + + if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { + column += QLatin1String(" PRIMARY KEY"); + } + + if (columnDescription.isUnique) { + column += QLatin1String(" UNIQUE"); + } + + if (!columnDescription.defaultValue.isEmpty()) { + const QString defaultValue = sqlValue(columnDescription, columnDescription.defaultValue); + + if (!defaultValue.isEmpty()) { + column += QStringLiteral(" DEFAULT %1").arg(defaultValue); + } + } + + return column; +} + +QString DbInitializerMySql::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const +{ + QMap data = dataDescription.data; + QStringList keys; + QStringList values; + for (auto it = data.begin(), end = data.end(); it != end; ++it) { + it.value().replace(QLatin1String("\\"), QLatin1String("\\\\")); + keys.push_back(it.key()); + values.push_back(it.value()); + } + + return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)").arg(tableDescription.name, keys.join(QLatin1Char(',')), values.join(QLatin1Char(','))); +} + +QStringList DbInitializerMySql::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const +{ + return {QStringLiteral("ALTER TABLE %1 ADD FOREIGN KEY (%2) REFERENCES %4Table(%5) %6") + .arg(table.name, column.name, column.refTable, column.refColumn, buildReferentialAction(column.onUpdate, column.onDelete))}; +} + +QStringList DbInitializerMySql::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const +{ + return {QStringLiteral("ALTER TABLE %1 DROP FOREIGN KEY %2").arg(table.name, fk.name)}; +} + +// END MySQL + +// BEGIN Sqlite + +DbInitializerSqlite::DbInitializerSqlite(const QSqlDatabase &database) + : DbInitializer(database) +{ +} + +bool DbInitializerSqlite::hasForeignKeyConstraints() const +{ + return true; +} + +QString DbInitializerSqlite::buildCreateTableStatement(const TableDescription &tableDescription) const +{ + QStringList columns; + + columns.reserve(tableDescription.columns.count() + 1); + for (const ColumnDescription &columnDescription : std::as_const(tableDescription.columns)) { + columns.append(buildColumnStatement(columnDescription, tableDescription)); + } + + if (tableDescription.primaryKeyColumnCount() > 1) { + columns.push_back(buildPrimaryKeyStatement(tableDescription)); + } + QStringList references; + for (const ColumnDescription &columnDescription : std::as_const(tableDescription.columns)) { + if (!columnDescription.refTable.isEmpty() && !columnDescription.refColumn.isEmpty()) { + const auto constraintName = + QStringLiteral("%1%2_%3%4_fk").arg(tableDescription.name, columnDescription.name, columnDescription.refTable, columnDescription.refColumn); + references << QStringLiteral("CONSTRAINT %1 FOREIGN KEY (%2) REFERENCES %3Table(%4) %5 DEFERRABLE INITIALLY DEFERRED") + .arg(constraintName, + columnDescription.name, + columnDescription.refTable, + columnDescription.refColumn, + buildReferentialAction(columnDescription.onUpdate, columnDescription.onDelete)); + } + } + columns << references; + + return QStringLiteral("CREATE TABLE %1 (%2)").arg(tableDescription.name, columns.join(QStringLiteral(", "))); +} + +QString DbInitializerSqlite::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const +{ + QString column = columnDescription.name + QLatin1Char(' '); + + if (columnDescription.isAutoIncrement) { + column += QLatin1String("INTEGER"); + } else { + column += sqlType(columnDescription, columnDescription.size); + } + + if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { + column += QLatin1String(" PRIMARY KEY"); + } else if (columnDescription.isUnique) { + column += QLatin1String(" UNIQUE"); + } + + if (columnDescription.isAutoIncrement) { + column += QLatin1String(" AUTOINCREMENT"); + } + + if (!columnDescription.allowNull) { + column += QLatin1String(" NOT NULL"); + } + + if (!columnDescription.defaultValue.isEmpty()) { + const QString defaultValue = sqlValue(columnDescription, columnDescription.defaultValue); + + if (!defaultValue.isEmpty()) { + column += QStringLiteral(" DEFAULT %1").arg(defaultValue); + } + } + + return column; +} + +QString DbInitializerSqlite::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const +{ + QMap data = dataDescription.data; + QStringList keys; + QStringList values; + for (auto it = data.begin(), end = data.end(); it != end; ++it) { + it.value().replace(QLatin1String("true"), QLatin1String("1")); + it.value().replace(QLatin1String("false"), QLatin1String("0")); + keys.push_back(it.key()); + values.push_back(it.value()); + } + + return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)").arg(tableDescription.name, keys.join(QLatin1Char(',')), values.join(QLatin1Char(','))); +} + +QString DbInitializerSqlite::sqlValue(const ColumnDescription &col, const QString &value) const +{ + if (col.type == QLatin1String("bool")) { + if (value == QLatin1String("false")) { + return QStringLiteral("0"); + } else if (value == QLatin1String("true")) { + return QStringLiteral("1"); + } + return value; + } + + return Akonadi::Server::DbInitializer::sqlValue(col, value); +} + +QStringList DbInitializerSqlite::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription & /*column*/) const +{ + return buildUpdateForeignKeyConstraintsStatements(table); +} + +QStringList DbInitializerSqlite::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey & /*fk*/, const TableDescription &table) const +{ + return buildUpdateForeignKeyConstraintsStatements(table); +} + +QStringList DbInitializerSqlite::buildUpdateForeignKeyConstraintsStatements(const TableDescription &table) const +{ + // Unforunately, SQLite does not support add or removing foreign keys through ALTER TABLE, + // this is the only way how to do it. + return {QStringLiteral("PRAGMA defer_foreign_keys=ON"), + QStringLiteral("BEGIN TRANSACTION"), + QStringLiteral("ALTER TABLE %1 RENAME TO %1_old").arg(table.name), + buildCreateTableStatement(table), + QStringLiteral("INSERT INTO %1 SELECT * FROM %1_old").arg(table.name), + QStringLiteral("DROP TABLE %1_old").arg(table.name), + QStringLiteral("COMMIT"), + QStringLiteral("PRAGMA defer_foreign_keys=OFF")}; +} + +// END Sqlite + +// BEGIN PostgreSQL + +DbInitializerPostgreSql::DbInitializerPostgreSql(const QSqlDatabase &database) + : DbInitializer(database) +{ +} + +bool DbInitializerPostgreSql::hasForeignKeyConstraints() const +{ + return true; +} + +QString DbInitializerPostgreSql::sqlType(const ColumnDescription &col, int size) const +{ + if (col.type == QLatin1String("qint64")) { + return QStringLiteral("int8"); + } else if (col.type == QLatin1String("QByteArray")) { + return QStringLiteral("BYTEA"); + } else if (col.isEnum) { + return QStringLiteral("SMALLINT"); + } + + return DbInitializer::sqlType(col, size); +} + +QString DbInitializerPostgreSql::buildCreateTableStatement(const TableDescription &tableDescription) const +{ + QStringList columns; + columns.reserve(tableDescription.columns.size() + 1); + + Q_FOREACH (const ColumnDescription &columnDescription, tableDescription.columns) { + columns.append(buildColumnStatement(columnDescription, tableDescription)); + } + + if (tableDescription.primaryKeyColumnCount() > 1) { + columns.push_back(buildPrimaryKeyStatement(tableDescription)); + } + + return QStringLiteral("CREATE TABLE %1 (%2)").arg(tableDescription.name, columns.join(QStringLiteral(", "))); +} + +QString DbInitializerPostgreSql::buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const +{ + QString column = columnDescription.name + QLatin1Char(' '); + + if (columnDescription.isAutoIncrement) { + column += QLatin1String("SERIAL"); + } else { + column += sqlType(columnDescription, columnDescription.size); + } + + if (columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1) { + column += QLatin1String(" PRIMARY KEY"); + } else if (columnDescription.isUnique) { + column += QLatin1String(" UNIQUE"); + } + + if (!columnDescription.allowNull && !(columnDescription.isPrimaryKey && tableDescription.primaryKeyColumnCount() == 1)) { + column += QLatin1String(" NOT NULL"); + } + + if (!columnDescription.defaultValue.isEmpty()) { + const QString defaultValue = sqlValue(columnDescription, columnDescription.defaultValue); + + if (!defaultValue.isEmpty()) { + column += QStringLiteral(" DEFAULT %1").arg(defaultValue); + } + } + + return column; +} + +QString DbInitializerPostgreSql::buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const +{ + QStringList keys; + QStringList values; + for (auto it = dataDescription.data.cbegin(), end = dataDescription.data.cend(); it != end; ++it) { + keys.push_back(it.key()); + values.push_back(it.value()); + } + + return QStringLiteral("INSERT INTO %1 (%2) VALUES (%3)").arg(tableDescription.name, keys.join(QLatin1Char(',')), values.join(QLatin1Char(','))); +} + +QStringList DbInitializerPostgreSql::buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const +{ + // constraints must have name in PostgreSQL + const QString constraintName = table.name + column.name + QLatin1String("_") + column.refTable + column.refColumn + QLatin1String("_fk"); + return {QStringLiteral("ALTER TABLE %1 ADD CONSTRAINT %2 FOREIGN KEY (%3) REFERENCES %4Table(%5) %6 DEFERRABLE INITIALLY DEFERRED") + .arg(table.name, constraintName, column.name, column.refTable, column.refColumn, buildReferentialAction(column.onUpdate, column.onDelete))}; +} + +QStringList DbInitializerPostgreSql::buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const +{ + return {QStringLiteral("ALTER TABLE %1 DROP CONSTRAINT %2").arg(table.name, fk.name)}; +} + +// END PostgreSQL diff --git a/src/server/storage/dbinitializer_p.h b/src/server/storage/dbinitializer_p.h new file mode 100644 index 0000000..d8001fd --- /dev/null +++ b/src/server/storage/dbinitializer_p.h @@ -0,0 +1,71 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "storage/dbinitializer.h" + +namespace Akonadi +{ +namespace Server +{ +class DbInitializerMySql : public DbInitializer +{ +public: + explicit DbInitializerMySql(const QSqlDatabase &database); + + bool hasForeignKeyConstraints() const override; + +protected: + QString sqlType(const ColumnDescription &col, int size) const override; + + QString buildCreateTableStatement(const TableDescription &tableDescription) const override; + QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const override; + QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const override; + QStringList buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const override; + QStringList buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const override; +}; + +class DbInitializerSqlite : public DbInitializer +{ +public: + explicit DbInitializerSqlite(const QSqlDatabase &database); + + bool hasForeignKeyConstraints() const override; + +protected: + QString buildCreateTableStatement(const TableDescription &tableDescription) const override; + QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const override; + QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const override; + QString sqlValue(const ColumnDescription &col, const QString &value) const override; + QStringList buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const override; + QStringList buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const override; + +private: + QStringList buildUpdateForeignKeyConstraintsStatements(const TableDescription &table) const; +}; + +class DbInitializerPostgreSql : public DbInitializer +{ +public: + explicit DbInitializerPostgreSql(const QSqlDatabase &database); + + bool hasForeignKeyConstraints() const override; + +protected: + QString sqlType(const ColumnDescription &col, int size) const override; + + QString buildCreateTableStatement(const TableDescription &tableDescription) const override; + QString buildColumnStatement(const ColumnDescription &columnDescription, const TableDescription &tableDescription) const override; + QString buildInsertValuesStatement(const TableDescription &tableDescription, const DataDescription &dataDescription) const override; + QStringList buildAddForeignKeyConstraintStatements(const TableDescription &table, const ColumnDescription &column) const override; + QStringList buildRemoveForeignKeyConstraintStatements(const DbIntrospector::ForeignKey &fk, const TableDescription &table) const override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbintrospector.cpp b/src/server/storage/dbintrospector.cpp new file mode 100644 index 0000000..e73c4db --- /dev/null +++ b/src/server/storage/dbintrospector.cpp @@ -0,0 +1,107 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbintrospector.h" +#include "akonadiserver_debug.h" +#include "dbexception.h" +#include "dbintrospector_impl.h" +#include "dbtype.h" +#include "querybuilder.h" + +#include +#include +#include + +using namespace Akonadi::Server; + +DbIntrospector::Ptr DbIntrospector::createInstance(const QSqlDatabase &database) +{ + switch (DbType::type(database)) { + case DbType::MySQL: + return Ptr(new DbIntrospectorMySql(database)); + case DbType::Sqlite: + return Ptr(new DbIntrospectorSqlite(database)); + case DbType::PostgreSQL: + return Ptr(new DbIntrospectorPostgreSql(database)); + case DbType::Unknown: + break; + } + qCCritical(AKONADISERVER_LOG) << database.driverName() << "backend not supported"; + return Ptr(); +} + +DbIntrospector::DbIntrospector(const QSqlDatabase &database) + : m_database(database) +{ +} + +DbIntrospector::~DbIntrospector() +{ +} + +bool DbIntrospector::hasTable(const QString &tableName) +{ + return m_database.tables().contains(tableName, Qt::CaseInsensitive); +} + +bool DbIntrospector::hasIndex(const QString &tableName, const QString &indexName) +{ + QSqlQuery query(m_database); + if (!query.exec(hasIndexQuery(tableName, indexName))) { + throw DbException(query, "Failed to query index"); + } + return query.next(); +} + +bool DbIntrospector::hasColumn(const QString &tableName, const QString &columnName) +{ + QStringList columns = m_columnCache.value(tableName); + + if (columns.isEmpty()) { + // QPSQL requires the name to be lower case, but it breaks compatibility with existing + // tables with other drivers (see BKO#409234). Yay for abstraction... + const auto name = (DbType::type(m_database) == DbType::PostgreSQL) ? tableName.toLower() : tableName; + const QSqlRecord table = m_database.record(name); + const int numTables = table.count(); + columns.reserve(numTables); + for (int i = 0; i < numTables; ++i) { + const QSqlField column = table.field(i); + columns.push_back(column.name().toLower()); + } + + m_columnCache.insert(tableName, columns); + } + + return columns.contains(columnName.toLower()); +} + +bool DbIntrospector::isTableEmpty(const QString &tableName) +{ + QueryBuilder queryBuilder(tableName, QueryBuilder::Select); + queryBuilder.addColumn(QStringLiteral("*")); + queryBuilder.setLimit(1); + if (!queryBuilder.exec()) { + throw DbException(queryBuilder.query(), "Unable to retrieve data from table."); + } + + QSqlQuery query = queryBuilder.query(); + return (query.size() == 0 || !query.first()); +} + +QVector DbIntrospector::foreignKeyConstraints(const QString &tableName) +{ + Q_UNUSED(tableName) + return QVector(); +} + +QString DbIntrospector::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + Q_UNUSED(tableName) + Q_UNUSED(indexName) + qCCritical(AKONADISERVER_LOG) << "Implement index support for your database!"; + return QString(); +} diff --git a/src/server/storage/dbintrospector.h b/src/server/storage/dbintrospector.h new file mode 100644 index 0000000..9a8249b --- /dev/null +++ b/src/server/storage/dbintrospector.h @@ -0,0 +1,112 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include +#include + +class DbIntrospectorTest; + +namespace Akonadi +{ +namespace Server +{ +/** + * Methods for introspecting the current state of a database schema. + * I.e. this is about the structure of a database, not its content. + */ +class DbIntrospector +{ +public: + using Ptr = QSharedPointer; + + /** A structure describing an existing foreign key. */ + class ForeignKey + { + public: + QString name; + QString column; + QString refTable; + QString refColumn; + QString onUpdate; // TODO use same enum as DbInitializer + QString onDelete; // dito + }; + + /** + * Returns an introspector instance for a given database. + */ + static DbIntrospector::Ptr createInstance(const QSqlDatabase &database); + + virtual ~DbIntrospector(); + + /** + * Returns @c true if table @p tableName exists. + * The default implementation relies on QSqlDatabase::tables(). Usually this + * does not need to be reimplemented. + */ + virtual bool hasTable(const QString &tableName); + + /** + * Returns @c true of the given table has an index with the given name. + * The default implementation performs the query returned by hasIndexQuery(). + * @see hasIndexQuery() + * @throws DbException on database errors. + */ + virtual bool hasIndex(const QString &tableName, const QString &indexName); + + /** + * Check whether table @p tableName has a column named @p columnName. + * The default implementation should work with all backends. + */ + virtual bool hasColumn(const QString &tableName, const QString &columnName); + + /** + * Check whether table @p tableName is empty, ie. does not contain any rows. + * The default implementation should work for all backends. + * @throws DbException on database errors. + */ + virtual bool isTableEmpty(const QString &tableName); + + /** + * Returns the foreign key constraints on table @p tableName. + * The default implementation returns an empty list, so any backend supporting + * referential integrity should reimplement this. + */ + virtual QVector foreignKeyConstraints(const QString &tableName); + +protected: + /** + * Creates a new database introspector, call from subclass. + * + * @param database The database to introspect. + */ + DbIntrospector(const QSqlDatabase &database); + + /** + * Returns a query string to determine if @p tableName has an index @p indexName. + * The query is expected to have one boolean result row/column. + * This is used by the default implementation of hasIndex() only, thus reimplmentation + * is not necessary if you reimplement hasIndex() + * The default implementation asserts. + */ + virtual QString hasIndexQuery(const QString &tableName, const QString &indexName); + + /** The database connection we are introspecting. */ + QSqlDatabase m_database; + +private: + Q_DISABLE_COPY_MOVE(DbIntrospector) + + friend class ::DbIntrospectorTest; + QHash m_columnCache; // avoids extra db roundtrips +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbintrospector_impl.cpp b/src/server/storage/dbintrospector_impl.cpp new file mode 100644 index 0000000..e6ee558 --- /dev/null +++ b/src/server/storage/dbintrospector_impl.cpp @@ -0,0 +1,199 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbintrospector_impl.h" +#include "datastore.h" +#include "dbexception.h" +#include "querybuilder.h" + +#include "akonadiserver_debug.h" + +using namespace Akonadi::Server; + +// BEGIN MySql + +DbIntrospectorMySql::DbIntrospectorMySql(const QSqlDatabase &database) + : DbIntrospector(database) +{ +} + +QString DbIntrospectorMySql::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + return QStringLiteral("SHOW INDEXES FROM %1 WHERE `Key_name` = '%2'").arg(tableName, indexName); +} + +QVector DbIntrospectorMySql::foreignKeyConstraints(const QString &tableName) +{ + QueryBuilder qb(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS"), QueryBuilder::Select); + qb.addJoin(QueryBuilder::InnerJoin, + QStringLiteral("information_schema.KEY_COLUMN_USAGE"), + QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME"), + QStringLiteral("information_schema.KEY_COLUMN_USAGE.CONSTRAINT_NAME")); + qb.addColumn(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME")); + qb.addColumn(QStringLiteral("information_schema.KEY_COLUMN_USAGE.COLUMN_NAME")); + qb.addColumn(QStringLiteral("information_schema.KEY_COLUMN_USAGE.REFERENCED_TABLE_NAME")); + qb.addColumn(QStringLiteral("information_schema.KEY_COLUMN_USAGE.REFERENCED_COLUMN_NAME")); + qb.addColumn(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.UPDATE_RULE")); + qb.addColumn(QStringLiteral("information_schema.REFERENTIAL_CONSTRAINTS.DELETE_RULE")); + + qb.addValueCondition(QStringLiteral("information_schema.KEY_COLUMN_USAGE.TABLE_SCHEMA"), Query::Equals, m_database.databaseName()); + qb.addValueCondition(QStringLiteral("information_schema.KEY_COLUMN_USAGE.TABLE_NAME"), Query::Equals, tableName); + + if (!qb.exec()) { + throw DbException(qb.query()); + } + + QVector result; + while (qb.query().next()) { + ForeignKey fk; + fk.name = qb.query().value(0).toString(); + fk.column = qb.query().value(1).toString(); + fk.refTable = qb.query().value(2).toString(); + fk.refColumn = qb.query().value(3).toString(); + fk.onUpdate = qb.query().value(4).toString(); + fk.onDelete = qb.query().value(5).toString(); + result.push_back(fk); + } + qb.query().finish(); + + return result; +} + +// END MySql + +// BEGIN Sqlite + +DbIntrospectorSqlite::DbIntrospectorSqlite(const QSqlDatabase &database) + : DbIntrospector(database) +{ +} + +QVector DbIntrospectorSqlite::foreignKeyConstraints(const QString &tableName) +{ + QSqlQuery query(DataStore::self()->database()); + if (!query.exec(QStringLiteral("PRAGMA foreign_key_list(%1)").arg(tableName))) { + throw DbException(query); + } + + QVector result; + while (query.next()) { + ForeignKey fk; + fk.column = query.value(3).toString(); + fk.refTable = query.value(2).toString(); + fk.refColumn = query.value(4).toString(); + fk.onUpdate = query.value(5).toString(); + fk.onDelete = query.value(6).toString(); + fk.name = tableName + fk.column + QLatin1Char('_') + fk.refTable + fk.refColumn + QStringLiteral("_fk"); + result.push_back(fk); + } + + return result; +} + +QString DbIntrospectorSqlite::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + return QStringLiteral("SELECT * FROM sqlite_master WHERE type='index' AND tbl_name='%1' AND name='%2';").arg(tableName, indexName); +} + +// END Sqlite + +// BEGIN PostgreSql + +DbIntrospectorPostgreSql::DbIntrospectorPostgreSql(const QSqlDatabase &database) + : DbIntrospector(database) +{ +} + +QVector DbIntrospectorPostgreSql::foreignKeyConstraints(const QString &tableName) +{ +#define TABLE_CONSTRAINTS "information_schema.table_constraints" +#define KEY_COLUMN_USAGE "information_schema.key_column_usage" +#define REFERENTIAL_CONSTRAINTS "information_schema.referential_constraints" +#define CONSTRAINT_COLUMN_USAGE "information_schema.constraint_column_usage" + + Query::Condition keyColumnUsageCondition(Query::And); + keyColumnUsageCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_catalog"), + Query::Equals, + QStringLiteral(KEY_COLUMN_USAGE ".constraint_catalog")); + keyColumnUsageCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_schema"), + Query::Equals, + QStringLiteral(KEY_COLUMN_USAGE ".constraint_schema")); + keyColumnUsageCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_name"), + Query::Equals, + QStringLiteral(KEY_COLUMN_USAGE ".constraint_name")); + + Query::Condition referentialConstraintsCondition(Query::And); + referentialConstraintsCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_catalog"), + Query::Equals, + QStringLiteral(REFERENTIAL_CONSTRAINTS ".constraint_catalog")); + referentialConstraintsCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_schema"), + Query::Equals, + QStringLiteral(REFERENTIAL_CONSTRAINTS ".constraint_schema")); + referentialConstraintsCondition.addColumnCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_name"), + Query::Equals, + QStringLiteral(REFERENTIAL_CONSTRAINTS ".constraint_name")); + + Query::Condition constraintColumnUsageCondition(Query::And); + constraintColumnUsageCondition.addColumnCondition(QStringLiteral(REFERENTIAL_CONSTRAINTS ".unique_constraint_catalog"), + Query::Equals, + QStringLiteral(CONSTRAINT_COLUMN_USAGE ".constraint_catalog")); + constraintColumnUsageCondition.addColumnCondition(QStringLiteral(REFERENTIAL_CONSTRAINTS ".unique_constraint_schema"), + Query::Equals, + QStringLiteral(CONSTRAINT_COLUMN_USAGE ".constraint_schema")); + constraintColumnUsageCondition.addColumnCondition(QStringLiteral(REFERENTIAL_CONSTRAINTS ".unique_constraint_name"), + Query::Equals, + QStringLiteral(CONSTRAINT_COLUMN_USAGE ".constraint_name")); + + QueryBuilder qb(QStringLiteral(TABLE_CONSTRAINTS), QueryBuilder::Select); + qb.addColumn(QStringLiteral(TABLE_CONSTRAINTS ".constraint_name")); + qb.addColumn(QStringLiteral(KEY_COLUMN_USAGE ".column_name")); + qb.addColumn(QStringLiteral(CONSTRAINT_COLUMN_USAGE ".table_name AS referenced_table")); + qb.addColumn(QStringLiteral(CONSTRAINT_COLUMN_USAGE ".column_name AS referenced_column")); + qb.addColumn(QStringLiteral(REFERENTIAL_CONSTRAINTS ".update_rule")); + qb.addColumn(QStringLiteral(REFERENTIAL_CONSTRAINTS ".delete_rule")); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral(KEY_COLUMN_USAGE), keyColumnUsageCondition); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral(REFERENTIAL_CONSTRAINTS), referentialConstraintsCondition); + qb.addJoin(QueryBuilder::LeftJoin, QStringLiteral(CONSTRAINT_COLUMN_USAGE), constraintColumnUsageCondition); + qb.addValueCondition(QStringLiteral(TABLE_CONSTRAINTS ".constraint_type"), Query::Equals, QLatin1String("FOREIGN KEY")); + qb.addValueCondition(QStringLiteral(TABLE_CONSTRAINTS ".table_name"), Query::Equals, tableName.toLower()); + +#undef TABLE_CONSTRAINTS +#undef KEY_COLUMN_USAGE +#undef REFERENTIAL_CONSTRAINTS +#undef CONSTRAINT_COLUMN_USAGE + + if (!qb.exec()) { + throw DbException(qb.query()); + } + + QVector result; + while (qb.query().next()) { + ForeignKey fk; + fk.name = qb.query().value(0).toString(); + fk.column = qb.query().value(1).toString(); + fk.refTable = qb.query().value(2).toString(); + fk.refColumn = qb.query().value(3).toString(); + fk.onUpdate = qb.query().value(4).toString(); + fk.onDelete = qb.query().value(5).toString(); + result.push_back(fk); + } + qb.query().finish(); + + return result; +} + +QString DbIntrospectorPostgreSql::hasIndexQuery(const QString &tableName, const QString &indexName) +{ + QString query = QStringLiteral("SELECT indexname FROM pg_catalog.pg_indexes"); + query += QStringLiteral(" WHERE tablename ilike '%1'").arg(tableName); + query += QStringLiteral(" AND indexname ilike '%1'").arg(indexName); + query += QStringLiteral(" UNION SELECT conname FROM pg_catalog.pg_constraint "); + query += QStringLiteral(" WHERE conname ilike '%1'").arg(indexName); + return query; +} + +// END PostgreSql diff --git a/src/server/storage/dbintrospector_impl.h b/src/server/storage/dbintrospector_impl.h new file mode 100644 index 0000000..822e25d --- /dev/null +++ b/src/server/storage/dbintrospector_impl.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "dbintrospector.h" + +namespace Akonadi +{ +namespace Server +{ +class DbIntrospectorMySql : public DbIntrospector +{ +public: + explicit DbIntrospectorMySql(const QSqlDatabase &database); + QVector foreignKeyConstraints(const QString &tableName) override; + QString hasIndexQuery(const QString &tableName, const QString &indexName) override; +}; + +class DbIntrospectorSqlite : public DbIntrospector +{ +public: + explicit DbIntrospectorSqlite(const QSqlDatabase &database); + QVector foreignKeyConstraints(const QString &tableName) override; + QString hasIndexQuery(const QString &tableName, const QString &indexName) override; +}; + +class DbIntrospectorPostgreSql : public DbIntrospector +{ +public: + explicit DbIntrospectorPostgreSql(const QSqlDatabase &database); + QVector foreignKeyConstraints(const QString &tableName) override; + QString hasIndexQuery(const QString &tableName, const QString &indexName) override; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbtype.cpp b/src/server/storage/dbtype.cpp new file mode 100644 index 0000000..6b397ab --- /dev/null +++ b/src/server/storage/dbtype.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbtype.h" + +using namespace Akonadi::Server; + +DbType::Type DbType::type(const QSqlDatabase &db) +{ + return typeForDriverName(db.driverName()); +} + +DbType::Type DbType::typeForDriverName(const QString &driverName) +{ + if (driverName.startsWith(QLatin1String("QMYSQL"))) { + return MySQL; + } + if (driverName == QLatin1String("QPSQL")) { + return PostgreSQL; + } + if (driverName.startsWith(QLatin1String("QSQLITE"))) { + return Sqlite; + } + return Unknown; +} + +bool DbType::isSystemSQLite(const QSqlDatabase &db) +{ + return db.driverName() == QLatin1String("QSQLITE"); +} diff --git a/src/server/storage/dbtype.h b/src/server/storage/dbtype.h new file mode 100644 index 0000000..10425f7 --- /dev/null +++ b/src/server/storage/dbtype.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +/** Helper methods for checking the database system we are dealing with. */ +namespace DbType +{ +/** Supported database types. */ +enum Type { + Unknown, + Sqlite, + MySQL, + PostgreSQL, +}; + +/** Returns the type of the given database object. */ +Type type(const QSqlDatabase &db); + +/** Returns the type for the given driver name. */ +Type typeForDriverName(const QString &driverName); + +/** Returns true when using QSQLITE driver shipped with Qt, FALSE otherwise */ +bool isSystemSQLite(const QSqlDatabase &db); + +} // namespace DbType +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/dbupdate.xml b/src/server/storage/dbupdate.xml new file mode 100644 index 0000000..921ef26 --- /dev/null +++ b/src/server/storage/dbupdate.xml @@ -0,0 +1,342 @@ + + + + + + + + + ALTER TABLE LocationTable DROP COLUMN existCount; + ALTER TABLE LocationTable DROP COLUMN recentCount; + ALTER TABLE LocationTable DROP COLUMN unseenCount; + ALTER TABLE LocationTable DROP COLUMN firstUnseen; + + + + UPDATE LocationTable SET subscribed = true; + + + + ALTER TABLE LocationTable DROP COLUMN cachePolicyId; + ALTER TABLE ResourceTable DROP COLUMN cachePolicyId; + DROP TABLE CachePolicyTable; + + + + UPDATE PartTable SET name = 'PLD:ENVELOPE' WHERE name = 'ENVELOPE'; + UPDATE PartTable SET name = 'PLD:RFC822' WHERE name = 'RFC822'; + UPDATE PartTable SET name = 'PLD:HEAD' WHERE name = 'HEAD'; + UPDATE PartTable SET name = concat( 'ATR:', name ) WHERE substr( name, 1, 4 ) != 'PLD:'; + + + + + DROP TABLE CollectionTable; + ALTER TABLE LocationTable RENAME TO CollectionTable; + ALTER TABLE PimItemTable DROP COLUMN collectionId; + ALTER TABLE PimItemTable CHANGE locationId collectionId BIGINT; + DROP TABLE CollectionAttributeTable; + ALTER TABLE LocationAttributeTable CHANGE locationId collectionId BIGINT; + ALTER TABLE LocationAttributeTable RENAME TO CollectionAttributeTable; + DROP TABLE CollectionMimeTypeRelation; + ALTER TABLE LocationMimeTypeRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationMimeTypeRelation RENAME TO CollectionMimeTypeRelation; + DROP TABLE CollectionPimItemRelation; + ALTER TABLE LocationPimItemRelation CHANGE Location_Id Collection_Id BIGINT NOT NULL DEFAULT '0'; + ALTER TABLE LocationPimItemRelation RENAME TO CollectionPimItemRelation; + + + + ALTER TABLE PartTable CHANGE datasize datasize BIGINT; + + + + UPDATE CollectionTable SET parentId = NULL WHERE parentId = 0; + ALTER TABLE CollectionTable CHANGE parentId parentId BIGINT DEFAULT NULL; + + + + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_nepomuktag_resource'; + UPDATE ResourceTable SET isVirtual = true WHERE name = 'akonadi_search_resource'; + + + + UPDATE CollectionTable SET queryString = remoteId WHERE resourceId = 1 AND parentId IS NOT NULL; + UPDATE CollectionTable SET queryLanguage = 'SPARQL' WHERE resourceId = 1 AND parentId IS NOT NULL; + + + + ALTER TABLE CollectionAttributeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionMimeTypeRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionPimItemRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE CollectionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE FlagTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE MimeTypeTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PartTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimItemFlagRelation CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE PimitemTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE ResourceTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + ALTER TABLE SchemaVersionTable CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; + + + + + ALTER TABLE ResourceTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE name name VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryString queryString VARCHAR(255) BINARY; + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARCHAR(255) BINARY; + ALTER TABLE MimeTypeTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARCHAR(255) BINARY; + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARCHAR(255) BINARY; + ALTER TABLE FlagTable CHANGE name name VARCHAR(255) BINARY UNIQUE; + ALTER TABLE PartTable CHANGE name name VARCHAR(255) BINARY; + + + + + ALTER TABLE ResourceTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE CollectionTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE CollectionTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE CollectionTable CHANGE name name VARBINARY(255); + ALTER TABLE CollectionTable CHANGE cachePolicyLocalParts cachePolicyLocalParts VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(255); + ALTER TABLE CollectionTable CHANGE queryLanguage queryLanguage VARBINARY(255); + ALTER TABLE MimeTypeTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PimItemTable CHANGE remoteId remoteId VARBINARY(255); + ALTER TABLE PimItemTable CHANGE remoteRevision remoteRevision VARBINARY(255); + ALTER TABLE FlagTable CHANGE name name VARBINARY(255) UNIQUE; + ALTER TABLE PartTable CHANGE name name VARBINARY(255); + + + UPDATE PimItemFlagRelation SET Flag_id=(SELECT id FROM FlagTable WHERE name='\\SEEN') WHERE Flag_id=(SELECT id FROM FlagTable WHERE name='\\Seen'); + DELETE FROM FlagTable WHERE name='\\Seen'; + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(1024); + + + + + ALTER TABLE CollectionTable CHANGE queryString queryString VARBINARY(32768); + + + + + ALTER TABLE PimItemFlagRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + ALTER TABLE PimItemFlagRelation CHANGE Flag_id Flag_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionMimeTypeRelation CHANGE MimeType_id MimeType_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE Collection_id Collection_id BIGINT NOT NULL + ALTER TABLE CollectionPimItemRelation CHANGE PimItem_id PimItem_id BIGINT NOT NULL + + + + UPDATE CollectionTable SET isVirtual = true WHERE resourceId IN (SELECT id FROM ResourceTable WHERE isVirtual = true) + UPDATE CollectionTable SET isVirtual = 1 WHERE resourceId IN (SELECT id FROM ResourceTable WHERE isVirtual = 1) + + + + ALTER TABLE CollectionTable ALTER remoteId TYPE text USING convert_from(remoteId,'utf8'); + ALTER TABLE CollectionTable ALTER remoteRevision TYPE text USING convert_from(remoteRevision,'utf8'); + ALTER TABLE CollectionTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE CollectionTable ALTER cachePolicyLocalParts TYPE text USING convert_from(cachePolicyLocalParts,'utf8'); + ALTER TABLE CollectionTable ALTER queryString TYPE text USING convert_from(queryString,'utf8'); + ALTER TABLE CollectionTable ALTER queryLanguage TYPE text USING convert_from(queryLanguage,'utf8'); + ALTER TABLE FlagTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE MimeTypeTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE PartTable ALTER name TYPE text USING convert_from(name,'utf8'); + ALTER TABLE PimItemTable ALTER remoteId TYPE text USING convert_from(remoteId,'utf8'); + ALTER TABLE PimItemTable ALTER remoteRevision TYPE text USING convert_from(remoteRevision,'utf8'); + ALTER TABLE ResourceTable ALTER name TYPE text USING convert_from(name,'utf8'); + + + + + + + + + UPDATE CollectionTable SET queryAttributes = 'QUERYLANGUAGE SPARQL' WHERE queryLanguage = 'SPARQL'; + ALTER TABLE CollectionTable DROP COLUMN queryLanguage; + + + + UPDATE CollectionTable SET enabled = subscribed; + ALTER TABLE CollectionTable DROP COLUMN subscribed; + + + + + + DELETE FROM PimItemFlagRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM PimItemFlagRelation + LEFT JOIN PimItemTable ON PimItemFlagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + DELETE FROM PimItemFlagRelation WHERE flag_id IN ( + SELECT flag_id FROM PimItemFlagRelation + LEFT JOIN FlagTable ON PimItemFlagRelation.flag_id = FlagTable.id + WHERE FlagTable.id IS NULL) + + + DELETE FROM PimItemTagRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM PimItemTagRelation + LEFT JOIN PimItemTable ON PimItemTagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + DELETE FROM PimItemTagRelation WHERE tag_id IN ( + SELECT tag_id FROM PimItemTagRelation + LEFT JOIN TagTable ON PimItemTagRelation.tag_id = TagTable.id + WHERE TagTable.id IS NULL) + + + DELETE FROM CollectionMimeTypeRelation WHERE collection_id IN ( + SELECT collection_id FROM CollectionMimeTypeRelation + LEFT JOIN CollectionTable ON CollectionMimeTypeRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) + + DELETE FROM CollectionMimeTypeRelation WHERE mimeType_id IN ( + SELECT mimeType_id FROM CollectionMimeTypeRelation + LEFT JOIN MimeTypeTable ON CollectionMimeTypeRelation.mimeType_id = MimeTypeTable.id + WHERE MimeTypeTable.id IS NULL) + + + DELETE FROM CollectionPimItemRelation WHERE collection_id IN ( + SELECT collection_id FROM CollectionPimItemRelation + LEFT JOIN CollectionTable ON CollectionPimItemRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) + + DELETE FROM CollectionPimItemRelation WHERE pimItem_id IN ( + SELECT pimItem_id FROM CollectionPimItemRelation + LEFT JOIN PimItemTable ON CollectionPimItemRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) + + + + + + DELETE FROM PimItemFlagRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM PimItemFlagRelation + LEFT JOIN PimItemTable ON PimItemFlagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + DELETE FROM PimItemFlagRelation WHERE flag_id IN ( + SELECT id FROM ( + SELECT flag_id AS id FROM PimItemFlagRelation + LEFT JOIN FlagTable ON PimItemFlagRelation.flag_id = FlagTable.id + WHERE FlagTable.id IS NULL) x) + + + DELETE FROM PimItemTagRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM PimItemTagRelation + LEFT JOIN PimItemTable ON PimItemTagRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + DELETE FROM PimItemTagRelation WHERE tag_id IN ( + SELECT id FROM ( + SELECT tag_id AS id FROM PimItemTagRelation + LEFT JOIN TagTable ON PimItemTagRelation.tag_id = TagTable.id + WHERE TagTable.id IS NULL) x) + + + DELETE FROM CollectionMimeTypeRelation WHERE collection_id IN ( + SELECT id FROM ( + SELECT collection_id AS id FROM CollectionMimeTypeRelation + LEFT JOIN CollectionTable ON CollectionMimeTypeRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) x) + + DELETE FROM CollectionMimeTypeRelation WHERE mimeType_id IN ( + SELECT id FROM ( + SELECT mimeType_id AS id FROM CollectionMimeTypeRelation + LEFT JOIN MimeTypeTable ON CollectionMimeTypeRelation.mimeType_id = MimeTypeTable.id + WHERE MimeTypeTable.id IS NULL) x) + + + DELETE FROM CollectionPimItemRelation WHERE collection_id IN ( + SELECT id FROM ( + SELECT collection_id AS id FROM CollectionPimItemRelation + LEFT JOIN CollectionTable ON CollectionPimItemRelation.collection_id = CollectionTable.id + WHERE CollectionTable.id IS NULL) x) + + DELETE FROM CollectionPimItemRelation WHERE pimItem_id IN ( + SELECT id FROM ( + SELECT pimItem_id AS id FROM CollectionPimItemRelation + LEFT JOIN PimItemTable ON CollectionPimItemRelation.pimItem_id = PimItemTable.id + WHERE PimItemTable.id IS NULL) x) + + + + SELECT setval('tagtypetable_id_seq', (SELECT max(id) FROM TagTypeTable)) + SELECT setval('relationtypetable_id_seq', (SELECT max(id) FROM RelationTypeTable)) + + + + UPDATE PartTable SET storage = external; + ALTER TABLE PartTable DROP COLUMN external; + + UPDATE PartTable SET storage = cast(external as integer); + ALTER TABLE PartTable DROP COLUMN external; + + + + + + + + + + UPDATE TagRemoteIdResourceRelationTable SET remoteId = printf('%s', remoteId) + + + + ALTER TABLE TagTable MODIFY COLUMN parentId BIGINT(20); + ALTER TABLE TagTable ALTER COLUMN parentId DROP DEFAULT; + + + + + + ALTER TABLE PimItemTable ROW_FORMAT=DYNAMIC + ALTER TABLE PimItemTable MODIFY COLUMN remoteId VARBINARY(1024) + + diff --git a/src/server/storage/dbupdate.xsd b/src/server/storage/dbupdate.xsd new file mode 100644 index 0000000..aace749 --- /dev/null +++ b/src/server/storage/dbupdate.xsd @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/server/storage/dbupdater.cpp b/src/server/storage/dbupdater.cpp new file mode 100644 index 0000000..40d0fea --- /dev/null +++ b/src/server/storage/dbupdater.cpp @@ -0,0 +1,570 @@ +/* + SPDX-FileCopyrightText: 2007-2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "dbupdater.h" +#include "akonadischema.h" +#include "akonadiserver_debug.h" +#include "datastore.h" +#include "dbconfig.h" +#include "dbinitializer_p.h" +#include "dbintrospector.h" +#include "dbtype.h" +#include "entities.h" +#include "querybuilder.h" +#include "selectquerybuilder.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +DbUpdater::DbUpdater(const QSqlDatabase &database, const QString &filename) + : m_database(database) + , m_filename(filename) +{ +} + +bool DbUpdater::run() +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + + // TODO error handling + auto currentVersion = SchemaVersion::retrieveAll().at(0); + + UpdateSet::Map updates; + + if (!parseUpdateSets(currentVersion.version(), updates)) { + return false; + } + + if (updates.isEmpty()) { + return true; + } + + // indicate clients this might take a while + // we can ignore unregistration in error cases, that'll kill the server anyway + if (!QDBusConnection::sessionBus().registerService(DBus::serviceName(DBus::UpgradeIndicator))) { + qCCritical(AKONADISERVER_LOG) << "Unable to connect to dbus service: " << QDBusConnection::sessionBus().lastError().message(); + } + + // QMap is sorted, so we should be replaying the changes in correct order + for (QMap::ConstIterator it = updates.constBegin(); it != updates.constEnd(); ++it) { + Q_ASSERT(it.key() > currentVersion.version()); + qCDebug(AKONADISERVER_LOG) << "DbUpdater: update to version:" << it.key() << " mandatory:" << it.value().abortOnFailure; + + bool success = false; + bool hasTransaction = false; + if (it.value().complex) { // complex update + const QString methodName = QStringLiteral("complexUpdate_%1()").arg(it.value().version); + const int index = metaObject()->indexOfMethod(methodName.toLatin1().constData()); + if (index == -1) { + success = false; + qCCritical(AKONADISERVER_LOG) << "Update to version" << it.value().version << "marked as complex, but no implementation is available"; + } else { + const QMetaMethod method = metaObject()->method(index); + method.invoke(this, Q_RETURN_ARG(bool, success)); + if (!success) { + qCCritical(AKONADISERVER_LOG) << "Update failed"; + } + } + } else { // regular update + success = m_database.transaction(); + if (success) { + hasTransaction = true; + const QStringList statements = it.value().statements; + for (const QString &statement : statements) { + QSqlQuery query(m_database); + success = query.exec(statement); + if (!success) { + qCCritical(AKONADISERVER_LOG) << "DBUpdater: query error:" << query.lastError().text() << m_database.lastError().text(); + qCCritical(AKONADISERVER_LOG) << "Query was: " << statement; + qCCritical(AKONADISERVER_LOG) << "Target version was: " << it.key(); + qCCritical(AKONADISERVER_LOG) << "Mandatory: " << it.value().abortOnFailure; + } + } + } + } + + if (success) { + currentVersion.setVersion(it.key()); + success = currentVersion.update(); + } + + if (!success || (hasTransaction && !m_database.commit())) { + qCCritical(AKONADISERVER_LOG) << "Failed to commit transaction for database update"; + if (hasTransaction) { + m_database.rollback(); + } + if (it.value().abortOnFailure) { + return false; + } + } + } + + QDBusConnection::sessionBus().unregisterService(DBus::serviceName(DBus::UpgradeIndicator)); + return true; +} + +bool DbUpdater::parseUpdateSets(int currentVersion, UpdateSet::Map &updates) const +{ + QFile file(m_filename); + if (!file.open(QIODevice::ReadOnly)) { + qCCritical(AKONADISERVER_LOG) << "Unable to open update description file" << m_filename; + return false; + } + + QDomDocument document; + + QString errorMsg; + int line; + int column; + if (!document.setContent(&file, &errorMsg, &line, &column)) { + qCCritical(AKONADISERVER_LOG) << "Unable to parse update description file" << m_filename << ":" << errorMsg << "at line" << line << "column" << column; + return false; + } + + const QDomElement documentElement = document.documentElement(); + if (documentElement.tagName() != QLatin1String("updates")) { + qCCritical(AKONADISERVER_LOG) << "Invalid update description file format"; + return false; + } + + // iterate over the xml document and extract update information into an UpdateSet + QDomElement updateElement = documentElement.firstChildElement(); + while (!updateElement.isNull()) { + if (updateElement.tagName() == QLatin1String("update")) { + const int version = updateElement.attribute(QStringLiteral("version"), QStringLiteral("-1")).toInt(); + if (version <= 0) { + qCCritical(AKONADISERVER_LOG) << "Invalid version attribute in database update description"; + return false; + } + + if (updates.contains(version)) { + qCCritical(AKONADISERVER_LOG) << "Duplicate version attribute in database update description"; + return false; + } + + if (version <= currentVersion) { + qCDebug(AKONADISERVER_LOG) << "skipping update" << version; + } else { + UpdateSet updateSet; + updateSet.version = version; + updateSet.abortOnFailure = (updateElement.attribute(QStringLiteral("abortOnFailure")) == QLatin1String("true")); + + QDomElement childElement = updateElement.firstChildElement(); + while (!childElement.isNull()) { + if (childElement.tagName() == QLatin1String("raw-sql")) { + if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) { + updateSet.statements << buildRawSqlStatement(childElement); + } + } else if (childElement.tagName() == QLatin1String("complex-update")) { + if (updateApplicable(childElement.attribute(QStringLiteral("backends")))) { + updateSet.complex = true; + } + } + // TODO: check for generic tags here in the future + + childElement = childElement.nextSiblingElement(); + } + + if (!updateSet.statements.isEmpty() || updateSet.complex) { + updates.insert(version, updateSet); + } + } + } + updateElement = updateElement.nextSiblingElement(); + } + + return true; +} + +bool DbUpdater::updateApplicable(const QString &backends) const +{ + const QStringList matchingBackends = backends.split(QLatin1Char(',')); + + QString currentBackend; + switch (DbType::type(m_database)) { + case DbType::MySQL: + currentBackend = QStringLiteral("mysql"); + break; + case DbType::PostgreSQL: + currentBackend = QStringLiteral("psql"); + break; + case DbType::Sqlite: + currentBackend = QStringLiteral("sqlite"); + break; + case DbType::Unknown: + return false; + } + + return matchingBackends.contains(currentBackend); +} + +QString DbUpdater::buildRawSqlStatement(const QDomElement &element) const +{ + return element.text().trimmed(); +} + +bool DbUpdater::complexUpdate_25() +{ + qCDebug(AKONADISERVER_LOG) << "Starting database update to version 25"; + + DbType::Type dbType = DbType::type(DataStore::self()->database()); + + QElapsedTimer ttotal; + ttotal.start(); + + // Recover from possibly failed or interrupted update + { + // We don't care if this fails, it just means that there was no failed update + QSqlQuery query(DataStore::self()->database()); + query.exec(QStringLiteral("ALTER TABLE PartTable_old RENAME TO PartTable")); + } + + { + QSqlQuery query(DataStore::self()->database()); + query.exec(QStringLiteral("DROP TABLE IF EXISTS PartTable_new")); + } + + { + // Make sure the table is empty, otherwise we get duplicate key error + QSqlQuery query(DataStore::self()->database()); + if (dbType == DbType::Sqlite) { + query.exec(QStringLiteral("DELETE FROM PartTypeTable")); + } else { // MySQL, PostgreSQL + query.exec(QStringLiteral("TRUNCATE TABLE PartTypeTable")); + } + } + + { + // It appears that more users than expected have the invalid "GID" part in their + // PartTable, which breaks the migration below (see BKO#331867), so we apply this + // wanna-be fix to remove the invalid part before we start the actual migration. + QueryBuilder qb(QStringLiteral("PartTable"), QueryBuilder::Delete); + qb.addValueCondition(QStringLiteral("PartTable.name"), Query::Equals, QLatin1String("GID")); + qb.exec(); + } + + qCDebug(AKONADISERVER_LOG) << "Creating a PartTable_new"; + { + TableDescription description; + description.name = QStringLiteral("PartTable_new"); + + ColumnDescription idColumn; + idColumn.name = QStringLiteral("id"); + idColumn.type = QStringLiteral("qint64"); + idColumn.isAutoIncrement = true; + idColumn.isPrimaryKey = true; + description.columns << idColumn; + + ColumnDescription pimItemIdColumn; + pimItemIdColumn.name = QStringLiteral("pimItemId"); + pimItemIdColumn.type = QStringLiteral("qint64"); + pimItemIdColumn.allowNull = false; + description.columns << pimItemIdColumn; + + ColumnDescription partTypeIdColumn; + partTypeIdColumn.name = QStringLiteral("partTypeId"); + partTypeIdColumn.type = QStringLiteral("qint64"); + partTypeIdColumn.allowNull = false; + description.columns << partTypeIdColumn; + + ColumnDescription dataColumn; + dataColumn.name = QStringLiteral("data"); + dataColumn.type = QStringLiteral("QByteArray"); + description.columns << dataColumn; + + ColumnDescription dataSizeColumn; + dataSizeColumn.name = QStringLiteral("datasize"); + dataSizeColumn.type = QStringLiteral("qint64"); + dataSizeColumn.allowNull = false; + description.columns << dataSizeColumn; + + ColumnDescription versionColumn; + versionColumn.name = QStringLiteral("version"); + versionColumn.type = QStringLiteral("int"); + versionColumn.defaultValue = QStringLiteral("0"); + description.columns << versionColumn; + + ColumnDescription externalColumn; + externalColumn.name = QStringLiteral("external"); + externalColumn.type = QStringLiteral("bool"); + externalColumn.defaultValue = QStringLiteral("false"); + description.columns << externalColumn; + + DbInitializer::Ptr initializer = DbInitializer::createInstance(DataStore::self()->database()); + const QString queryString = initializer->buildCreateTableStatement(description); + + QSqlQuery query(DataStore::self()->database()); + if (!query.exec(queryString)) { + qCCritical(AKONADISERVER_LOG) << query.lastError().text(); + return false; + } + } + + qCDebug(AKONADISERVER_LOG) << "Migrating part types"; + { + // Get list of all part names + QueryBuilder qb(QStringLiteral("PartTable"), QueryBuilder::Select); + qb.setDistinct(true); + qb.addColumn(QStringLiteral("PartTable.name")); + + if (!qb.exec()) { + qCCritical(AKONADISERVER_LOG) << qb.query().lastError().text(); + return false; + } + + // Process them one by one + QSqlQuery query = qb.query(); + while (query.next()) { + // Split the part name to namespace and name and insert it to PartTypeTable + const QString partName = query.value(0).toString(); + const QString ns = partName.left(3); + const QString name = partName.mid(4); + + { + QueryBuilder qb(QStringLiteral("PartTypeTable"), QueryBuilder::Insert); + qb.setColumnValue(QStringLiteral("ns"), ns); + qb.setColumnValue(QStringLiteral("name"), name); + if (!qb.exec()) { + qCCritical(AKONADISERVER_LOG) << qb.query().lastError().text(); + return false; + } + } + qCDebug(AKONADISERVER_LOG) << "\t Moved part type" << partName << "to PartTypeTable"; + } + query.finish(); + } + + qCDebug(AKONADISERVER_LOG) << "Migrating data from PartTable to PartTable_new"; + { + QSqlQuery query(DataStore::self()->database()); + QString queryString; + if (dbType == DbType::PostgreSQL) { + queryString = QStringLiteral( + "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " + "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " + " PartTable.datasize, PartTable.version, PartTable.external " + "FROM PartTable " + "LEFT JOIN PartTypeTable ON " + " PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)"); + } else if (dbType == DbType::MySQL) { + queryString = QStringLiteral( + "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " + "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " + "PartTable.datasize, PartTable.version, PartTable.external " + "FROM PartTable " + "LEFT JOIN PartTypeTable ON PartTable.name = CONCAT(PartTypeTable.ns, ':', PartTypeTable.name)"); + } else if (dbType == DbType::Sqlite) { + queryString = QStringLiteral( + "INSERT INTO PartTable_new (id, pimItemId, partTypeId, data, datasize, version, external) " + "SELECT PartTable.id, PartTable.pimItemId, PartTypeTable.id, PartTable.data, " + "PartTable.datasize, PartTable.version, PartTable.external " + "FROM PartTable " + "LEFT JOIN PartTypeTable ON PartTable.name = PartTypeTable.ns || ':' || PartTypeTable.name"); + } + + if (!query.exec(queryString)) { + qCCritical(AKONADISERVER_LOG) << query.lastError().text(); + return false; + } + } + + qCDebug(AKONADISERVER_LOG) << "Swapping PartTable_new for PartTable"; + { + // Does an atomic swap + + QSqlQuery query(DataStore::self()->database()); + + if (dbType == DbType::PostgreSQL || dbType == DbType::Sqlite) { + if (dbType == DbType::PostgreSQL) { + DataStore::self()->beginTransaction(QStringLiteral("DBUPDATER (r25)")); + } + + if (!query.exec(QStringLiteral("ALTER TABLE PartTable RENAME TO PartTable_old"))) { + qCCritical(AKONADISERVER_LOG) << query.lastError().text(); + DataStore::self()->rollbackTransaction(); + return false; + } + + // If this fails in SQLite (i.e. without transaction), we can still recover on next start) + if (!query.exec(QStringLiteral("ALTER TABLE PartTable_new RENAME TO PartTable"))) { + qCCritical(AKONADISERVER_LOG) << query.lastError().text(); + if (DataStore::self()->inTransaction()) { + DataStore::self()->rollbackTransaction(); + } + return false; + } + + if (dbType == DbType::PostgreSQL) { + DataStore::self()->commitTransaction(); + } + } else { // MySQL cannot do rename in transaction, but supports atomic renames + if (!query.exec(QStringLiteral("RENAME TABLE PartTable TO PartTable_old," + " PartTable_new TO PartTable"))) { + qCCritical(AKONADISERVER_LOG) << query.lastError().text(); + return false; + } + } + } + + qCDebug(AKONADISERVER_LOG) << "Removing PartTable_old"; + { + QSqlQuery query(DataStore::self()->database()); + if (!query.exec(QStringLiteral("DROP TABLE PartTable_old;"))) { + // It does not matter when this fails, we are successfully migrated + qCDebug(AKONADISERVER_LOG) << query.lastError().text(); + qCDebug(AKONADISERVER_LOG) << "Not a fatal problem, continuing..."; + } + } + + // Fine tuning for PostgreSQL + qCDebug(AKONADISERVER_LOG) << "Final tuning of new PartTable"; + { + QSqlQuery query(DataStore::self()->database()); + if (dbType == DbType::PostgreSQL) { + query.exec(QStringLiteral("ALTER TABLE PartTable RENAME CONSTRAINT parttable_new_pkey TO parttable_pkey")); + query.exec(QStringLiteral("ALTER SEQUENCE parttable_new_id_seq RENAME TO parttable_id_seq")); + query.exec(QStringLiteral("SELECT setval('parttable_id_seq', MAX(id) + 1) FROM PartTable")); + } else if (dbType == DbType::MySQL) { + // 0 will automatically reset AUTO_INCREMENT to SELECT MAX(id) + 1 FROM PartTable + query.exec(QStringLiteral("ALTER TABLE PartTable AUTO_INCREMENT = 0")); + } + } + + qCDebug(AKONADISERVER_LOG) << "Update done in" << ttotal.elapsed() << "ms"; + + // Foreign keys and constraints will be reconstructed automatically once + // all updates are done + + return true; +} + +bool DbUpdater::complexUpdate_36() +{ + qCDebug(AKONADISERVER_LOG, "Starting database update to version 36"); + Q_ASSERT(DbType::type(DataStore::self()->database()) == DbType::Sqlite); + + QSqlQuery query(DataStore::self()->database()); + if (!query.exec(QStringLiteral("PRAGMA foreign_key_checks=OFF"))) { + qCCritical(AKONADISERVER_LOG, "Failed to disable foreign key checks!"); + return false; + } + + const auto hasForeignKeys = [](const TableDescription &desc) { + return std::any_of(desc.columns.cbegin(), desc.columns.cend(), [](const ColumnDescription &col) { + return !col.refTable.isEmpty() && !col.refColumn.isEmpty(); + }); + }; + + const auto recreateTableWithForeignKeys = [](const TableDescription &table) -> QPair { + qCDebug(AKONADISERVER_LOG) << "Updating foreign keys in table" << table.name; + + QSqlQuery query(DataStore::self()->database()); + + // Recover from possibly failed or interrupted update + // We don't care if this fails, it just means that there was no failed update + query.exec(QStringLiteral("ALTER TABLE %1_old RENAME TO %1").arg(table.name)); + query.exec(QStringLiteral("DROP TABLE %1_new").arg(table.name)); + + qCDebug(AKONADISERVER_LOG, "\tCreating table %s_new with foreign keys", qUtf8Printable(table.name)); + { + const auto initializer = DbInitializer::createInstance(DataStore::self()->database()); + TableDescription copy = table; + copy.name += QStringLiteral("_new"); + if (!query.exec(initializer->buildCreateTableStatement(copy))) { + // If this fails we will recover on next start + return {false, query}; + } + } + + qCDebug(AKONADISERVER_LOG, + "\tCopying values from %s to %s_new (this may take a very long of time...)", + qUtf8Printable(table.name), + qUtf8Printable(table.name)); + if (!query.exec(QStringLiteral("INSERT INTO %1_new SELECT * FROM %1").arg(table.name))) { + // If this fails, we will recover on next start + return {false, query}; + } + + qCDebug(AKONADISERVER_LOG, "\tSwapping %s_new for %s", qUtf8Printable(table.name), qUtf8Printable(table.name)); + if (!query.exec(QStringLiteral("ALTER TABLE %1 RENAME TO %1_old").arg(table.name))) { + // If this fails we will recover on next start + return {false, query}; + } + + if (!query.exec(QStringLiteral("ALTER TABLE %1_new RENAME TO %1").arg(table.name))) { + // If this fails we will recover on next start + return {false, query}; + } + + qCDebug(AKONADISERVER_LOG, "\tRemoving table %s_old", qUtf8Printable(table.name)); + if (!query.exec(QStringLiteral("DROP TABLE %1_old").arg(table.name))) { + // We don't care if this fails + qCWarning(AKONADISERVER_LOG, "Failed to DROP TABLE %s (not fatal, update will continue)", qUtf8Printable(table.name)); + qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); + } + + qCDebug(AKONADISERVER_LOG) << "\tOptimizing table %s", qUtf8Printable(table.name); + if (!query.exec(QStringLiteral("ANALYZE %1").arg(table.name))) { + // We don't care if this fails + qCWarning(AKONADISERVER_LOG, "Failed to ANALYZE %s (not fatal, update will continue)", qUtf8Printable(table.name)); + qCWarning(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); + } + + qCDebug(AKONADISERVER_LOG) << "\tDone"; + return {true, QSqlQuery()}; + }; + + AkonadiSchema schema; + const auto tables = schema.tables(); + for (const auto &table : tables) { + if (!hasForeignKeys(table)) { + continue; + } + + const auto &[ok, query] = recreateTableWithForeignKeys(table); + if (!ok) { + qCCritical(AKONADISERVER_LOG, "SQL error when updating table %s", qUtf8Printable(table.name)); + qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery())); + qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); + return false; + } + } + + const auto relations = schema.relations(); + for (const auto &relation : relations) { + const RelationTableDescription table(relation); + const auto &[ok, query] = recreateTableWithForeignKeys(table); + if (!ok) { + qCCritical(AKONADISERVER_LOG, "SQL error when updating relation table %s", qUtf8Printable(table.name)); + qCCritical(AKONADISERVER_LOG, "Query: %s", qUtf8Printable(query.executedQuery())); + qCCritical(AKONADISERVER_LOG, "Error: %s", qUtf8Printable(query.lastError().text())); + return false; + } + } + + qCDebug(AKONADISERVER_LOG) << "Running VACUUM to reduce DB size"; + if (!query.exec(QStringLiteral("VACUUM"))) { + qCWarning(AKONADISERVER_LOG) << "Vacuum failed (not fatal, update will continue)"; + qCWarning(AKONADISERVER_LOG) << "Error:" << query.lastError().text(); + } + + return true; +} diff --git a/src/server/storage/dbupdater.h b/src/server/storage/dbupdater.h new file mode 100644 index 0000000..66fa545 --- /dev/null +++ b/src/server/storage/dbupdater.h @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class QDomElement; +class DbUpdaterTest; + +namespace Akonadi +{ +namespace Server +{ +/** + * @short A helper class that contains an update set. + */ +class UpdateSet +{ +public: + using Map = QMap; + + UpdateSet() + : version(-1) + , abortOnFailure(false) + , complex(false) + { + } + + int version; + bool abortOnFailure; + QStringList statements; + bool complex; +}; + +/** + Updates the database schema. +*/ +class DbUpdater : public QObject +{ + Q_OBJECT + +public: + /** + * Creates a new database updates. + * + * @param database The reference to the database. + * @param filename The file containing the update descriptions. + */ + DbUpdater(const QSqlDatabase &database, const QString &filename); + + /** + * Starts the update process. + * On success true is returned, false otherwise. + */ + bool run(); + +private Q_SLOTS: + bool complexUpdate_25(); + bool complexUpdate_36(); + +private: + friend class ::DbUpdaterTest; + + bool updateApplicable(const QString &backends) const; + QString buildRawSqlStatement(const QDomElement &element) const; + + bool parseUpdateSets(int, UpdateSet::Map &updates) const; + + QSqlDatabase m_database; + QString m_filename; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/doxygen-preprocess-entities.sh b/src/server/storage/doxygen-preprocess-entities.sh new file mode 100755 index 0000000..e163105 --- /dev/null +++ b/src/server/storage/doxygen-preprocess-entities.sh @@ -0,0 +1,17 @@ +if test -z "`which xsltproc`"; then + echo "No xlstproc found!" + exit 1; +fi + +case $1 in +create) + xsltproc --stringparam code header entities.xsl akonadidb.xml > entities.h + xsltproc --stringparam code source entities.xsl akonadidb.xml > entities.cpp + xsltproc entities-dox.xsl akonadidb.xml > Database.dox +;; +cleanup) + rm -f entities.h entities.cpp + rm -f Database.dox +;; +esac + diff --git a/src/server/storage/entities-dox.xsl b/src/server/storage/entities-dox.xsl new file mode 100644 index 0000000..fe15f8b --- /dev/null +++ b/src/server/storage/entities-dox.xsl @@ -0,0 +1,65 @@ + + + + + + +// autogenerated from akonadi.db and entities-dox.xsl +/** +\page akonadi_server_database Database Design + +\section akonadi_server_database_layout Database Layout + +This is an overview of the database layout of the \ref akonadi_design_storage "storage server". +The schema gets generated by the server using the helper class DbInitializer, based on the +definition found in @c server/src/storage/akonadidb.xml. + +\dot +digraph "Akonadi Database Layout" { + graph [rankdir="LR" fontsize="10"] + node [fontsize="10" shape="record" style="filled" fillcolor="lightyellow"] + edge [fontsize="10"] + + + [label="<1>|<>" URL="classAkonadi_1_1.html"]; + + + : -> :[label="n:1"]; + + + + + + : -> :[label="n:m" arrowtail=normal]; + +} +\enddot + + +\section akonadi_server_database_codegeneration Code Generation + +Code to access the database is generated from @c akonadidb.xml using an XSL stylesheet, @c entities.xsl. +The generated code encapsulates basic database operations, such as retrieving, inserting, updating and +removing records, as well as methods to retrieve related records. They also contain methods to retrieve +table and column names for creating SQL queries in a typo-safe way. + +The following classes are generated: + +- Akonadi::Server:: + + +For the helper tables used for n:m relations, the following classes are generated. They are only useful +when creating SQL queries that handle the n:m relations manually. + +- Akonadi::Server::Relation + +*/ + + + + diff --git a/src/server/storage/entities-header.xsl b/src/server/storage/entities-header.xsl new file mode 100644 index 0000000..1712ce3 --- /dev/null +++ b/src/server/storage/entities-header.xsl @@ -0,0 +1,299 @@ + + + + + + + + + +/** + Representation of a record in the table. + + <br> + + + This class is implicitly shared. +*/ +class : private Entity +{ + friend class DataStore; + +public: + /// List of records. + typedef QVector<> List; + + // make some stuff accessible from Entity: + using Entity::Id; + using Entity::id; + using Entity::setId; + using Entity::isValid; + using Entity::joinByName; + using Entity::addToRelation; + using Entity::removeFromRelation; + + + enum { + + = , + + }; + + + // constructor + (); + explicit ( + + , + ); + + explicit ( + + , + ); + + (const &other); + + // destructor + ~(); + + /// assignment operator + &operator=(const &other); + + /// comparisson operator, compares ids, not content + bool operator==(const &other) const; + + // accessor methods + + /** + Returns the value of the column of this record. + + */ + () const; + /** + Sets the value of the column of this record. + + */ + void ; + + + /** Returns the name of the SQL table. */ + static QString tableName(); + + /** + Returns a list of all SQL column names. The names are in the correct + order for usage with extractResult(). + */ + static QStringList columnNames(); + + /** + Returns a list of all SQL column names prefixed with their tables names. + The names are in the correct order for usage with extractResult(). + */ + static QStringList fullColumnNames(); + + + static QString Column(); + static QString FullColumnName(); + + + /** + Extracts the query result. + @param query A executed query containing a list of records. + Note that the fields need to be in the correct order (same as in the constructor)! + */ + static QVector<> extractResult(QSqlQuery &query); + + /** Count records with value @p value in column @p column. */ + static int count(const QString &column, const QVariant &value); + + // check existence + + /** Checks if a record with id @p id exists. */ + static bool exists(qint64 id); + + + /** Checks if a record with name @name exists. */ + static bool exists(const &name); + + + // data retrieval + + /** Returns the record with id @p id. */ + static retrieveById(qint64 id); + + + + /** Returns the record with name @p name. */ + static retrieveByName(const &name); + + /** Returns the record with name @p name. If such record does not exist, + it will be created. This method is thread-safe, so if multiple callers + call it on non-existent name, only one will create the new record, others + will wait and read it from the cache. */ + static retrieveByNameOrCreate(const &name); + + + + + static PartType retrieveByFQName(const QString &ns, const QString &name); + static PartType retrieveByFQNameOrCreate(const QString &ns, const QString &name); + + + /** Retrieve all records from this table. */ + static ::List retrieveAll(); + /** Retrieve all records with value @p value in column @p key. */ + static ::List retrieveFiltered(const QString &key, const QVariant &value); + + + + + /** + Retrieve the record referred to by the + column of this record. + */ + () const; + + /** + Set the record referred to by the + column of this record. + */ + void set + (const &value); + + + + + /** + Retrieve a list of all records referring to this record + in their column . + */ + QVector<> () const; + + + // data retrieval for n:m relations + + QVector<> s() const; + + + /** + Inserts this record into the DataStore. + @param insertId pointer to an int, filled with the identifier of this record on success. + */ + bool insert(qint64 *insertId = nullptr); + + /** + Returns @c true if this record has any pending changes. + */ + bool hasPendingChanges() const; + + /** + Stores all changes made to this record into the database. + Note that this method assumes the existence of an 'id' column to identify + the record to update. If that column does not exist, all records will be + changed. + @returns true on success, false otherwise. + */ + bool update(); + + + /** Deletes this record. */ + bool remove(); + + /** Deletes the record with the given id. */ + static bool remove(qint64 id); + + + /** + Invalidates the cache entry for this record. + This method has no effect if caching is not enabled for this table. + */ + void invalidateCache() const; + + /** + Invalidates all cache entries for this table. + This method has no effect if caching is not enabled for this table. + */ + static void invalidateCompleteCache(); + + /** + Enable/disable caching for this table. + This method is not thread-safe, call before activating multi-threading. + */ + static void enableCache(bool enable); + + // manipulate n:m relations + + + /** + Checks wether this record is in a n:m relation with the @p value. + */ + bool relatesTo(const &value) const; + static bool relatesTo(qint64 leftId, qint64 rightId); + + /** + Adds a n:m relation between this record and the @p value. + */ + bool add(const &value) const; + static bool add(qint64 leftId, qint64 rightId); + + /** + Removes a n:m relation between this record and the @p value. + */ + bool remove(const &value) const; + static bool remove(qint64 leftId, qint64 rightId); + + /** + Removes all relations between this record and any . + */ + bool clears() const; + static bool clears(qint64 id); + + + // delete records + static bool remove(const QString &column, const QVariant &value); + +private: + class Private; + QSharedDataPointer<Private> d; +}; + + + + + + +#ifndef QT_NO_DEBUG_STREAM +// debug stream operator +QDebug &operator<<(QDebug &d, const Akonadi::Server:: &entity); +#endif + + + + + +Relation + + +/** + +*/ + +class +{ + public: + // SQL table information + static QString tableName(); + static QString leftColumn(); + static QString leftFullColumnName(); + static QString rightColumn(); + static QString rightFullColumnName(); +}; + + + diff --git a/src/server/storage/entities-source.xsl b/src/server/storage/entities-source.xsl new file mode 100644 index 0000000..493af0d --- /dev/null +++ b/src/server/storage/entities-source.xsl @@ -0,0 +1,734 @@ + + + + + + + +Table + + +// private class +class ::Private : public QSharedData +{ +public: + Private() : QSharedData() // NOLINT(readability-redundant-member-init) + + + , (false) + + + , _changed(false) + + + {} + + + + qint64 = 0; + + + + + QString ; + + + QByteArray ; + + + // on non-wince, QDateTime is one int + + QDateTime ; + + + + int = 0; + + + bool : 1; + + + Tristate ; + + + = 0; + + + bool _changed : 1; + + + + static void addToCache(const &entry); + + // cache + static QAtomicInt cacheEnabled; + static QMutex cacheMutex; + + static QHash<qint64, > idCache; + + + static QHash<, > nameCache; + +}; + + +// static members +QAtomicInt ::Private::cacheEnabled(0); +QMutex ::Private::cacheMutex; + +QHash<qint64, > ::Private::idCache; + + +QHash<, > ::Private::nameCache; + + + +void ::Private::addToCache(const &entry) +{ + Q_ASSERT(cacheEnabled); + Q_UNUSED(entry); + QMutexLocker lock(&cacheMutex); + + idCache.insert(entry.id(), entry); + + + + + + nameCache.insert(entry.ns() + QLatin1Char(':') + entry.name(), entry); + + + nameCache.insert(entry.name(), entry); + + + +} + + +// constructor +::() + : d(new Private) +{ +} + +::( + + , + +) : d(new Private) +{ + + d-> = ; + d->_changed = true; + +} + + +::( + + , + +) : + Entity(id), + d(new Private) +{ + + d-> = ; + d->_changed = true; + +} + + +::(const &other) + : Entity(other), d(other.d) +{ +} + +// destructor +::~() {} + +// assignment operator +& ::operator=(const &other) +{ + if (this != &other) { + d = other.d; + setId(other.id()); + } + return *this; +} + +// comparisson operator +bool ::operator==(const &other) const +{ + return id() == other.id(); +} + +// accessor methods + + ::() const +{ + return d->; +} + +void :: +{ + d-> = ; + d->_changed = true; +} + + + +// SQL table information +QString ::tableName() +{ + static const QString tableName = QStringLiteral(""); + return tableName; +} + +QStringList ::columnNames() +{ + static const QStringList columns = { + + Column() + , + + }; + return columns; +} + +QStringList ::fullColumnNames() +{ + static const QStringList columns = { + + FullColumnName() + , + + }; + return columns; +} + + +QString ::Column() +{ + static const QString column = QStringLiteral(""); + return column; +} + +QString ::FullColumnName() +{ + static const QString column = QStringLiteral("."); + return column; +} + + + +// count records +int ::count(const QString &column, const QVariant &value) +{ + return Entity::count<>(column, value); +} + +// check existence + +bool ::exists(qint64 id) +{ + if (Private::cacheEnabled) { + QMutexLocker lock(&Private::cacheMutex); + if (Private::idCache.contains(id)) { + return true; + } + } + return count(idColumn(), id) > 0; +} + + +bool ::exists(const &name) +{ + if (Private::cacheEnabled) { + QMutexLocker lock(&Private::cacheMutex); + if (Private::nameCache.contains(name)) { + return true; + } + } + return count(nameColumn(), name) > 0; +} + + + +// result extraction +QVector<> ::extractResult(QSqlQuery &query) +{ + QVector<> rv; + if (query.driver()->hasFeature(QSqlDriver::QuerySize)) { + rv.reserve(query.size()); + } + while (query.next()) { + rv.append(( + + (query.isNull() + ? () + : + + + Utils::variantToString(query.value()) + + + static_cast<>(query.value().value<int>()) + + + query.value().value<>() + + + ), + + )); + } + query.finish(); + return rv; +} + +// data retrieval + + ::retrieveById(qint64 id) +{ + + id + idCache + +} + + + + ::retrieveByName(const &name) +{ + + name + nameCache + +} + + ::retrieveByNameOrCreate(const &name) +{ + static QMutex lock; + QMutexLocker locker(&lock); + auto rv = retrieveByName(name); + if (rv.isValid()) { + return rv; + } + + rv.setName(name); + if (!rv.insert()) { + return (); + } + + if (Private::cacheEnabled) { + Private::addToCache(rv); + } + return rv; +} + + + +PartType PartType::retrieveByFQName(const QString &ns, const QString &name) +{ + const QString fqname = ns + QLatin1Char(':') + name; + + ns + name + fqname + nameCache + +} + +PartType PartType::retrieveByFQNameOrCreate(const QString &ns, const QString &name) +{ + static QMutex lock; + QMutexLocker locker(&lock); + PartType rv = retrieveByFQName(ns, name); + if (rv.isValid()) { + return rv; + } + + rv.setNs(ns); + rv.setName(name); + if (!rv.insert()) { + return PartType(); + } + + if (Private::cacheEnabled) { + Private::addToCache(rv); + } + return rv; +} + + +QVector<> ::retrieveAll() +{ + QSqlDatabase db = DataStore::self()->database(); + if (!db.isOpen()) { + return {}; + } + + QueryBuilder qb(tableName(), QueryBuilder::Select); + qb.addColumns(columnNames()); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during selection of all records from table" << tableName() + << qb.query().lastError().text() << qb.query().lastQuery(); + return {}; + } + return extractResult(qb.query()); +} + +QVector<> ::retrieveFiltered(const QString &key, const QVariant &value) +{ + QSqlDatabase db = DataStore::self()->database(); + if (!db.isOpen()) { + return {}; + } + + SelectQueryBuilder<> qb; + if (value.isNull()) { + qb.addValueCondition(key, Query::Is, QVariant()); + } else { + qb.addValueCondition(key, Query::Equals, value); + } + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during selection of records from table" << tableName() + << "filtered by" << key << "=" << value + << qb.query().lastError().text(); + return {}; + } + return qb.result(); +} + +// data retrieval for referenced tables + + + ::() const +{ + return ::retrieveById(()); +} + +void :: + set + (const &value) +{ + d-> = value.id(); + d->_changed = true; +} + + +// data retrieval for inverse referenced tables + +QVector<> ::() const +{ + return ::retrieveFiltered(::Column(), id()); +} + + + + +Relation + + + +// data retrieval for n:m relations +QVector<> ::s() const +{ + QSqlDatabase db = DataStore::self()->database(); + if (!db.isOpen()) { + return {}; + } + + QueryBuilder qb(::tableName(), QueryBuilder::Select); + static const QStringList columns = { + + ::FullColumnName() + , + + }; + qb.addColumns(columns); + qb.addJoin(QueryBuilder::InnerJoin, ::tableName(), + ::rightFullColumnName(), + ::FullColumnName()); + qb.addValueCondition(::leftFullColumnName(), Query::Equals, id()); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during selection of records from table Relation" + << qb.query().lastError().text(); + return {}; + } + + return ::extractResult(qb.query()); +} + +// manipulate n:m relations +bool ::relatesTo(const &value) const +{ + return Entity::relatesTo<>(id(), value.id()); +} + +bool ::relatesTo(qint64 leftId, qint64 rightId) +{ + return Entity::relatesTo<>(leftId, rightId); +} + +bool ::add(const &value) const +{ + return Entity::addToRelation<>(id(), value.id()); +} + +bool ::add(qint64 leftId, qint64 rightId) +{ + return Entity::addToRelation<>(leftId, rightId); +} + +bool ::remove(const &value) const +{ + return Entity::removeFromRelation<>(id(), value.id()); +} + +bool ::remove(qint64 leftId, qint64 rightId) +{ + return Entity::removeFromRelation<>(leftId, rightId); +} + +bool ::clears() const +{ + return Entity::clearRelation<>(id()); +} + +bool ::clears(qint64 id) +{ + return Entity::clearRelation<>(id); +} + + + +#ifndef QT_NO_DEBUG_STREAM +// debug stream operator +QDebug &operator<<(QDebug &d, const &entity) +{ + d << "[: " + + << " = " << + + + static_cast<int>(entity.()) + + + entity.() + + + << ", " + + << "]"; + return d; +} +#endif + +// inserting new data +bool ::insert(qint64* insertId) +{ + QSqlDatabase db = DataStore::self()->database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder qb(tableName(), QueryBuilder::Insert); + + qb.setIdentificationColumn(QLatin1String("")); + + + + + if (d->_changed && d-> > 0) { + qb.setColumnValue( Column(), this->() ); + } + + + if (d->_changed) { + + + qb.setColumnValue(Column(), static_cast<int>(this->())); + + + qb.setColumnValue(Column(), this->()); + + + } + + + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during insertion into table" << tableName() + << qb.query().lastError().text(); + return false; + } + + setId(qb.insertId()); + if (insertId) { + *insertId = id(); + } + return true; +} + +bool ::hasPendingChanges() const +{ + return false // NOLINT(readability-simplify-boolean-expr) + + || d->_changed + ; +} + +// update existing data +bool ::update() +{ + invalidateCache(); + QSqlDatabase db = DataStore::self()->database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder qb(tableName(), QueryBuilder::Update); + + + + if (d->_changed) { + + if (d-> <= 0) { + qb.setColumnValue(Column(), QVariant()); + } else { + + + + qb.setColumnValue(Column(), static_cast<int>(this->())); + + + qb.setColumnValue(Column(), this->()); + + + + } + + } + + + + qb.addValueCondition(idColumn(), Query::Equals, id()); + + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during updating record with id" << id() + << " in table" << tableName() << qb.query().lastError().text(); + return false; + } + return true; +} + +// delete records +bool ::remove(const QString &column, const QVariant &value) +{ + invalidateCompleteCache(); + return Entity::remove<>(column, value); +} + + +bool ::remove() +{ + invalidateCache(); + return Entity::remove<>(idColumn(), id()); +} + +bool ::remove(qint64 id) +{ + return remove(idColumn(), id); +} + + +// cache stuff +void ::invalidateCache() const +{ + if (Private::cacheEnabled) { + QMutexLocker lock(&Private::cacheMutex); + + Private::idCache.remove(id()); + + + + + + Private::nameCache.remove(ns() + QLatin1Char(':') + name()); + + + Private::nameCache.remove(name()); + + + + } +} + +void ::invalidateCompleteCache() +{ + if (Private::cacheEnabled) { + QMutexLocker lock(&Private::cacheMutex); + + Private::idCache.clear(); + + + Private::nameCache.clear(); + + } +} + +void ::enableCache(bool enable) +{ + Private::cacheEnabled = enable; +} + + + + + + +Relation +Relation + +// SQL table information +QString ::tableName() +{ + static const QString table = QStringLiteral("" ); + return table; +} + +QString ::leftColumn() +{ + static const QString column = QStringLiteral("_"); + return column; +} + +QString ::leftFullColumnName() +{ + static const QString column = QStringLiteral("._"); + return column; +} + +QString ::rightColumn() +{ + static const QString column = QStringLiteral("_"); + return column; +} + +QString ::rightFullColumnName() +{ + static const QString column = QStringLiteral("._"); + return column; +} + + + diff --git a/src/server/storage/entities.xsl b/src/server/storage/entities.xsl new file mode 100644 index 0000000..607ed75 --- /dev/null +++ b/src/server/storage/entities.xsl @@ -0,0 +1,262 @@ + + + + + + + + + +header + + +/* + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + + + +#ifndef AKONADI_ENTITIES_H +#define AKONADI_ENTITIES_H +#include "storage/entity.h" + +#include <private/tristate_p.h> + +#include <QtCore/QDebug> +#include <QtCore/QSharedDataPointer> +#include <QtCore/QString> +#include <QtCore/QVariant> + +template <typename T> class QVector; +class QSqlQuery; +class QStringList; + +namespace Akonadi { +namespace Server { + +// forward declaration for table classes + +class ; + + +// forward declaration for relation classes + +class Relation; + + + + + + + + + + +/** Returns a list of all table names. */ +QVector<QString> allDatabaseTables(); + +} // namespace Server +} // namespace Akonadi + + + + + + +Q_DECLARE_TYPEINFO(Akonadi::Server::, Q_MOVABLE_TYPE); + +#endif + + + + + +#include <entities.h> +#include <storage/datastore.h> +#include <storage/selectquerybuilder.h> +#include <utils.h> +#include <akonadiserver_debug.h> + +#include <QSqlDatabase> +#include <QSqlQuery> +#include <QSqlError> +#include <QSqlDriver> +#include <QVariant> +#include <QHash> +#include <QMutex> +#include <QThread> + +using namespace Akonadi; +using namespace Akonadi::Server; + +static QStringList removeEntry(QStringList list, const QString& entry) +{ + list.removeOne(entry); + return list; +} + + + + + + + + + +QVector<QString> Akonadi::Server::allDatabaseTables() +{ + static const QVector<QString> allTables = { + + QStringLiteral("Table"), + + + QStringLiteral("Relation") + , + + }; + + return allTables; +} + + + + + + + + + + + + + + + + + :: + + + + + + + const + & + + + + + + + +set() + + + + + + + , + + + + + + + + + + + + if (Private::cacheEnabled) { + QMutexLocker lock(&Private::cacheMutex); + auto it = Private::.constFind(); + if (it != Private::.constEnd()) { + return it.value(); + } + } + + QSqlDatabase db = DataStore::self()->database(); + if (!db.isOpen()) { + return (); + } + + QueryBuilder qb(tableName(), QueryBuilder::Select); + static const QStringList columns = removeEntry(columnNames(), Column()); + qb.addColumns(columns); + qb.addValueCondition(Column(), Query::Equals, ); + + qb.addValueCondition(Column(), Query::Equals, ); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during selection of record with " + << << "from table" << tableName() + << qb.query().lastError().text(); + return (); + } + if (!qb.query().next()) { + return (); + } + + + int valueIndex = 0; + + const &value = + + + ; + + + (qb.query().isNull(valueIndex)) ? + () : + + + Utils::variantToString(qb.query().value( valueIndex)) + + + static_cast<>(qb.query().value( valueIndex ).value<int>()) + + + Utils::variantToDateTime(qb.query().value(valueIndex)) + + + qb.query().value( valueIndex ).value<>() + + + ; ++valueIndex; + + + + + rv( + + value + , + + ); + if (Private::cacheEnabled) { + Private::addToCache(rv); + } + return rv; + + + + + + + + + + + + + + + + diff --git a/src/server/storage/entity.cpp b/src/server/storage/entity.cpp new file mode 100644 index 0000000..9e95c63 --- /dev/null +++ b/src/server/storage/entity.cpp @@ -0,0 +1,167 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Andreas Gungl * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "entity.h" +#include "countquerybuilder.h" +#include "datastore.h" + +#include +#include + +using namespace Akonadi::Server; + +Entity::Entity() + : m_id(-1) +{ +} + +Entity::Entity(qint64 id) + : m_id(id) +{ +} + +Entity::~Entity() +{ +} + +qint64 Entity::id() const +{ + return m_id; +} + +void Entity::setId(qint64 id) +{ + m_id = id; +} + +bool Entity::isValid() const +{ + return m_id != -1; +} + +QSqlDatabase Entity::database() +{ + return DataStore::self()->database(); +} + +int Entity::countImpl(const QString &tableName, const QString &column, const QVariant &value) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return -1; + } + + CountQueryBuilder builder(tableName); + builder.addValueCondition(column, Query::Equals, value); + + if (!builder.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error counting records in table" << tableName; + return -1; + } + + return builder.result(); +} + +bool Entity::removeImpl(const QString &tableName, const QString &column, const QVariant &value) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder builder(tableName, QueryBuilder::Delete); + builder.addValueCondition(column, Query::Equals, value); + + if (!builder.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during deleting records from table" << tableName; + return false; + } + return true; +} + +bool Entity::relatesToImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + CountQueryBuilder builder(tableName); + builder.addValueCondition(leftColumn, Query::Equals, leftId); + builder.addValueCondition(rightColumn, Query::Equals, rightId); + + if (!builder.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during counting records in table" << tableName; + return false; + } + + return builder.result() > 0; +} + +bool Entity::addToRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder qb(tableName, QueryBuilder::Insert); + qb.setColumnValue(leftColumn, leftId); + qb.setColumnValue(rightColumn, rightId); + qb.setIdentificationColumn(QString()); + + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during adding a record to table" << tableName; + return false; + } + + return true; +} + +bool Entity::removeFromRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder builder(tableName, QueryBuilder::Delete); + builder.addValueCondition(leftColumn, Query::Equals, leftId); + builder.addValueCondition(rightColumn, Query::Equals, rightId); + + if (!builder.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during removing a record from relation table" << tableName; + return false; + } + + return true; +} + +bool Entity::clearRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 id, RelationSide side) +{ + QSqlDatabase db = database(); + if (!db.isOpen()) { + return false; + } + + QueryBuilder builder(tableName, QueryBuilder::Delete); + switch (side) { + case Left: + builder.addValueCondition(leftColumn, Query::Equals, id); + break; + case Right: + builder.addValueCondition(rightColumn, Query::Equals, id); + break; + default: + qFatal("Invalid enum value"); + } + if (!builder.exec()) { + qCWarning(AKONADISERVER_LOG) << "Error during clearing relation table" << tableName << "for ID" << id; + return false; + } + + return true; +} diff --git a/src/server/storage/entity.h b/src/server/storage/entity.h new file mode 100644 index 0000000..5e71d8a --- /dev/null +++ b/src/server/storage/entity.h @@ -0,0 +1,168 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Andreas Gungl * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include + +class QVariant; +class QSqlDatabase; + +namespace Akonadi +{ +namespace Server +{ +/** + Base class for classes representing database records. It also contains + low-level data access and manipulation template methods. +*/ +class Entity +{ +public: + using Id = qint64; + +protected: + qint64 id() const; + void setId(qint64 id); + + bool isValid() const; + +public: + template static QString joinByName(const QVector &list, const QString &sep) + { + QStringList tmp; + tmp.reserve(list.count()); + for (const T &t : list) { + tmp << t.name(); + } + return tmp.join(sep); + } + + /** + Returns the number of records having @p value in @p column. + @param column The name of the key column. + @param value The value used to identify the record. + */ + template inline static int count(const QString &column, const QVariant &value) + { + return Entity::countImpl(T::tableName(), column, value); + } + + /** + Deletes all records having @p value in @p column. + */ + template inline static bool remove(const QString &column, const QVariant &value) + { + return Entity::removeImpl(T::tableName(), column, value); + } + + /** + Checks whether an entry in a n:m relation table exists. + @param leftId Identifier of the left part of the relation. + @param rightId Identifier of the right part of the relation. + */ + template inline static bool relatesTo(qint64 leftId, qint64 rightId) + { + return Entity::relatesToImpl(T::tableName(), T::leftColumn(), T::rightColumn(), leftId, rightId); + } + + /** + Adds an entry to a n:m relation table (specified by the template parameter). + @param leftId Identifier of the left part of the relation. + @param rightId Identifier of the right part of the relation. + */ + template inline static bool addToRelation(qint64 leftId, qint64 rightId) + { + return Entity::addToRelationImpl(T::tableName(), T::leftColumn(), T::rightColumn(), leftId, rightId); + } + + /** + Removes an entry from a n:m relation table (specified by the template parameter). + @param leftId Identifier of the left part of the relation. + @param rightId Identifier of the right part of the relation. + */ + template inline static bool removeFromRelation(qint64 leftId, qint64 rightId) + { + return Entity::removeFromRelationImpl(T::tableName(), T::leftColumn(), T::rightColumn(), leftId, rightId); + } + + enum RelationSide { + Left, + Right, + }; + + /** + Clears all entries from a n:m relation table (specified by the given template parameter). + @param id Identifier on the relation side. + @param side The side of the relation. + */ + template inline static bool clearRelation(qint64 id, RelationSide side = Left) + { + return Entity::clearRelationImpl(T::tableName(), T::leftColumn(), T::rightColumn(), id, side); + } + +protected: + Entity(); + explicit Entity(qint64 id); + ~Entity(); + +private: + static int countImpl(const QString &tableName, const QString &column, const QVariant &value); + static bool removeImpl(const QString &tableName, const QString &column, const QVariant &value); + static bool relatesToImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId); + static bool addToRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId); + static bool removeFromRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 leftId, qint64 rightId); + static bool clearRelationImpl(const QString &tableName, const QString &leftColumn, const QString &rightColumn, qint64 id, RelationSide side); + +private: + static QSqlDatabase database(); + qint64 m_id; +}; + +namespace _detail +{ +/*! + Binary predicate to sort collections of Entity subclasses by + their id. + + Example for sorting: + \code + std::sort( coll.begin(), coll.end(), _detail::ById() ); + \endcode + + Example for finding by id: + \code + // linear: + std::find_if( coll.begin(), coll.end(), bind( _detail::ById(), _1, myId ) ); + // binary: + std::lower_bound( coll.begin(), coll.end(), myId, _detail::ById() ); + \endcode +*/ +template class Op> struct ById { + using result_type = bool; + bool operator()(Entity::Id lhs, Entity::Id rhs) const + { + return Op()(lhs, rhs); + } + template bool operator()(const E &lhs, const E &rhs) const + { + return this->operator()(lhs.id(), rhs.id()); + } + template bool operator()(const E &lhs, Entity::Id rhs) const + { + return this->operator()(lhs.id(), rhs); + } + template bool operator()(Entity::Id lhs, const E &rhs) const + { + return this->operator()(lhs, rhs.id()); + } +}; +} + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/itemqueryhelper.cpp b/src/server/storage/itemqueryhelper.cpp new file mode 100644 index 0000000..6b3a51e --- /dev/null +++ b/src/server/storage/itemqueryhelper.cpp @@ -0,0 +1,139 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemqueryhelper.h" + +#include "collectionqueryhelper.h" +#include "commandcontext.h" +#include "handler.h" +#include "storage/querybuilder.h" +#include "storage/queryhelper.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void ItemQueryHelper::itemSetToQuery(const ImapSet &set, QueryBuilder &qb, const Collection &collection) +{ + if (!set.isEmpty()) { + QueryHelper::setToQuery(set, PimItem::idFullColumnName(), qb); + } + if (collection.isValid()) { + if (collection.isVirtual() || collection.resource().isVirtual()) { + qb.addJoin(QueryBuilder::InnerJoin, + CollectionPimItemRelation::tableName(), + CollectionPimItemRelation::rightFullColumnName(), + PimItem::idFullColumnName()); + qb.addValueCondition(CollectionPimItemRelation::leftFullColumnName(), Query::Equals, collection.id()); + } else { + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, collection.id()); + } + } +} + +void ItemQueryHelper::itemSetToQuery(const ImapSet &set, const CommandContext &context, QueryBuilder &qb) +{ + if (context.collectionId() >= 0) { + itemSetToQuery(set, qb, context.collection()); + } else { + itemSetToQuery(set, qb); + } + + const auto tagId = context.tagId(); + if (tagId.has_value()) { + // When querying for items by tag, only return matches from that resource + if (context.resource().isValid()) { + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + } + qb.addJoin(QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName()); + qb.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, *tagId); + } +} + +void ItemQueryHelper::remoteIdToQuery(const QStringList &rids, const CommandContext &context, QueryBuilder &qb) +{ + if (rids.size() == 1) { + qb.addValueCondition(PimItem::remoteIdFullColumnName(), Query::Equals, rids.first()); + } else { + qb.addValueCondition(PimItem::remoteIdFullColumnName(), Query::In, rids); + } + + if (context.resource().isValid()) { + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + } + if (context.collectionId() > 0) { + qb.addValueCondition(PimItem::collectionIdFullColumnName(), Query::Equals, context.collectionId()); + } + + const auto tagId = context.tagId(); + if (tagId.has_value()) { + qb.addJoin(QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName()); + qb.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, *tagId); + } +} + +void ItemQueryHelper::gidToQuery(const QStringList &gids, const CommandContext &context, QueryBuilder &qb) +{ + if (gids.size() == 1) { + qb.addValueCondition(PimItem::gidFullColumnName(), Query::Equals, gids.first()); + } else { + qb.addValueCondition(PimItem::gidFullColumnName(), Query::In, gids); + } + + const auto tagId = context.tagId(); + if (tagId.has_value()) { + // When querying for items by tag, only return matches from that resource + if (context.resource().isValid()) { + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + } + qb.addJoin(QueryBuilder::InnerJoin, PimItemTagRelation::tableName(), PimItem::idFullColumnName(), PimItemTagRelation::leftFullColumnName()); + qb.addValueCondition(PimItemTagRelation::rightFullColumnName(), Query::Equals, *tagId); + } +} + +void ItemQueryHelper::scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb) +{ + // Handle fetch by collection/tag + if (scope.scope() == Scope::Invalid && !context.isEmpty()) { + itemSetToQuery(ImapSet(), context, qb); + return; + } + + if (scope.scope() == Scope::Uid) { + itemSetToQuery(scope.uidSet(), context, qb); + return; + } + + if (scope.scope() == Scope::Gid) { + ItemQueryHelper::gidToQuery(scope.gidSet(), context, qb); + return; + } + + if (context.collectionId() <= 0 && !context.resource().isValid()) { + throw HandlerException("Operations based on remote identifiers require a resource or collection context"); + } + + if (scope.scope() == Scope::Rid) { + ItemQueryHelper::remoteIdToQuery(scope.ridSet(), context, qb); + return; + } else if (scope.scope() == Scope::HierarchicalRid) { + QVector hridChain = scope.hridChain(); + const Scope::HRID itemHRID = hridChain.takeFirst(); + const Collection parentCol = CollectionQueryHelper::resolveHierarchicalRID(hridChain, context.resource().id()); + const Collection oldSelection = context.collection(); + CommandContext tmpContext(context); + tmpContext.setCollection(parentCol); + remoteIdToQuery(QStringList() << itemHRID.remoteId, tmpContext, qb); + return; + } + + throw HandlerException("Dude, WTF?!?"); +} diff --git a/src/server/storage/itemqueryhelper.h b/src/server/storage/itemqueryhelper.h new file mode 100644 index 0000000..ef9fa60 --- /dev/null +++ b/src/server/storage/itemqueryhelper.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" + +namespace Akonadi +{ +class ImapSet; +class Scope; + +namespace Server +{ +class CommandContext; +class QueryBuilder; + +/** + Helper methods to generate WHERE clauses for item queries based on the item set + used in the protocol. +*/ +namespace ItemQueryHelper +{ +/** + Add conditions to @p qb for the given item set @p set. If @p collection is valid, + only items in this collection are considered. +*/ +void itemSetToQuery(const ImapSet &set, QueryBuilder &qb, const Collection &collection = Collection()); + +/** + Convenience method, does essentially the same as the one above. +*/ +void itemSetToQuery(const ImapSet &set, const CommandContext &context, QueryBuilder &qb); + +/** + Add conditions to @p qb for the given remote identifier @p rid. + The rid context is taken from @p context. +*/ +void remoteIdToQuery(const QStringList &rids, const CommandContext &context, QueryBuilder &qb); +void gidToQuery(const QStringList &gids, const CommandContext &context, QueryBuilder &qb); + +/** + Add conditions to @p qb for the given item operation scope @p scope. + The rid context is taken from @p context, if none is specified an exception is thrown. +*/ +void scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb); +} + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/itemretrievaljob.cpp b/src/server/storage/itemretrievaljob.cpp new file mode 100644 index 0000000..65f2cb0 --- /dev/null +++ b/src/server/storage/itemretrievaljob.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemretrievaljob.h" +#include "akonadiserver_debug.h" +#include "resourceinterface.h" + +#include + +using namespace Akonadi::Server; + +AbstractItemRetrievalJob::AbstractItemRetrievalJob(ItemRetrievalRequest req, QObject *parent) + : QObject(parent) + , m_result(std::move(req)) +{ +} + +ItemRetrievalJob::~ItemRetrievalJob() +{ + Q_ASSERT(!m_active); +} + +void ItemRetrievalJob::start() +{ + qCDebug(AKONADISERVER_LOG) << "processing retrieval request for item" << request().ids << " parts:" << request().parts + << " of resource:" << request().resourceId; + + // call the resource + if (m_interface) { + m_active = true; + auto reply = m_interface->requestItemDelivery(request().ids, request().parts); + auto watcher = new QDBusPendingCallWatcher(reply, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, &ItemRetrievalJob::callFinished); + } else { + m_result.errorMsg = QStringLiteral("Unable to contact resource"); + Q_EMIT requestCompleted(this); + deleteLater(); + } +} + +void ItemRetrievalJob::kill() +{ + m_active = false; + m_result.errorMsg = QStringLiteral("Request cancelled"); + Q_EMIT requestCompleted(this); +} + +void ItemRetrievalJob::callFinished(QDBusPendingCallWatcher *watcher) +{ + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (m_active) { + m_active = false; + if (reply.isError()) { + m_result.errorMsg = QStringLiteral("Unable to retrieve item from resource: %1").arg(reply.error().message()); + } + Q_EMIT requestCompleted(this); + } + deleteLater(); +} diff --git a/src/server/storage/itemretrievaljob.h b/src/server/storage/itemretrievaljob.h new file mode 100644 index 0000000..4755e1a --- /dev/null +++ b/src/server/storage/itemretrievaljob.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "itemretrievalrequest.h" + +class QDBusPendingCallWatcher; +class OrgFreedesktopAkonadiResourceInterface; + +namespace Akonadi +{ +namespace Server +{ +class ItemRetrievalRequest; + +class AbstractItemRetrievalJob : public QObject +{ + Q_OBJECT +public: + AbstractItemRetrievalJob(ItemRetrievalRequest req, QObject *parent); + ~AbstractItemRetrievalJob() override = default; + + virtual void start() = 0; + virtual void kill() = 0; + + const ItemRetrievalRequest &request() const + { + return m_result.request; + } + const ItemRetrievalResult &result() const + { + return m_result; + } + +Q_SIGNALS: + void requestCompleted(Akonadi::Server::AbstractItemRetrievalJob *job); + +protected: + ItemRetrievalResult m_result; +}; + +/// Async D-Bus retrieval, no modification of the request (thus no need for locking) +class ItemRetrievalJob : public AbstractItemRetrievalJob +{ + Q_OBJECT +public: + ItemRetrievalJob(ItemRetrievalRequest req, QObject *parent) + : AbstractItemRetrievalJob(std::move(req), parent) + { + } + + void setInterface(OrgFreedesktopAkonadiResourceInterface *interface) + { + m_interface = interface; + } + + ~ItemRetrievalJob() override; + void start() override; + void kill() override; + +private Q_SLOTS: + void callFinished(QDBusPendingCallWatcher *watcher); + +private: + bool m_active = false; + OrgFreedesktopAkonadiResourceInterface *m_interface = nullptr; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/itemretrievalmanager.cpp b/src/server/storage/itemretrievalmanager.cpp new file mode 100644 index 0000000..48ce8b5 --- /dev/null +++ b/src/server/storage/itemretrievalmanager.cpp @@ -0,0 +1,224 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemretrievalmanager.h" +#include "akonadiserver_debug.h" +#include "itemretrievaljob.h" + +#include "resourceinterface.h" + +#include + +#include +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(Akonadi::Server::ItemRetrievalResult) + +class ItemRetrievalJobFactory : public AbstractItemRetrievalJobFactory +{ + AbstractItemRetrievalJob *retrievalJob(ItemRetrievalRequest request, QObject *parent) override + { + return new ItemRetrievalJob(std::move(request), parent); + } +}; + +ItemRetrievalManager::ItemRetrievalManager(QObject *parent) + : ItemRetrievalManager(std::make_unique(), parent) +{ +} + +ItemRetrievalManager::ItemRetrievalManager(std::unique_ptr factory, QObject *parent) + : AkThread(QStringLiteral("ItemRetrievalManager"), QThread::HighPriority, parent) + , mJobFactory(std::move(factory)) +{ + qRegisterMetaType("Akonadi::Server::ItemRetrievalResult"); + qDBusRegisterMetaType(); +} + +ItemRetrievalManager::~ItemRetrievalManager() +{ + quitThread(); +} + +void ItemRetrievalManager::init() +{ + AkThread::init(); + + QDBusConnection conn = QDBusConnection::sessionBus(); + connect(conn.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &ItemRetrievalManager::serviceOwnerChanged); + connect(this, &ItemRetrievalManager::requestAdded, this, &ItemRetrievalManager::processRequest, Qt::QueuedConnection); +} + +// called within the retrieval thread +void ItemRetrievalManager::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) +{ + Q_UNUSED(newOwner) + if (oldOwner.isEmpty()) { + return; + } + const auto service = DBus::parseAgentServiceName(serviceName); + if (!service.has_value() || service->agentType != DBus::Resource) { + return; + } + qCDebug(AKONADISERVER_LOG) << "ItemRetrievalManager lost connection to resource" << serviceName << ", discarding cached interface"; + mResourceInterfaces.erase(service->identifier); +} + +// called within the retrieval thread +org::freedesktop::Akonadi::Resource *ItemRetrievalManager::resourceInterface(const QString &id) +{ + if (id.isEmpty()) { + return nullptr; + } + + auto ifaceIt = mResourceInterfaces.find(id); + if (ifaceIt != mResourceInterfaces.cend() && ifaceIt->second->isValid()) { + return ifaceIt->second.get(); + } + + auto iface = + std::make_unique(DBus::agentServiceName(id, DBus::Resource), QStringLiteral("/"), QDBusConnection::sessionBus()); + if (!iface->isValid()) { + qCCritical(AKONADISERVER_LOG, + "Cannot connect to agent instance with identifier '%s', error message: '%s'", + qUtf8Printable(id), + qUtf8Printable(iface ? iface->lastError().message() : QString())); + return nullptr; + } + // DBus calls can take some time to reply -- e.g. if a huge local mbox has to be parsed first. + iface->setTimeout(5 * 60 * 1000); // 5 minutes, rather than 25 seconds + std::tie(ifaceIt, std::ignore) = mResourceInterfaces.emplace(id, std::move(iface)); + return ifaceIt->second.get(); +} + +// called from any thread +void ItemRetrievalManager::requestItemDelivery(ItemRetrievalRequest req) +{ + QWriteLocker locker(&mLock); + qCDebug(AKONADISERVER_LOG) << "ItemRetrievalManager posting retrieval request for items" << req.ids << "to" << req.resourceId << ". There are" + << mPendingRequests.size() << "request queues and" << mPendingRequests[req.resourceId].size() << "items mine"; + mPendingRequests[req.resourceId].emplace_back(std::move(req)); + locker.unlock(); + + Q_EMIT requestAdded(); +} + +QVector ItemRetrievalManager::scheduleJobsForIdleResourcesLocked() +{ + QVector newJobs; + for (auto it = mPendingRequests.begin(); it != mPendingRequests.end();) { + if (it->second.empty()) { + it = mPendingRequests.erase(it); + continue; + } + + if (!mCurrentJobs.contains(it->first) || mCurrentJobs.value(it->first) == nullptr) { + // TODO: check if there is another one for the same uid with more parts requested + auto req = std::move(it->second.front()); + it->second.pop_front(); + Q_ASSERT(req.resourceId == it->first); + auto job = mJobFactory->retrievalJob(std::move(req), this); + connect(job, &AbstractItemRetrievalJob::requestCompleted, this, &ItemRetrievalManager::retrievalJobFinished); + mCurrentJobs.insert(job->request().resourceId, job); + // delay job execution until after we unlocked the mutex, since the job can emit the finished signal immediately in some cases + newJobs.append(job); + qCDebug(AKONADISERVER_LOG) << "ItemRetrievalJob" << job << "started for request" << job->request().id; + } + ++it; + } + + return newJobs; +} + +// called within the retrieval thread +void ItemRetrievalManager::processRequest() +{ + QWriteLocker locker(&mLock); + // look for idle resources + auto newJobs = scheduleJobsForIdleResourcesLocked(); + // someone asked as to process requests although everything is done already, he might still be waiting + if (mPendingRequests.empty() && mCurrentJobs.isEmpty() && newJobs.isEmpty()) { + return; + } + locker.unlock(); + + // Start the jobs + for (auto job : newJobs) { + if (auto j = qobject_cast(job)) { + j->setInterface(resourceInterface(j->request().resourceId)); + } + job->start(); + } +} + +namespace +{ +bool isSubsetOf(const QByteArrayList &superset, const QByteArrayList &subset) +{ + // For very small lists like these, this is faster than copy, sort and std::include + return std::all_of(subset.cbegin(), subset.cend(), [&superset](const auto &val) { + return superset.contains(val); + }); +} + +} + +void ItemRetrievalManager::retrievalJobFinished(AbstractItemRetrievalJob *job) +{ + const auto &request = job->request(); + const auto &result = job->result(); + + if (result.errorMsg.has_value()) { + qCWarning(AKONADISERVER_LOG) << "ItemRetrievalJob for request" << request.id << "finished with error:" << *result.errorMsg; + } else { + qCInfo(AKONADISERVER_LOG) << "ItemRetrievalJob for request" << request.id << "finished"; + } + + QWriteLocker locker(&mLock); + Q_ASSERT(mCurrentJobs.contains(request.resourceId)); + mCurrentJobs.remove(request.resourceId); + // Check if there are any pending requests that are satisfied by this retrieval job + auto &requests = mPendingRequests[request.resourceId]; + for (auto it = requests.begin(); it != requests.end();) { + // TODO: also complete requests that are subset of the completed one + if (it->ids == request.ids && isSubsetOf(request.parts, it->parts)) { + qCDebug(AKONADISERVER_LOG) << "Someone else requested items " << request.ids << "as well, marking as processed."; + ItemRetrievalResult otherResult{std::move(*it)}; + otherResult.errorMsg = result.errorMsg; + Q_EMIT requestFinished(otherResult); + it = requests.erase(it); + } else { + ++it; + } + } + locker.unlock(); + + Q_EMIT requestFinished(result); + Q_EMIT requestAdded(); // trigger processRequest() again, in case there is more in the queues +} + +// Can be called from any thread +void ItemRetrievalManager::triggerCollectionSync(const QString &resource, qint64 colId) +{ + QTimer::singleShot(0, this, [this, resource, colId]() { + if (auto interface = resourceInterface(resource)) { + interface->synchronizeCollection(colId); + } + }); +} + +void ItemRetrievalManager::triggerCollectionTreeSync(const QString &resource) +{ + QTimer::singleShot(0, this, [this, resource]() { + if (auto interface = resourceInterface(resource)) { + interface->synchronizeCollectionTree(); + } + }); +} diff --git a/src/server/storage/itemretrievalmanager.h b/src/server/storage/itemretrievalmanager.h new file mode 100644 index 0000000..04e0719 --- /dev/null +++ b/src/server/storage/itemretrievalmanager.h @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akthread.h" +#include "itemretrievalrequest.h" +#include "itemretriever.h" +#include + +#include +class QObject; +#include +#include + +#include + +class OrgFreedesktopAkonadiResourceInterface; + +namespace Akonadi +{ +namespace Server +{ +class Collection; +class ItemRetrievalJob; +class AbstractItemRetrievalJob; + +class AbstractItemRetrievalJobFactory +{ +public: + virtual ~AbstractItemRetrievalJobFactory() = default; + + virtual AbstractItemRetrievalJob *retrievalJob(ItemRetrievalRequest request, QObject *parent) = 0; + +protected: + explicit AbstractItemRetrievalJobFactory() = default; + +private: + Q_DISABLE_COPY_MOVE(AbstractItemRetrievalJobFactory) +}; + +/** Manages and processes item retrieval requests. */ +class ItemRetrievalManager : public AkThread +{ + Q_OBJECT +public: + explicit ItemRetrievalManager(QObject *parent = nullptr); + explicit ItemRetrievalManager(std::unique_ptr factory, QObject *parent = nullptr); + ~ItemRetrievalManager() override; + + /** + * Added for convenience. ItemRetrievalManager takes ownership over the + * pointer and deletes it when the request is processed. + */ + virtual void requestItemDelivery(ItemRetrievalRequest request); + + void triggerCollectionSync(const QString &resource, qint64 colId); + void triggerCollectionTreeSync(const QString &resource); + +Q_SIGNALS: + void requestFinished(const Akonadi::Server::ItemRetrievalResult &result); + void requestAdded(); + +private: + OrgFreedesktopAkonadiResourceInterface *resourceInterface(const QString &id); + QVector scheduleJobsForIdleResourcesLocked(); + +private Q_SLOTS: + void init() override; + + void serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner); + void processRequest(); + void retrievalJobFinished(Akonadi::Server::AbstractItemRetrievalJob *job); + +protected: + std::unique_ptr mJobFactory; + + /// Protects mPendingRequests and every Request object posted to it + QReadWriteLock mLock; + /// Used to let requesting threads wait until the request has been processed + QWaitCondition mWaitCondition; + + /// Pending requests queues, one per resource + std::unordered_map> mPendingRequests; + /// Currently running jobs, one per resource + QHash mCurrentJobs; + + // resource dbus interface cache + std::unordered_map> mResourceInterfaces; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/itemretrievalrequest.cpp b/src/server/storage/itemretrievalrequest.cpp new file mode 100644 index 0000000..a98707d --- /dev/null +++ b/src/server/storage/itemretrievalrequest.cpp @@ -0,0 +1,16 @@ +/* + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemretrievalrequest.h" + +using namespace Akonadi::Server; + +ItemRetrievalRequest::Id ItemRetrievalRequest::lastId{0}; + +ItemRetrievalRequest::ItemRetrievalRequest() + : id(lastId.next()) +{ +} diff --git a/src/server/storage/itemretrievalrequest.h b/src/server/storage/itemretrievalrequest.h new file mode 100644 index 0000000..e72ad31 --- /dev/null +++ b/src/server/storage/itemretrievalrequest.h @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace Akonadi +{ +namespace Server +{ +class ItemRetrievalRequest; + +/// Details of a single item retrieval request +class ItemRetrievalRequest +{ +public: + struct Id { + explicit Id(uint32_t value) + : mValue(value){}; + bool operator==(Id other) const + { + return mValue == other.mValue; + } + + private: + uint32_t mValue; + Id next() + { + return Id{++mValue}; + } + + friend class ItemRetrievalRequest; + friend QDebug operator<<(QDebug, Id); + }; + + explicit ItemRetrievalRequest(); + + Id id; + QVector ids; + QString resourceId; + QByteArrayList parts; // list instead of vector to simplify client-side handling + +private: + static Id lastId; +}; + +class ItemRetrievalResult +{ +public: + explicit ItemRetrievalResult() = default; // don't use, sadly Qt metatype system requires type to be default-constructible + ItemRetrievalResult(ItemRetrievalRequest request) + : request(std::move(request)) + { + } + + ItemRetrievalRequest request; + + std::optional errorMsg{}; +}; + +inline QDebug operator<<(QDebug dbg, ItemRetrievalRequest::Id id) +{ + dbg.nospace() << id.mValue; + return dbg.space(); +} + +} // namespace Server +} // namespace Akonadi diff --git a/src/server/storage/itemretriever.cpp b/src/server/storage/itemretriever.cpp new file mode 100644 index 0000000..3450ec3 --- /dev/null +++ b/src/server/storage/itemretriever.cpp @@ -0,0 +1,440 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + SPDX-FileCopyrightText: 2010 Milian Wolff + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemretriever.h" + +#include "akonadi.h" +#include "connection.h" +#include "storage/datastore.h" +#include "storage/itemqueryhelper.h" +#include "storage/itemretrievalmanager.h" +#include "storage/itemretrievalrequest.h" +#include "storage/parthelper.h" +#include "storage/parttypehelper.h" +#include "storage/querybuilder.h" +#include "storage/selectquerybuilder.h" +#include "utils.h" + +#include +#include + +#include + +#include "akonadiserver_debug.h" + +using namespace Akonadi; +using namespace Akonadi::Server; +using namespace AkRanges; + +Q_DECLARE_METATYPE(ItemRetrievalResult) + +ItemRetriever::ItemRetriever(ItemRetrievalManager &manager, Connection *connection, const CommandContext &context) + : mItemRetrievalManager(manager) + , mConnection(connection) + , mContext(context) + , mFullPayload(false) + , mRecursive(false) + , mCanceled(false) +{ + qRegisterMetaType("Akonadi::Server::ItemRetrievalResult"); + if (mConnection) { + connect(mConnection, &Connection::disconnected, this, [this]() { + mCanceled = true; + }); + } +} + +Connection *ItemRetriever::connection() const +{ + return mConnection; +} + +void ItemRetriever::setRetrieveParts(const QVector &parts) +{ + mParts = parts; + std::sort(mParts.begin(), mParts.end()); + mParts.erase(std::unique(mParts.begin(), mParts.end()), mParts.end()); + + // HACK, we need a full payload available flag in PimItem + if (mFullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) { + mParts.append(AKONADI_PARAM_PLD_RFC822); + } +} + +void ItemRetriever::setItemSet(const ImapSet &set, const Collection &collection) +{ + mItemSet = set; + mCollection = collection; +} + +void ItemRetriever::setItemSet(const ImapSet &set, bool isUid) +{ + if (!isUid && mContext.collectionId() >= 0) { + setItemSet(set, mContext.collection()); + } else { + setItemSet(set); + } +} + +void ItemRetriever::setItem(Entity::Id id) +{ + ImapSet set; + set.add(ImapInterval(id, id)); + mItemSet = set; + mCollection = Collection(); +} + +void ItemRetriever::setRetrieveFullPayload(bool fullPayload) +{ + mFullPayload = fullPayload; + // HACK, we need a full payload available flag in PimItem + if (fullPayload && !mParts.contains(AKONADI_PARAM_PLD_RFC822)) { + mParts.append(AKONADI_PARAM_PLD_RFC822); + } +} + +void ItemRetriever::setCollection(const Collection &collection, bool recursive) +{ + mCollection = collection; + mItemSet = ImapSet(); + mRecursive = recursive; +} + +void ItemRetriever::setScope(const Scope &scope) +{ + mScope = scope; +} + +Scope ItemRetriever::scope() const +{ + return mScope; +} + +void ItemRetriever::setChangedSince(const QDateTime &changedSince) +{ + mChangedSince = changedSince; +} + +QVector ItemRetriever::retrieveParts() const +{ + return mParts; +} + +enum QueryColumns { + PimItemIdColumn, + + CollectionIdColumn, + ResourceIdColumn, + + PartTypeNameColumn, + PartDatasizeColumn +}; + +QSqlQuery ItemRetriever::buildQuery() const +{ + QueryBuilder qb(PimItem::tableName()); + + qb.addJoin(QueryBuilder::InnerJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + + qb.addJoin(QueryBuilder::LeftJoin, Part::tableName(), PimItem::idFullColumnName(), Part::pimItemIdFullColumnName()); + + Query::Condition partTypeJoinCondition; + partTypeJoinCondition.addColumnCondition(Part::partTypeIdFullColumnName(), Query::Equals, PartType::idFullColumnName()); + if (!mFullPayload && !mParts.isEmpty()) { + partTypeJoinCondition.addCondition(PartTypeHelper::conditionFromFqNames(mParts)); + } + partTypeJoinCondition.addValueCondition(PartType::nsFullColumnName(), Query::Equals, QStringLiteral("PLD")); + qb.addJoin(QueryBuilder::LeftJoin, PartType::tableName(), partTypeJoinCondition); + + qb.addColumn(PimItem::idFullColumnName()); + qb.addColumn(PimItem::collectionIdFullColumnName()); + qb.addColumn(Collection::resourceIdFullColumnName()); + qb.addColumn(PartType::nameFullColumnName()); + qb.addColumn(Part::datasizeFullColumnName()); + + if (!mItemSet.isEmpty() || mCollection.isValid()) { + ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection); + } else { + ItemQueryHelper::scopeToQuery(mScope, mContext, qb); + } + + // prevent a resource to trigger item retrieval from itself + if (mConnection) { + const Resource res = Resource::retrieveByName(QString::fromUtf8(mConnection->sessionId())); + if (res.isValid()) { + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::NotEquals, res.id()); + } + } + + if (mChangedSince.isValid()) { + qb.addValueCondition(PimItem::datetimeFullColumnName(), Query::GreaterOrEqual, mChangedSince.toUTC()); + } + + qb.addSortColumn(PimItem::idFullColumnName(), Query::Ascending); + + if (!qb.exec()) { + mLastError = "Unable to retrieve items"; + throw ItemRetrieverException(mLastError); + } + + qb.query().next(); + + return qb.query(); +} + +namespace +{ +bool hasAllParts(const ItemRetrievalRequest &req, const QSet &availableParts) +{ + return std::all_of(req.parts.begin(), req.parts.end(), [&availableParts](const auto &part) { + return availableParts.contains(part); + }); +} +} + +bool ItemRetriever::runItemRetrievalRequests(std::list requests) // clazy:exclude=function-args-by-ref +{ + QEventLoop eventLoop; + std::vector pendingRequests; + connect(&mItemRetrievalManager, + &ItemRetrievalManager::requestFinished, + this, + [this, &eventLoop, &pendingRequests](const ItemRetrievalResult &result) { // clazy:exclude=lambda-in-connect + const auto requestId = std::find(pendingRequests.begin(), pendingRequests.end(), result.request.id); + if (requestId != pendingRequests.end()) { + if (mCanceled) { + eventLoop.exit(1); + } else if (result.errorMsg.has_value()) { + mLastError = result.errorMsg->toUtf8(); + eventLoop.exit(1); + } else { + Q_EMIT itemsRetrieved(result.request.ids); + pendingRequests.erase(requestId); + if (pendingRequests.empty()) { + eventLoop.quit(); + } + } + } + }); + + if (mConnection) { + connect(mConnection, &Connection::connectionClosing, &eventLoop, [&eventLoop]() { + eventLoop.exit(1); + }); + } + + for (auto &&request : requests) { + if ((!mFullPayload && request.parts.isEmpty()) || request.ids.isEmpty()) { + continue; + } + + // TODO: how should we handle retrieval errors here? so far they have been ignored, + // which makes sense in some cases, do we need a command parameter for this? + try { + // Request is deleted inside ItemRetrievalManager, so we need to take + // a copy here + // const auto ids = request->ids; + pendingRequests.push_back(request.id); + mItemRetrievalManager.requestItemDelivery(std::move(request)); + } catch (const ItemRetrieverException &e) { + qCCritical(AKONADISERVER_LOG) << e.type() << ": " << e.what(); + mLastError = e.what(); + return false; + } + } + + if (!pendingRequests.empty()) { + if (eventLoop.exec()) { + return false; + } + } + + return true; +} + +std::optional ItemRetriever::prepareRequests(QSqlQuery &query, const QByteArrayList &parts) +{ + QHash resourceIdNameCache; + std::list requests; + QHash colRequests; + QHash itemRequests; + QVector readyItems; + qint64 prevPimItemId = -1; + QSet availableParts; + auto lastRequest = requests.end(); + while (query.isValid()) { + const qint64 pimItemId = query.value(PimItemIdColumn).toLongLong(); + const qint64 collectionId = query.value(CollectionIdColumn).toLongLong(); + const qint64 resourceId = query.value(ResourceIdColumn).toLongLong(); + const auto itemIter = itemRequests.constFind(pimItemId); + + if (Q_UNLIKELY(mCanceled)) { + return std::nullopt; + } + + if (pimItemId == prevPimItemId) { + if (query.value(PartTypeNameColumn).isNull()) { + // This is not the first part of the Item we saw, but LEFT JOIN PartTable + // returned a null row - that means the row is an ATR part + // which we don't care about + query.next(); + continue; + } + } else { + if (lastRequest != requests.end()) { + if (hasAllParts(*lastRequest, availableParts)) { + // We went through all parts of a single item, if we have all + // parts available in the DB and they are not expired, then + // exclude this item from the retrieval + lastRequest->ids.removeOne(prevPimItemId); + itemRequests.remove(prevPimItemId); + readyItems.push_back(prevPimItemId); + } + } + availableParts.clear(); + prevPimItemId = pimItemId; + } + + if (itemIter != itemRequests.constEnd()) { + lastRequest = itemIter.value(); + } else { + const auto colIt = colRequests.find(collectionId); + lastRequest = (colIt == colRequests.end()) ? requests.end() : colIt.value(); + if (lastRequest == requests.end() || lastRequest->ids.size() > 100) { + requests.emplace_front(ItemRetrievalRequest{}); + lastRequest = requests.begin(); + lastRequest->ids.push_back(pimItemId); + auto resIter = resourceIdNameCache.find(resourceId); + if (resIter == resourceIdNameCache.end()) { + resIter = resourceIdNameCache.insert(resourceId, Resource::retrieveById(resourceId).name()); + } + lastRequest->resourceId = *resIter; + lastRequest->parts = parts; + colRequests.insert(collectionId, lastRequest); + itemRequests.insert(pimItemId, lastRequest); + } else { + lastRequest->ids.push_back(pimItemId); + itemRequests.insert(pimItemId, lastRequest); + colRequests.insert(collectionId, lastRequest); + } + } + Q_ASSERT(lastRequest != requests.end()); + + if (query.value(PartTypeNameColumn).isNull()) { + // LEFT JOIN did not find anything, retrieve all parts + query.next(); + continue; + } + + qint64 datasize = query.value(PartDatasizeColumn).toLongLong(); + const QByteArray partName = Utils::variantToByteArray(query.value(PartTypeNameColumn)); + Q_ASSERT(!partName.startsWith(AKONADI_PARAM_PLD)); + if (datasize <= 0) { + // request update for this part + if (mFullPayload && !lastRequest->parts.contains(partName)) { + lastRequest->parts.push_back(partName); + } + } else { + // add the part to list of available parts, we will compare it with + // the list of request parts once we handle all parts of this item + availableParts.insert(partName); + } + query.next(); + } + query.finish(); + + // Post-check in case we only queried one item thus did not reach the check + // at the beginning of the while() loop above + if (lastRequest != requests.end() && hasAllParts(*lastRequest, availableParts)) { + lastRequest->ids.removeOne(prevPimItemId); + readyItems.push_back(prevPimItemId); + // No need to update the hashtable at this point + } + + return PreparedRequests{std::move(requests), std::move(readyItems)}; +} + +bool ItemRetriever::exec() +{ + if (mParts.isEmpty() && !mFullPayload) { + return true; + } + + verifyCache(); + + QSqlQuery query = buildQuery(); + const auto parts = mParts | Views::filter([](const auto &part) { + return part.startsWith(AKONADI_PARAM_PLD); + }) + | Views::transform([](const auto &part) { + return part.mid(4); + }) + | Actions::toQList; + + auto requests = prepareRequests(query, parts); + if (!requests.has_value()) { + return false; + } + + if (!requests->readyItems.isEmpty()) { + Q_EMIT itemsRetrieved(requests->readyItems); + } + + if (!runItemRetrievalRequests(std::move(requests->requests))) { + return false; + } + + // retrieve items in child collections if requested + bool result = true; + if (mRecursive && mCollection.isValid()) { + Q_FOREACH (const Collection &col, mCollection.children()) { + ItemRetriever retriever(mItemRetrievalManager, mConnection, mContext); + retriever.setCollection(col, mRecursive); + retriever.setRetrieveParts(mParts); + retriever.setRetrieveFullPayload(mFullPayload); + connect(&retriever, &ItemRetriever::itemsRetrieved, this, &ItemRetriever::itemsRetrieved); + result = retriever.exec(); + if (!result) { + break; + } + } + } + + return result; +} + +void ItemRetriever::verifyCache() +{ + if (!connection() || !connection()->verifyCacheOnRetrieval()) { + return; + } + + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::InnerJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); + qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); + qb.addValueCondition(Part::dataFullColumnName(), Query::IsNot, QVariant()); + if (mScope.scope() != Scope::Invalid) { + ItemQueryHelper::scopeToQuery(mScope, mContext, qb); + } else { + ItemQueryHelper::itemSetToQuery(mItemSet, qb, mCollection); + } + + if (!qb.exec()) { + mLastError = QByteArrayLiteral("Unable to query parts."); + throw ItemRetrieverException(mLastError); + } + + const Part::List externalParts = qb.result(); + for (Part part : externalParts) { + PartHelper::verify(part); + } +} + +QByteArray ItemRetriever::lastError() const +{ + return mLastError; +} diff --git a/src/server/storage/itemretriever.h b/src/server/storage/itemretriever.h new file mode 100644 index 0000000..1a36704 --- /dev/null +++ b/src/server/storage/itemretriever.h @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "../exception.h" +#include "entities.h" + +#include +#include + +#include + +AKONADI_EXCEPTION_MAKE_INSTANCE(ItemRetrieverException); + +namespace Akonadi +{ +namespace Server +{ +class Connection; +class CommandContext; +class ItemRetrievalManager; +class ItemRetrievalRequest; + +/** + Helper class for retrieving missing items parts from remote resources. + + Stuff in here happens in the calling thread and does not access shared data. + + @todo make usable for Fetch by allowing to share queries +*/ +class ItemRetriever : public QObject +{ + Q_OBJECT + +public: + explicit ItemRetriever(ItemRetrievalManager &manager, Connection *connection, const CommandContext &context); + + Connection *connection() const; + + void setRetrieveParts(const QVector &parts); + QVector retrieveParts() const; + void setRetrieveFullPayload(bool fullPayload); + void setChangedSince(const QDateTime &changedSince); + void setItemSet(const ImapSet &set, const Collection &collection = Collection()); + void setItemSet(const ImapSet &set, bool isUid); + void setItem(Entity::Id id); + /** Retrieve all items in the given collection. */ + void setCollection(const Collection &collection, bool recursive = true); + + /** Retrieve all items matching the given item scope. */ + void setScope(const Scope &scope); + Scope scope() const; + + bool exec(); + + QByteArray lastError() const; + +Q_SIGNALS: + void itemsRetrieved(const QVector &ids); + +private: + QSqlQuery buildQuery() const; + + /** + * Checks if external files are still present + * This costs extra, but allows us to automatically recover from something changing the external file storage. + */ + void verifyCache(); + + /// Execute the retrieval + bool runItemRetrievalRequests(std::list requests); + struct PreparedRequests { + std::list requests; + QVector readyItems; + }; + std::optional prepareRequests(QSqlQuery &query, const QByteArrayList &parts); + + Akonadi::ImapSet mItemSet; + Collection mCollection; + Scope mScope; + ItemRetrievalManager &mItemRetrievalManager; + Connection *mConnection = nullptr; + const CommandContext &mContext; + QVector mParts; + bool mFullPayload; + bool mRecursive; + QDateTime mChangedSince; + mutable QByteArray mLastError; + bool mCanceled; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/mysql-global-mobile.conf b/src/server/storage/mysql-global-mobile.conf new file mode 100644 index 0000000..00ca954 --- /dev/null +++ b/src/server/storage/mysql-global-mobile.conf @@ -0,0 +1,103 @@ +# +# Global Akonadi MySQL server settings, +# These settings can be adjusted using $HOME/.config/akonadi/mysql-local.conf +# +# Based on advice by Kris Köhntopp +# +[mysqld] + +# strict query parsing/interpretation +# TODO: make Akonadi work with those settings enabled +# sql_mode=strict_trans_tables,strict_all_tables,strict_error_for_division_by_zero,no_auto_create_user,no_auto_value_on_zero,no_engine_substitution,no_zero_date,no_zero_in_date,only_full_group_by,pipes_as_concat +# sql_mode=strict_trans_tables + +# DEBUGGING: +# log all queries, useful for debugging but generates an enormous amount of data +# log=mysql.full +# log queries slower than n seconds, log file name relative to datadir (for debugging only) +# log_slow_queries=mysql.slow +# long_query_time=1 +# log queries not using indices, debug only, disable for production use +# log_queries_not_using_indexes=1 +# +# mesure database size and adjust innodb_buffer_pool_size +# SELECT sum(data_length) as bla, sum(index_length) as blub FROM information_schema.tables WHERE table_schema not in ("mysql", "information_schema"); + +# NOTES: +# Keep Innob_log_waits and keep Innodb_buffer_pool_wait_free small (see show global status like "inno%", show global variables) + +#expire_logs_days=3 + +#sync_bin_log=0 + +# Use UTF-8 encoding for tables +character_set_server=utf8 +collation_server=utf8_general_ci + +# use InnoDB for transactions and better crash recovery +default_storage_engine=innodb + +# memory buffer InnoDB uses to cache data and indexes of its tables (default:128M) +# Larger values means less I/O +innodb_buffer_pool_size=8M + +# Create a .ibd file for each table (default:0) +innodb_file_per_table=1 + +# Write out the log buffer to the log file at each commit (default:1) +innodb_flush_log_at_trx_commit=2 + +# Buffer size used to write to the log files on disk (default:1M for builtin, 8M for plugin) +# larger values means less I/O +innodb_log_buffer_size=1M + +# Size of each log file in a log group (default:5M) larger means less I/O but more time for recovery. +innodb_log_file_size=2M + +# Enable varchar index keys up to 3072 bytes (1024 characters), compared to 768 bytes (255 characters) with normal settings +innodb_large_prefix=1 + +# # error log file name, relative to datadir (default:hostname.err) +log_error=mysql.err + +# print warnings and connection errors (default:1) +loose_log_warnings=2 + +# Convert table named to lowercase +lower_case_table_names=1 + +# Maximum size of one packet or any generated/intermediate string. (default:1M) +max_allowed_packet=32M + +# Maximum simultaneous connections allowed (default:100) +max_connections=256 + +# The two options below make no sense with prepared statements and/or transactions +# (make sense when having the same query multiple times) + +# Memory allocated for caching query results (default:0 (disabled)) +loose_query_cache_size=0 + +# Do not cache results (default:1) +loose_query_cache_type=0 + +# Do not use the privileges mechanisms +skip_grant_tables + +# Do not listen for TCP/IP connections at all +skip_networking + +# The number of open tables for all threads. (default:64) +table_open_cache=200 + +# How many threads the server should cache for reuse (default:0) +thread_cache_size=3 + +# wait 365d before dropping the DB connection (default:8h) +wait_timeout=31536000 + +# We use InnoDB, so don't let MyISAM eat up memory +key_buffer_size=16K + +[client] +default-character-set=utf8 diff --git a/src/server/storage/mysql-global.conf b/src/server/storage/mysql-global.conf new file mode 100644 index 0000000..755af05 --- /dev/null +++ b/src/server/storage/mysql-global.conf @@ -0,0 +1,100 @@ +# +# Global Akonadi MySQL server settings, +# These settings can be adjusted using $HOME/.config/akonadi/mysql-local.conf +# +# Based on advice by Kris Köhntopp +# +[mysqld] + +# strict query parsing/interpretation +# TODO: make Akonadi work with those settings enabled +# sql_mode=strict_trans_tables,strict_all_tables,strict_error_for_division_by_zero,no_auto_create_user,no_auto_value_on_zero,no_engine_substitution,no_zero_date,no_zero_in_date,only_full_group_by,pipes_as_concat +# sql_mode=strict_trans_tables + +# DEBUGGING: +# log all queries, useful for debugging but generates an enormous amount of data +# log=mysql.full +# log queries slower than n seconds, log file name relative to datadir (for debugging only) +# log_slow_queries=mysql.slow +# long_query_time=1 +# log queries not using indices, debug only, disable for production use +# log_queries_not_using_indexes=1 +# +# mesure database size and adjust innodb_buffer_pool_size +# SELECT sum(data_length) as bla, sum(index_length) as blub FROM information_schema.tables WHERE table_schema not in ("mysql", "information_schema"); + +# NOTES: +# Keep Innob_log_waits and keep Innodb_buffer_pool_wait_free small (see show global status like "inno%", show global variables) + +#expire_logs_days=3 + +#sync_bin_log=0 + +# Use UTF-8 encoding for tables +character_set_server=utf8 +collation_server=utf8_general_ci + +# use InnoDB for transactions and better crash recovery +default_storage_engine=innodb + +# memory buffer InnoDB uses to cache data and indexes of its tables (default:128M) +# Larger values means less I/O +innodb_buffer_pool_size=128M + +# Create a .ibd file for each table (default:0) +innodb_file_per_table=1 + +# Write out the log buffer to the log file at each commit (default:1) +innodb_flush_log_at_trx_commit=2 + +# Buffer size used to write to the log files on disk (default:1M for builtin, 8M for plugin) +# larger values means less I/O +innodb_log_buffer_size=1M + +# Size of each log file in a log group (default:5M) larger means less I/O but more time for recovery. +innodb_log_file_size=64M + +# # error log file name, relative to datadir (default:hostname.err) +log_error=mysql.err + +# print warnings and connection errors (default:1) +loose_log_warnings=2 + +# Convert table named to lowercase +lower_case_table_names=1 + +# Maximum size of one packet or any generated/intermediate string. (default:1M) +max_allowed_packet=32M + +# Maximum simultaneous connections allowed (default:100) +max_connections=256 + +# The two options below make no sense with prepared statements and/or transactions +# (make sense when having the same query multiple times) + +# Memory allocated for caching query results (default:0 (disabled)) +loose_query_cache_size=0 + +# Do not cache results (default:1) +loose_query_cache_type=0 + +# Do not use the privileges mechanisms +skip_grant_tables + +# Do not listen for TCP/IP connections at all +skip_networking + +# The number of open tables for all threads. (default:64) +table_open_cache=200 + +# How many threads the server should cache for reuse (default:0) +thread_cache_size=3 + +# wait 365d before dropping the DB connection (default:8h) +wait_timeout=31536000 + +# We use InnoDB, so don't let MyISAM eat up memory +key_buffer_size=16K + +[client] +default-character-set=utf8 diff --git a/src/server/storage/notificationcollector.cpp b/src/server/storage/notificationcollector.cpp new file mode 100644 index 0000000..cc99299 --- /dev/null +++ b/src/server/storage/notificationcollector.cpp @@ -0,0 +1,605 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "notificationcollector.h" +#include "aggregatedfetchscope.h" +#include "akonadi.h" +#include "cachecleaner.h" +#include "connection.h" +#include "handler/itemfetchhelper.h" +#include "handlerhelper.h" +#include "intervalcheck.h" +#include "notificationmanager.h" +#include "search/searchmanager.h" +#include "selectquerybuilder.h" +#include "shared/akranges.h" +#include "storage/collectionstatistics.h" +#include "storage/datastore.h" +#include "storage/entity.h" + +#include "akonadiserver_debug.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +NotificationCollector::NotificationCollector(AkonadiServer &akonadi, DataStore *db) + : mDb(db) + , mAkonadi(akonadi) +{ + QObject::connect(db, &DataStore::transactionCommitted, [this]() { + if (!mIgnoreTransactions) { + dispatchNotifications(); + } + }); + QObject::connect(db, &DataStore::transactionRolledBack, [this]() { + if (!mIgnoreTransactions) { + clear(); + } + }); +} + +void NotificationCollector::itemAdded(const PimItem &item, bool seen, const Collection &collection, const QByteArray &resource) +{ + mAkonadi.searchManager().scheduleSearchUpdate(); + mAkonadi.collectionStatistics().itemAdded(collection, item.size(), seen); + itemNotification(Protocol::ItemChangeNotification::Add, item, collection, Collection(), resource); +} + +void NotificationCollector::itemChanged(const PimItem &item, const QSet &changedParts, const Collection &collection, const QByteArray &resource) +{ + mAkonadi.searchManager().scheduleSearchUpdate(); + itemNotification(Protocol::ItemChangeNotification::Modify, item, collection, Collection(), resource, changedParts); +} + +void NotificationCollector::itemsFlagsChanged(const PimItem::List &items, + const QSet &addedFlags, + const QSet &removedFlags, + const Collection &collection, + const QByteArray &resource) +{ + int seenCount = (addedFlags.contains(AKONADI_FLAG_SEEN) || addedFlags.contains(AKONADI_FLAG_IGNORED) ? items.count() : 0); + seenCount -= (removedFlags.contains(AKONADI_FLAG_SEEN) || removedFlags.contains(AKONADI_FLAG_IGNORED) ? items.count() : 0); + + mAkonadi.collectionStatistics().itemsSeenChanged(collection, seenCount); + itemNotification(Protocol::ItemChangeNotification::ModifyFlags, items, collection, Collection(), resource, QSet(), addedFlags, removedFlags); +} + +void NotificationCollector::itemsTagsChanged(const PimItem::List &items, + const QSet &addedTags, + const QSet &removedTags, + const Collection &collection, + const QByteArray &resource) +{ + itemNotification(Protocol::ItemChangeNotification::ModifyTags, + items, + collection, + Collection(), + resource, + QSet(), + QSet(), + QSet(), + addedTags, + removedTags); +} + +void NotificationCollector::itemsRelationsChanged(const PimItem::List &items, + const Relation::List &addedRelations, + const Relation::List &removedRelations, + const Collection &collection, + const QByteArray &resource) +{ + itemNotification(Protocol::ItemChangeNotification::ModifyRelations, + items, + collection, + Collection(), + resource, + QSet(), + QSet(), + QSet(), + QSet(), + QSet(), + addedRelations, + removedRelations); +} + +void NotificationCollector::itemsMoved(const PimItem::List &items, + const Collection &collectionSrc, + const Collection &collectionDest, + const QByteArray &sourceResource) +{ + mAkonadi.searchManager().scheduleSearchUpdate(); + itemNotification(Protocol::ItemChangeNotification::Move, items, collectionSrc, collectionDest, sourceResource); +} + +void NotificationCollector::itemsRemoved(const PimItem::List &items, const Collection &collection, const QByteArray &resource) +{ + itemNotification(Protocol::ItemChangeNotification::Remove, items, collection, Collection(), resource); +} + +void NotificationCollector::itemsLinked(const PimItem::List &items, const Collection &collection) +{ + itemNotification(Protocol::ItemChangeNotification::Link, items, collection, Collection(), QByteArray()); +} + +void NotificationCollector::itemsUnlinked(const PimItem::List &items, const Collection &collection) +{ + itemNotification(Protocol::ItemChangeNotification::Unlink, items, collection, Collection(), QByteArray()); +} + +void NotificationCollector::collectionAdded(const Collection &collection, const QByteArray &resource) +{ + if (auto cleaner = mAkonadi.cacheCleaner()) { + cleaner->collectionAdded(collection.id()); + } + mAkonadi.intervalChecker().collectionAdded(collection.id()); + collectionNotification(Protocol::CollectionChangeNotification::Add, collection, collection.parentId(), -1, resource); +} + +void NotificationCollector::collectionChanged(const Collection &collection, const QList &changes, const QByteArray &resource) +{ + if (auto cleaner = mAkonadi.cacheCleaner()) { + cleaner->collectionChanged(collection.id()); + } + mAkonadi.intervalChecker().collectionChanged(collection.id()); + if (changes.contains(AKONADI_PARAM_ENABLED)) { + mAkonadi.collectionStatistics().invalidateCollection(collection); + } + collectionNotification(Protocol::CollectionChangeNotification::Modify, + collection, + collection.parentId(), + -1, + resource, + changes | AkRanges::Actions::toQSet); +} + +void NotificationCollector::collectionMoved(const Collection &collection, const Collection &source, const QByteArray &resource, const QByteArray &destResource) +{ + if (auto cleaner = mAkonadi.cacheCleaner()) { + cleaner->collectionChanged(collection.id()); + } + mAkonadi.intervalChecker().collectionChanged(collection.id()); + collectionNotification(Protocol::CollectionChangeNotification::Move, + collection, + source.id(), + collection.parentId(), + resource, + QSet(), + destResource); +} + +void NotificationCollector::collectionRemoved(const Collection &collection, const QByteArray &resource) +{ + if (auto cleaner = mAkonadi.cacheCleaner()) { + cleaner->collectionRemoved(collection.id()); + } + mAkonadi.intervalChecker().collectionRemoved(collection.id()); + mAkonadi.collectionStatistics().invalidateCollection(collection); + collectionNotification(Protocol::CollectionChangeNotification::Remove, collection, collection.parentId(), -1, resource); +} + +void NotificationCollector::collectionSubscribed(const Collection &collection, const QByteArray &resource) +{ + if (auto cleaner = mAkonadi.cacheCleaner()) { + cleaner->collectionAdded(collection.id()); + } + mAkonadi.intervalChecker().collectionAdded(collection.id()); + collectionNotification(Protocol::CollectionChangeNotification::Subscribe, collection, collection.parentId(), -1, resource, QSet()); +} + +void NotificationCollector::collectionUnsubscribed(const Collection &collection, const QByteArray &resource) +{ + if (auto cleaner = mAkonadi.cacheCleaner()) { + cleaner->collectionRemoved(collection.id()); + } + mAkonadi.intervalChecker().collectionRemoved(collection.id()); + mAkonadi.collectionStatistics().invalidateCollection(collection); + collectionNotification(Protocol::CollectionChangeNotification::Unsubscribe, collection, collection.parentId(), -1, resource, QSet()); +} + +void NotificationCollector::tagAdded(const Tag &tag) +{ + tagNotification(Protocol::TagChangeNotification::Add, tag); +} + +void NotificationCollector::tagChanged(const Tag &tag) +{ + tagNotification(Protocol::TagChangeNotification::Modify, tag); +} + +void NotificationCollector::tagRemoved(const Tag &tag, const QByteArray &resource, const QString &remoteId) +{ + tagNotification(Protocol::TagChangeNotification::Remove, tag, resource, remoteId); +} + +void NotificationCollector::relationAdded(const Relation &relation) +{ + relationNotification(Protocol::RelationChangeNotification::Add, relation); +} + +void NotificationCollector::relationRemoved(const Relation &relation) +{ + relationNotification(Protocol::RelationChangeNotification::Remove, relation); +} + +void NotificationCollector::clear() +{ + mNotifications.clear(); +} + +void NotificationCollector::setConnection(Connection *connection) +{ + mConnection = connection; +} + +void NotificationCollector::itemNotification(Protocol::ItemChangeNotification::Operation op, + const PimItem &item, + const Collection &collection, + const Collection &collectionDest, + const QByteArray &resource, + const QSet &parts) +{ + PimItem::List items; + items << item; + itemNotification(op, items, collection, collectionDest, resource, parts); +} + +void NotificationCollector::itemNotification(Protocol::ItemChangeNotification::Operation op, + const PimItem::List &items, + const Collection &collection, + const Collection &collectionDest, + const QByteArray &resource, + const QSet &parts, + const QSet &addedFlags, + const QSet &removedFlags, + const QSet &addedTags, + const QSet &removedTags, + const Relation::List &addedRelations, + const Relation::List &removedRelations) +{ + QMap> vCollections; + + if ((op == Protocol::ItemChangeNotification::Modify) || (op == Protocol::ItemChangeNotification::ModifyFlags) + || (op == Protocol::ItemChangeNotification::ModifyTags) || (op == Protocol::ItemChangeNotification::ModifyRelations)) { + vCollections = DataStore::self()->virtualCollections(items); + } + + auto msg = Protocol::ItemChangeNotificationPtr::create(); + if (mConnection) { + msg->setSessionId(mConnection->sessionId()); + } + msg->setOperation(op); + + msg->setItemParts(parts); + msg->setAddedFlags(addedFlags); + msg->setRemovedFlags(removedFlags); + msg->setAddedTags(addedTags); + msg->setRemovedTags(removedTags); + if (!addedRelations.isEmpty()) { + QSet rels; + Q_FOREACH (const Relation &rel, addedRelations) { + rels.insert(Protocol::ItemChangeNotification::Relation(rel.leftId(), rel.rightId(), rel.relationType().name())); + } + msg->setAddedRelations(rels); + } + if (!removedRelations.isEmpty()) { + QSet rels; + Q_FOREACH (const Relation &rel, removedRelations) { + rels.insert(Protocol::ItemChangeNotification::Relation(rel.leftId(), rel.rightId(), rel.relationType().name())); + } + msg->setRemovedRelations(rels); + } + + if (collectionDest.isValid()) { + QByteArray destResourceName; + destResourceName = collectionDest.resource().name().toLatin1(); + msg->setDestinationResource(destResourceName); + } + + msg->setParentDestCollection(collectionDest.id()); + + QVector ntfItems; + Q_FOREACH (const PimItem &item, items) { + Protocol::FetchItemsResponse i; + i.setId(item.id()); + i.setRemoteId(item.remoteId()); + i.setRemoteRevision(item.remoteRevision()); + i.setMimeType(item.mimeType().name()); + ntfItems.push_back(std::move(i)); + } + + /* Notify all virtual collections the items are linked to. */ + QHash virtItems; + for (const auto &ntfItem : ntfItems) { + virtItems.insert(ntfItem.id(), ntfItem); + } + for (auto iter = vCollections.cbegin(), end = vCollections.constEnd(); iter != end; ++iter) { + auto copy = Protocol::ItemChangeNotificationPtr::create(*msg); + QVector items; + items.reserve(iter->size()); + for (const auto &item : std::as_const(*iter)) { + items.append(virtItems.value(item.id())); + } + copy->setItems(items); + copy->setParentCollection(iter.key()); + copy->setResource(resource); + + mAkonadi.collectionStatistics().invalidateCollection(Collection::retrieveById(iter.key())); + dispatchNotification(copy); + } + + msg->setItems(ntfItems); + + Collection col; + if (!collection.isValid()) { + msg->setParentCollection(items.first().collection().id()); + col = items.first().collection(); + } else { + msg->setParentCollection(collection.id()); + col = collection; + } + + QByteArray res = resource; + if (res.isEmpty()) { + if (col.resourceId() <= 0) { + col = Collection::retrieveById(col.id()); + } + res = col.resource().name().toLatin1(); + } + msg->setResource(res); + + // Add and ModifyFlags are handled incrementally + // (see itemAdded() and itemsFlagsChanged()) + if (msg->operation() != Protocol::ItemChangeNotification::Add && msg->operation() != Protocol::ItemChangeNotification::ModifyFlags) { + mAkonadi.collectionStatistics().invalidateCollection(col); + } + dispatchNotification(msg); +} + +void NotificationCollector::collectionNotification(Protocol::CollectionChangeNotification::Operation op, + const Collection &collection, + Collection::Id source, + Collection::Id destination, + const QByteArray &resource, + const QSet &changes, + const QByteArray &destResource) +{ + auto msg = Protocol::CollectionChangeNotificationPtr::create(); + msg->setOperation(op); + if (mConnection) { + msg->setSessionId(mConnection->sessionId()); + } + msg->setParentCollection(source); + msg->setParentDestCollection(destination); + msg->setDestinationResource(destResource); + msg->setChangedParts(changes); + + auto msgCollection = HandlerHelper::fetchCollectionsResponse(mAkonadi, collection); + if (auto mgr = mAkonadi.notificationManager()) { + auto fetchScope = mgr->collectionFetchScope(); + // Make sure we have all the data + if (!fetchScope->fetchIdOnly() && msgCollection.name().isEmpty()) { + const auto col = Collection::retrieveById(msgCollection.id()); + const auto mts = col.mimeTypes(); + QStringList mimeTypes; + mimeTypes.reserve(mts.size()); + for (const auto &mt : mts) { + mimeTypes.push_back(mt.name()); + } + msgCollection = HandlerHelper::fetchCollectionsResponse(mAkonadi, col, {}, false, 0, {}, {}, mimeTypes); + } + // Get up-to-date statistics + if (fetchScope->fetchStatistics()) { + Collection col; + col.setId(msgCollection.id()); + const auto stats = mAkonadi.collectionStatistics().statistics(col); + msgCollection.setStatistics(Protocol::FetchCollectionStatsResponse(stats.count, stats.count - stats.read, stats.size)); + } + // Get attributes + const auto requestedAttrs = fetchScope->attributes(); + auto msgColAttrs = msgCollection.attributes(); + // TODO: This assumes that we have either none or all attributes in msgCollection + if (msgColAttrs.isEmpty() && !requestedAttrs.isEmpty()) { + SelectQueryBuilder qb; + qb.addColumn(CollectionAttribute::typeFullColumnName()); + qb.addColumn(CollectionAttribute::valueFullColumnName()); + qb.addValueCondition(CollectionAttribute::collectionIdFullColumnName(), Query::Equals, msgCollection.id()); + Query::Condition cond(Query::Or); + for (const auto &attr : requestedAttrs) { + cond.addValueCondition(CollectionAttribute::typeFullColumnName(), Query::Equals, attr); + } + qb.addCondition(cond); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "NotificationCollector failed to query attributes for Collection" << collection.name() << "(ID" + << collection.id() << ")"; + } + const auto attrs = qb.result(); + for (const auto &attr : attrs) { + msgColAttrs.insert(attr.type(), attr.value()); + } + msgCollection.setAttributes(msgColAttrs); + } + } + msg->setCollection(std::move(msgCollection)); + + if (!collection.enabled()) { + msg->addMetadata("DISABLED"); + } + + QByteArray res = resource; + if (res.isEmpty()) { + res = collection.resource().name().toLatin1(); + } + msg->setResource(res); + + dispatchNotification(msg); +} + +void NotificationCollector::tagNotification(Protocol::TagChangeNotification::Operation op, const Tag &tag, const QByteArray &resource, const QString &remoteId) +{ + auto msg = Protocol::TagChangeNotificationPtr::create(); + msg->setOperation(op); + if (mConnection) { + msg->setSessionId(mConnection->sessionId()); + } + msg->setResource(resource); + Protocol::FetchTagsResponse msgTag; + msgTag.setId(tag.id()); + msgTag.setRemoteId(remoteId.toUtf8()); + msgTag.setParentId(tag.parentId()); + if (auto mgr = mAkonadi.notificationManager()) { + auto fetchScope = mgr->tagFetchScope(); + if (!fetchScope->fetchIdOnly() && msgTag.gid().isEmpty()) { + msgTag = HandlerHelper::fetchTagsResponse(Tag::retrieveById(msgTag.id()), fetchScope->toFetchScope(), mConnection); + } + + const auto requestedAttrs = fetchScope->attributes(); + auto msgTagAttrs = msgTag.attributes(); + if (msgTagAttrs.isEmpty() && !requestedAttrs.isEmpty()) { + SelectQueryBuilder qb; + qb.addColumn(TagAttribute::typeFullColumnName()); + qb.addColumn(TagAttribute::valueFullColumnName()); + qb.addValueCondition(TagAttribute::tagIdFullColumnName(), Query::Equals, msgTag.id()); + Query::Condition cond(Query::Or); + for (const auto &attr : requestedAttrs) { + cond.addValueCondition(TagAttribute::typeFullColumnName(), Query::Equals, attr); + } + qb.addCondition(cond); + if (!qb.exec()) { + qCWarning(AKONADISERVER_LOG) << "NotificationCollection failed to query attributes for Tag" << tag.id(); + } + const auto attrs = qb.result(); + for (const auto &attr : attrs) { + msgTagAttrs.insert(attr.type(), attr.value()); + } + msgTag.setAttributes(msgTagAttrs); + } + } + msg->setTag(std::move(msgTag)); + + dispatchNotification(msg); +} + +void NotificationCollector::relationNotification(Protocol::RelationChangeNotification::Operation op, const Relation &relation) +{ + auto msg = Protocol::RelationChangeNotificationPtr::create(); + msg->setOperation(op); + if (mConnection) { + msg->setSessionId(mConnection->sessionId()); + } + msg->setRelation(HandlerHelper::fetchRelationsResponse(relation)); + + dispatchNotification(msg); +} + +void NotificationCollector::completeNotification(const Protocol::ChangeNotificationPtr &changeMsg) +{ + if (changeMsg->type() == Protocol::Command::ItemChangeNotification) { + const auto msg = changeMsg.staticCast(); + auto const mgr = mAkonadi.notificationManager(); + if (mgr && msg->operation() != Protocol::ItemChangeNotification::Remove) { + if (mDb->inTransaction()) { + qCWarning(AKONADISERVER_LOG) << "NotificationCollector requested FetchHelper from within a transaction." + << "Aborting since this would deadlock!"; + return; + } + auto fetchScope = mgr->itemFetchScope(); + // NOTE: Checking and retrieving missing elements for each Item manually + // here would require a complex code (and I'm too lazy), so instead we simply + // feed the Items to FetchHelper and retrieve them all with the setup from + // the aggregated fetch scope. The worst case is that we re-fetch everything + // we already have, but that's stil better than the pre-ntf-payload situation + QVector ids; + const auto items = msg->items(); + ids.reserve(items.size()); + bool allHaveRID = true; + for (const auto &item : items) { + ids.push_back(item.id()); + allHaveRID &= !item.remoteId().isEmpty(); + } + + // FetchHelper may trigger ItemRetriever, which needs RemoteID. If we + // don't have one (maybe because the Resource has not stored it yet, + // we emit a notification without it and leave it up to the Monitor + // to retrieve the Item on demand - we should have a RID stored in + // Akonadi by then. + if (mConnection && (allHaveRID || msg->operation() != Protocol::ItemChangeNotification::Add)) { + // Prevent transactions inside FetchHelper to recursively call our slot + QScopedValueRollback ignoreTransactions(mIgnoreTransactions); + mIgnoreTransactions = true; + CommandContext context; + auto itemFetchScope = fetchScope->toFetchScope(); + auto tagFetchScope = mgr->tagFetchScope()->toFetchScope(); + itemFetchScope.setFetch(Protocol::ItemFetchScope::CacheOnly); + ItemFetchHelper helper(mConnection, context, Scope(ids), itemFetchScope, tagFetchScope, mAkonadi); + // The Item was just changed, which means the atime was + // updated, no need to do it again a couple milliseconds later. + helper.disableATimeUpdates(); + QVector fetchedItems; + auto callback = [&fetchedItems](Protocol::FetchItemsResponse &&cmd) { + fetchedItems.push_back(std::move(cmd)); + }; + if (helper.fetchItems(std::move(callback))) { + msg->setItems(fetchedItems); + } else { + qCWarning(AKONADISERVER_LOG) << "NotificationCollector railed to retrieve Items for notification!"; + } + } else { + QVector fetchedItems; + for (const auto &item : items) { + Protocol::FetchItemsResponse resp; + resp.setId(item.id()); + resp.setRevision(item.revision()); + resp.setMimeType(item.mimeType()); + resp.setParentId(item.parentId()); + resp.setGid(item.gid()); + resp.setSize(item.size()); + resp.setMTime(item.mTime()); + resp.setFlags(item.flags()); + fetchedItems.push_back(std::move(resp)); + } + msg->setItems(fetchedItems); + msg->setMustRetrieve(true); + } + } + } +} + +void NotificationCollector::dispatchNotification(const Protocol::ChangeNotificationPtr &msg) +{ + if (!mDb || mDb->inTransaction()) { + if (msg->type() == Protocol::Command::CollectionChangeNotification) { + Protocol::CollectionChangeNotification::appendAndCompress(mNotifications, msg); + } else { + mNotifications.append(msg); + } + } else { + completeNotification(msg); + notify({msg}); + } +} + +bool NotificationCollector::dispatchNotifications() +{ + if (!mNotifications.isEmpty()) { + for (auto &ntf : mNotifications) { + completeNotification(ntf); + } + notify(std::move(mNotifications)); + clear(); + return true; + } + + return false; +} + +void NotificationCollector::notify(Protocol::ChangeNotificationList &&msgs) +{ + if (auto mgr = mAkonadi.notificationManager()) { + QMetaObject::invokeMethod(mgr, "slotNotify", Qt::QueuedConnection, Q_ARG(Akonadi::Protocol::ChangeNotificationList, msgs)); + } +} diff --git a/src/server/storage/notificationcollector.h b/src/server/storage/notificationcollector.h new file mode 100644 index 0000000..45f91f4 --- /dev/null +++ b/src/server/storage/notificationcollector.h @@ -0,0 +1,247 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" + +#include + +#include +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class DataStore; +class Connection; +class AkonadiServer; + +/** + Part of the DataStore, collects change notifications and emits + them after the current transaction has been successfully committed. + Where possible, notifications are compressed. +*/ +class NotificationCollector +{ +public: + /** + Create a new notification collector for the given DataStore @p db. + @param db The datastore using this notification collector. + */ + explicit NotificationCollector(AkonadiServer &akonadi, DataStore *db); + + /** + Destroys this notification collector. + */ + virtual ~NotificationCollector() = default; + + /** + * Sets the connection that is causing the changes. + */ + void setConnection(Connection *connection); + + /** + Notify about an added item. + Provide as many parameters as you have at hand currently, everything + that is missing will be looked up in the database later. + */ + void itemAdded(const PimItem &item, bool seen, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); + + /** + Notify about a changed item. + Provide as many parameters as you have at hand currently, everything + that is missing will be looked up in the database later. + */ + void itemChanged(const PimItem &item, + const QSet &changedParts, + const Collection &collection = Collection(), + const QByteArray &resource = QByteArray()); + + /** + Notify about changed items flags + Provide as many parameters as you have at hand currently, everything + that is missing will be looked up in the database later. + */ + void itemsFlagsChanged(const PimItem::List &items, + const QSet &addedFlags, + const QSet &removedFlags, + const Collection &collection = Collection(), + const QByteArray &resource = QByteArray()); + + /** + Notify about changed items tags + **/ + void itemsTagsChanged(const PimItem::List &items, + const QSet &addedTags, + const QSet &removedTags, + const Collection &collection = Collection(), + const QByteArray &resource = QByteArray()); + + /** + Notify about changed items relations + **/ + void itemsRelationsChanged(const PimItem::List &items, + const Relation::List &addedRelations, + const Relation::List &removedRelations, + const Collection &collection = Collection(), + const QByteArray &resource = QByteArray()); + + /** + Notify about moved items + Provide as many parameters as you have at hand currently, everything + that is missing will be looked up in the database later. + */ + void itemsMoved(const PimItem::List &items, + const Collection &collectionSrc = Collection(), + const Collection &collectionDest = Collection(), + const QByteArray &sourceResource = QByteArray()); + + /** + Notify about removed items. + Make sure you either provide all parameters or call this function before + actually removing the item from database. + */ + void itemsRemoved(const PimItem::List &items, const Collection &collection = Collection(), const QByteArray &resource = QByteArray()); + + /** + * Notify about linked items + */ + void itemsLinked(const PimItem::List &items, const Collection &collection); + + /** + * Notify about unlinked items. + */ + void itemsUnlinked(const PimItem::List &items, const Collection &collection); + + /** + Notify about a added collection. + Provide as many parameters as you have at hand currently, everything + that is missing will be looked up in the database later. + */ + void collectionAdded(const Collection &collection, const QByteArray &resource = QByteArray()); + + /** + Notify about a changed collection. + Provide as many parameters as you have at hand currently, everything + that is missing will be looked up in the database later. + */ + void collectionChanged(const Collection &collection, const QList &changes, const QByteArray &resource = QByteArray()); + + /** + Notify about a moved collection. + Provide as many parameters as you have at hand currently, everything + missing will be looked up on demand in the database later. + */ + void collectionMoved(const Collection &collection, + const Collection &source, + const QByteArray &resource = QByteArray(), + const QByteArray &destResource = QByteArray()); + + /** + Notify about a removed collection. + Make sure you either provide all parameters or call this function before + actually removing the item from database. + */ + void collectionRemoved(const Collection &collection, const QByteArray &resource = QByteArray()); + + /** + * Notify about a collection subscription. + */ + void collectionSubscribed(const Collection &collection, const QByteArray &resource = QByteArray()); + /** + * Notify about a collection unsubscription + */ + void collectionUnsubscribed(const Collection &collection, const QByteArray &resource = QByteArray()); + + /** + Notify about an added tag. + */ + void tagAdded(const Tag &tag); + + /** + Notify about a changed tag. + */ + void tagChanged(const Tag &tag); + + /** + Notify about a removed tag. + */ + void tagRemoved(const Tag &tag, const QByteArray &resource, const QString &remoteId); + + /** + Notify about an added relation. + */ + void relationAdded(const Relation &relation); + + /** + Notify about a removed relation. + */ + void relationRemoved(const Relation &relation); + + /** + Trigger sending of collected notifications. + + @returns Returns true when any notifications were dispatched, false if there + were no pending notifications. + */ + bool dispatchNotifications(); + +private: + void itemNotification(Protocol::ItemChangeNotification::Operation op, + const PimItem::List &items, + const Collection &collection, + const Collection &collectionDest, + const QByteArray &resource, + const QSet &parts = QSet(), + const QSet &addedFlags = QSet(), + const QSet &removedFlags = QSet(), + const QSet &addedTags = QSet(), + const QSet &removedTags = QSet(), + const Relation::List &addedRelations = Relation::List(), + const Relation::List &removedRelations = Relation::List()); + void itemNotification(Protocol::ItemChangeNotification::Operation op, + const PimItem &item, + const Collection &collection, + const Collection &collectionDest, + const QByteArray &resource, + const QSet &parts = QSet()); + void collectionNotification(Protocol::CollectionChangeNotification::Operation op, + const Collection &collection, + Collection::Id source, + Collection::Id destination, + const QByteArray &resource, + const QSet &changes = QSet(), + const QByteArray &destResource = QByteArray()); + void tagNotification(Protocol::TagChangeNotification::Operation op, + const Tag &tag, + const QByteArray &resource = QByteArray(), + const QString &remoteId = QString()); + void relationNotification(Protocol::RelationChangeNotification::Operation op, const Relation &relation); + void dispatchNotification(const Protocol::ChangeNotificationPtr &msg); + void clear(); + + void completeNotification(const Protocol::ChangeNotificationPtr &msg); + +protected: + virtual void notify(Protocol::ChangeNotificationList &&ntfs); + +private: + Q_DISABLE_COPY_MOVE(NotificationCollector) + + DataStore *mDb; + Connection *mConnection = nullptr; + AkonadiServer &mAkonadi; + bool mIgnoreTransactions = false; + + Protocol::ChangeNotificationList mNotifications; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/parthelper.cpp b/src/server/storage/parthelper.cpp new file mode 100644 index 0000000..35df77c --- /dev/null +++ b/src/server/storage/parthelper.cpp @@ -0,0 +1,188 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2009 Andras Mantia * + * SPDX-FileCopyrightText: 2010 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "parthelper.h" +#include "dbconfig.h" +#include "parttypehelper.h" +#include "selectquerybuilder.h" + +#include + +#include + +#include "akonadiserver_debug.h" + +using namespace Akonadi; +using namespace Akonadi::Server; + +void PartHelper::update(Part *part, const QByteArray &data, qint64 dataSize) +{ + if (!part) { + throw PartHelperException("Invalid part"); + } + + const bool storeExternal = dataSize > DbConfig::configuredDatabase()->sizeThreshold(); + + QByteArray newFile; + if (part->storage() == Part::External && storeExternal) { + if (!ExternalPartStorage::self()->updatePartFile(data, part->data(), newFile)) { + throw PartHelperException(QStringLiteral("Failed to update external payload part")); + } + part->setData(newFile); + } else if (part->storage() != Part::External && storeExternal) { + if (!ExternalPartStorage::self()->createPartFile(data, part->id(), newFile)) { + throw PartHelperException(QStringLiteral("Failed to create external payload part")); + } + part->setData(newFile); + part->setStorage(Part::External); + } else { + if (part->storage() == Part::External && !storeExternal) { + const QString file = ExternalPartStorage::resolveAbsolutePath(part->data()); + ExternalPartStorage::self()->removePartFile(file); + } + part->setData(data); + part->setStorage(Part::Internal); + } + + part->setDatasize(dataSize); + const bool result = part->update(); + if (!result) { + throw PartHelperException("Failed to update database record"); + } +} + +bool PartHelper::insert(Part *part, qint64 *insertId) +{ + if (!part) { + return false; + } + + const bool storeInFile = part->datasize() > DbConfig::configuredDatabase()->sizeThreshold(); + // it is needed to insert first the metadata so a new id is generated for the part, + // and we need this id for the payload file name + QByteArray data; + if (storeInFile) { + data = part->data(); + part->setData(QByteArray()); + part->setStorage(Part::External); + } else { + part->setStorage(Part::Internal); + } + + bool result = part->insert(insertId); + + if (storeInFile && result) { + QByteArray filename; + if (!ExternalPartStorage::self()->createPartFile(data, part->id(), filename)) { + throw PartHelperException("Failed to create external payload part"); + } + part->setData(filename); + result = part->update(); + } + + return result; +} + +bool PartHelper::remove(Part *part) +{ + if (!part) { + return false; + } + + if (part->storage() == Part::External) { + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(part->data())); + } + return part->remove(); +} + +bool PartHelper::remove(const QString &column, const QVariant &value) +{ + SelectQueryBuilder builder; + builder.addValueCondition(column, Query::Equals, value); + builder.addValueCondition(Part::storageColumn(), Query::Equals, Part::External); + builder.addValueCondition(Part::dataColumn(), Query::IsNot, QVariant()); + if (!builder.exec()) { + // qCDebug(AKONADISERVER_LOG) << "Error selecting records to be deleted from table" + // << Part::tableName() << builder.query().lastError().text(); + return false; + } + const Part::List parts = builder.result(); + Part::List::ConstIterator it = parts.constBegin(); + Part::List::ConstIterator end = parts.constEnd(); + for (; it != end; ++it) { + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath((*it).data())); + } + return Part::remove(column, value); +} + +QByteArray PartHelper::translateData(const QByteArray &data, Part::Storage storage) +{ + if (storage == Part::External || storage == Part::Foreign) { + QFile file; + if (storage == Part::External) { + file.setFileName(ExternalPartStorage::resolveAbsolutePath(data)); + } else { + file.setFileName(QString::fromUtf8(data)); + } + + if (file.open(QIODevice::ReadOnly)) { + const QByteArray payload = file.readAll(); + file.close(); + return payload; + } else { + qCCritical(AKONADISERVER_LOG) << "Payload file " << file.fileName() << " could not be open for reading!"; + qCCritical(AKONADISERVER_LOG) << "Error: " << file.errorString(); + return QByteArray(); + } + } else { + // not external + return data; + } +} + +QByteArray PartHelper::translateData(const Part &part) +{ + return translateData(part.data(), part.storage()); +} + +bool PartHelper::truncate(Part &part) +{ + if (part.storage() == Part::External) { + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(part.data())); + } + + part.setData(QByteArray()); + part.setDatasize(0); + part.setStorage(Part::Internal); + return part.update(); +} + +bool PartHelper::verify(Part &part) +{ + if (part.storage() == Part::Internal) { + return true; + } + + QString fileName; + if (part.storage() == Part::External) { + fileName = ExternalPartStorage::resolveAbsolutePath(part.data()); + } else if (part.storage() == Part::Foreign) { + fileName = QString::fromUtf8(part.data()); + } else { + Q_ASSERT(false); + } + + if (!QFile::exists(fileName)) { + qCCritical(AKONADISERVER_LOG) << "Payload file" << fileName << "is missing, trying to recover."; + part.setData(QByteArray()); + part.setDatasize(0); + part.setStorage(Part::Internal); + return part.update(); + } + + return true; +} diff --git a/src/server/storage/parthelper.h b/src/server/storage/parthelper.h new file mode 100644 index 0000000..458bd4e --- /dev/null +++ b/src/server/storage/parthelper.h @@ -0,0 +1,68 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2009 Andras Mantia * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "../exception.h" +#include "entities.h" + +class QString; +class QVariant; + +namespace Akonadi +{ +namespace Server +{ +AKONADI_EXCEPTION_MAKE_INSTANCE(PartHelperException); + +/** + * Helper methods that store data in a file instead of the database. + * + * @author Andras Mantia + * + * @todo Use exceptions for error handling in all these methods. Requires that all callers + * can handle that first though. + */ +namespace PartHelper +{ +/** + * Update payload of an existing part @p part to @p data and size @p dataSize. + * Automatically decides whether or not the data should be stored in the database + * or the file system. + * @throw PartHelperException if file operations failed + */ +void update(Part *part, const QByteArray &data, qint64 dataSize); + +/** + * Adds a new part to the database and if necessary to the filesystem. + * @p part must not be in the database yet (ie. valid() == false) and must have + * a data size set. + */ +bool insert(Part *part, qint64 *insertId = nullptr); + +/** Deletes @p part from the database and also removes existing filesystem data if needed. */ +bool remove(Part *part); +/** Deletes all parts which match the given constraint, including all corresponding filesystem data. */ +bool remove(const QString &column, const QVariant &value); + +/** Returns the payload data. */ +QByteArray translateData(const QByteArray &data, Part::Storage storageType); +/** Convenience overload of the above. */ +QByteArray translateData(const Part &part); + +/** Truncate the payload of @p part and update filesystem/database accordingly. + * This is more efficient than using update since it does not require the data to be loaded. + */ +bool truncate(Part &part); + +/** Verifies and if necessary fixes the external reference of this part. */ +bool verify(Part &part); + +} // namespace PartHelper + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/partstreamer.cpp b/src/server/storage/partstreamer.cpp new file mode 100644 index 0000000..3e870ae --- /dev/null +++ b/src/server/storage/partstreamer.cpp @@ -0,0 +1,358 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "partstreamer.h" +#include "akonadiserver_debug.h" +#include "capabilities_p.h" +#include "connection.h" +#include "dbconfig.h" +#include "parthelper.h" +#include "parttypehelper.h" +#include "selectquerybuilder.h" + +#include +#include +#include + +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +PartStreamer::PartStreamer(Connection *connection, const PimItem &pimItem) + : mConnection(connection) + , mItem(pimItem) +{ + // Make sure the file_db_data path exists + StandardDirs::saveDir("data", QStringLiteral("file_db_data")); +} + +PartStreamer::~PartStreamer() +{ +} + +Protocol::PartMetaData PartStreamer::requestPartMetaData(const QByteArray &partName) +{ + { + Protocol::StreamPayloadCommand resp; + resp.setPayloadName(partName); + resp.setRequest(Protocol::StreamPayloadCommand::MetaData); + mConnection->sendResponse(std::move(resp)); + } + + const auto cmd = mConnection->readCommand(); + if (!cmd->isValid() || Protocol::cmdCast(cmd).isError()) { + throw PartStreamerException("Client failed to provide part metadata."); + } + + return Protocol::cmdCast(cmd).metaData(); +} + +void PartStreamer::streamPayload(Part &part, const QByteArray &partName) +{ + Protocol::PartMetaData metaPart = requestPartMetaData(partName); + if (metaPart.name().isEmpty()) { + throw PartStreamerException(QStringLiteral("Client sent empty metadata for part '%1'.").arg(QString::fromUtf8(partName))); + } + part.setVersion(metaPart.version()); + + if (part.datasize() != metaPart.size()) { + part.setDatasize(metaPart.size()); + // Shortcut: if sizes differ, we don't need to compare data later no in order + // to detect whether the part has changed + mDataChanged = mDataChanged || (metaPart.size() != part.datasize()); + } + + if (metaPart.storageType() == Protocol::PartMetaData::Foreign) { + streamForeignPayload(part, metaPart); + } else if (part.datasize() > DbConfig::configuredDatabase()->sizeThreshold()) { + // actual case when streaming storage is used: external payload is enabled, + // data is big enough in a literal + streamPayloadToFile(part, metaPart); + } else { + streamPayloadData(part, metaPart); + } +} + +void PartStreamer::streamPayloadData(Part &part, const Protocol::PartMetaData &metaPart) +{ + // If the part WAS external previously, remove data file + if (part.storage() == Part::External) { + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(part.data())); + } + + // Request the actual data + { + Protocol::StreamPayloadCommand resp; + resp.setPayloadName(metaPart.name()); + resp.setRequest(Protocol::StreamPayloadCommand::Data); + mConnection->sendResponse(std::move(resp)); + } + + const auto cmd = mConnection->readCommand(); + const auto &response = Protocol::cmdCast(cmd); + if (!response.isValid() || response.isError()) { + throw PartStreamerException(QStringLiteral("Client failed to provide payload data for part ID %1 (%2).").arg(part.id()).arg(part.partType().name())); + } + const QByteArray newData = response.data(); + // only use the data size with internal payload parts, for foreign parts + // we use the size reported by client + const auto newSize = (metaPart.storageType() == Protocol::PartMetaData::Internal) ? newData.size() : metaPart.size(); + if (newSize != metaPart.size()) { + throw PartStreamerException(QStringLiteral("Payload size mismatch: client advertised %1 bytes but sent %2 bytes.").arg(metaPart.size()).arg(newSize)); + } + + if (part.isValid()) { + if (!mDataChanged) { + mDataChanged = mDataChanged || (newData != part.data()); + } + PartHelper::update(&part, newData, newSize); + } else { + part.setData(newData); + part.setDatasize(newSize); + if (!part.insert()) { + throw PartStreamerException("Failed to insert new part into database."); + } + } +} + +void PartStreamer::streamPayloadToFile(Part &part, const Protocol::PartMetaData &metaPart) +{ + QByteArray origData; + if (!mDataChanged && mCheckChanged) { + origData = PartHelper::translateData(part); + } + + QByteArray filename; + if (part.isValid()) { + if (part.storage() == Part::External) { + // Part was external and is still external + filename = part.data(); + if (!filename.isEmpty()) { + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(filename)); + filename = ExternalPartStorage::updateFileNameRevision(filename); + } else { + // recover from data corruption + filename = ExternalPartStorage::nameForPartId(part.id()); + } + } else { + // Part wasn't external, but is now + filename = ExternalPartStorage::nameForPartId(part.id()); + } + + QFileInfo finfo(QString::fromUtf8(filename)); + if (finfo.isAbsolute()) { + filename = finfo.fileName().toUtf8(); + } + } + + part.setStorage(Part::External); + part.setDatasize(metaPart.size()); + part.setData(filename); + + if (part.isValid()) { + if (!part.update()) { + throw PartStreamerException(QStringLiteral("Failed to update part %1 in database.").arg(part.id())); + } + } else { + if (!part.insert()) { + throw PartStreamerException(QStringLiteral("Failed to insert new part fo PimItem %1 into database.").arg(part.pimItemId())); + } + + filename = ExternalPartStorage::nameForPartId(part.id()); + part.setData(filename); + if (!part.update()) { + throw PartStreamerException(QStringLiteral("Failed to update part %1 in database.").arg(part.id())); + } + } + + { + Protocol::StreamPayloadCommand cmd; + cmd.setPayloadName(metaPart.name()); + cmd.setRequest(Protocol::StreamPayloadCommand::Data); + cmd.setDestination(QString::fromUtf8(filename)); + mConnection->sendResponse(std::move(cmd)); + } + + const auto cmd = mConnection->readCommand(); + const auto &response = Protocol::cmdCast(cmd); + if (!response.isValid() || response.isError()) { + throw PartStreamerException("Client failed to store payload into file."); + } + + QFile file(ExternalPartStorage::resolveAbsolutePath(filename)); + if (!file.exists()) { + throw PartStreamerException(QStringLiteral("External payload file %1 does not exist.").arg(file.fileName())); + } + + if (file.size() != metaPart.size()) { + throw PartStreamerException( + QStringLiteral("Payload size mismatch, client advertised %1 bytes, but the file is %2 bytes.").arg(metaPart.size(), file.size())); + } + + if (mCheckChanged && !mDataChanged) { + // This is invoked only when part already exists, data sizes match and + // caller wants to know whether parts really differ + mDataChanged = (origData != PartHelper::translateData(part)); + } +} + +void PartStreamer::streamForeignPayload(Part &part, const Protocol::PartMetaData &metaPart) +{ + QByteArray origData; + if (!mDataChanged && mCheckChanged) { + origData = PartHelper::translateData(part); + } + + { + Protocol::StreamPayloadCommand cmd; + cmd.setPayloadName(metaPart.name()); + cmd.setRequest(Protocol::StreamPayloadCommand::Data); + mConnection->sendResponse(std::move(cmd)); + } + + const auto cmd = mConnection->readCommand(); + const auto &response = Protocol::cmdCast(cmd); + if (!response.isValid() || response.isError()) { + throw PartStreamerException("Client failed to store payload into file."); + } + + // If the part was previously external, clean up the data + if (part.storage() == Part::External) { + const QString filename = QString::fromUtf8(part.data()); + ExternalPartStorage::self()->removePartFile(ExternalPartStorage::resolveAbsolutePath(filename)); + } + + part.setStorage(Part::Foreign); + part.setData(response.data()); + + if (part.isValid()) { + if (!part.update()) { + throw PartStreamerException(QStringLiteral("Failed to update part %1 in database.").arg(part.id())); + } + } else { + if (!part.insert()) { + throw PartStreamerException(QStringLiteral("Failed to insert part for PimItem %1 into database.").arg(part.pimItemId())); + } + } + + const QString filename = QString::fromUtf8(response.data()); + QFile file(filename); + if (!file.exists()) { + throw PartStreamerException(QStringLiteral("Foreign payload file %1 does not exist.").arg(filename)); + } + + if (file.size() != metaPart.size()) { + throw PartStreamerException( + QStringLiteral("Foreign payload size mismatch, client advertised %1 bytes, but the file size is %2 bytes.").arg(metaPart.size(), file.size())); + } + + if (mCheckChanged && !mDataChanged) { + // This is invoked only when part already exists, data sizes match and + // caller wants to know whether parts really differ + mDataChanged = (origData != PartHelper::translateData(part)); + } +} + +void PartStreamer::preparePart(bool checkExists, const QByteArray &partName, Part &part) +{ + mDataChanged = false; + + const PartType partType = PartTypeHelper::fromFqName(partName); + + if (checkExists || mCheckChanged) { + SelectQueryBuilder qb; + qb.addValueCondition(Part::pimItemIdColumn(), Query::Equals, mItem.id()); + qb.addValueCondition(Part::partTypeIdColumn(), Query::Equals, partType.id()); + if (!qb.exec()) { + throw PartStreamerException(QStringLiteral("Failed to check if part %1 exists in PimItem %2.").arg(QString::fromUtf8(partName)).arg(mItem.id())); + } + + const Part::List result = qb.result(); + if (!result.isEmpty()) { + part = result.at(0); + } + } + + // Shortcut: newly created parts are always "changed" + if (!part.isValid()) { + mDataChanged = true; + } + + part.setPartType(partType); + part.setPimItemId(mItem.id()); +} + +void PartStreamer::stream(bool checkExists, const QByteArray &partName, qint64 &partSize, bool *changed) +{ + mCheckChanged = (changed != nullptr); + if (changed != nullptr) { + *changed = false; + } + + Part part; + preparePart(checkExists, partName, part); + + streamPayload(part, partName); + if (changed && mCheckChanged) { + *changed = mDataChanged; + } + + partSize = part.datasize(); +} + +void PartStreamer::streamAttribute(bool checkExists, const QByteArray &_partName, const QByteArray &value, bool *changed) +{ + mCheckChanged = (changed != nullptr); + if (changed != nullptr) { + *changed = false; + } + + QByteArray partName; + if (!_partName.startsWith("ATR:")) { + partName = "ATR:" + _partName; + } else { + partName = _partName; + } + + Part part; + preparePart(checkExists, partName, part); + + if (part.isValid()) { + if (mCheckChanged) { + if (PartHelper::translateData(part) != value) { + mDataChanged = true; + } + } + PartHelper::update(&part, value, value.size()); + } else { + const bool storeInFile = value.size() > DbConfig::configuredDatabase()->sizeThreshold(); + part.setDatasize(value.size()); + part.setVersion(0); + if (storeInFile) { + if (!part.insert()) { + throw PartStreamerException(QStringLiteral("Failed to store attribute part for PimItem %1 in database.").arg(part.pimItemId())); + } + PartHelper::update(&part, value, value.size()); + } else { + part.setData(value); + if (!part.insert()) { + throw PartStreamerException(QStringLiteral("Failed to store attribute part for PimItem %1 in database.").arg(part.pimItemId())); + } + } + } + + if (mCheckChanged) { + *changed = mDataChanged; + } +} diff --git a/src/server/storage/partstreamer.h b/src/server/storage/partstreamer.h new file mode 100644 index 0000000..4188edd --- /dev/null +++ b/src/server/storage/partstreamer.h @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include + +#include "entities.h" +#include "exception.h" + +namespace Akonadi +{ +namespace Protocol +{ +class PartMetaData; +class Command; +using CommandPtr = QSharedPointer; +} + +namespace Server +{ +AKONADI_EXCEPTION_MAKE_INSTANCE(PartStreamerException); + +class PimItem; +class Part; +class Connection; + +class PartStreamer +{ +public: + explicit PartStreamer(Connection *connection, const PimItem &pimItem); + ~PartStreamer(); + + /** + * @throws PartStreamException + */ + void stream(bool checkExists, const QByteArray &partName, qint64 &partSize, bool *changed = nullptr); + + /** + * @throws PartStreamerException + */ + void streamAttribute(bool checkExists, const QByteArray &partName, const QByteArray &value, bool *changed = nullptr); + +private: + void streamPayload(Part &part, const QByteArray &partName); + void streamPayloadToFile(Part &part, const Protocol::PartMetaData &metaPart); + void streamPayloadData(Part &part, const Protocol::PartMetaData &metaPart); + void streamForeignPayload(Part &part, const Protocol::PartMetaData &metaPart); + + Protocol::PartMetaData requestPartMetaData(const QByteArray &partName); + void preparePart(bool checkExists, const QByteArray &partName, Part &part); + + Connection *mConnection; + PimItem mItem; + bool mCheckChanged; + bool mDataChanged; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/parttypehelper.cpp b/src/server/storage/parttypehelper.cpp new file mode 100644 index 0000000..bb33f77 --- /dev/null +++ b/src/server/storage/parttypehelper.cpp @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "parttypehelper.h" + +#include "entities.h" +#include "selectquerybuilder.h" + +#include + +#include + +using namespace Akonadi::Server; + +std::pair PartTypeHelper::parseFqName(const QString &fqName) +{ + const QStringList name = fqName.split(QLatin1Char(':'), Qt::SkipEmptyParts); + if (name.size() != 2) { + throw PartTypeException("Invalid part type name."); + } + return {name.first(), name.last()}; +} + +PartType PartTypeHelper::fromFqName(const QString &fqName) +{ + return std::apply(qOverload(PartTypeHelper::fromFqName), parseFqName(fqName)); +} + +PartType PartTypeHelper::fromFqName(const QByteArray &fqName) +{ + return fromFqName(QLatin1String(fqName)); +} + +PartType PartTypeHelper::fromFqName(const QString &ns, const QString &name) +{ + const PartType partType = PartType::retrieveByFQNameOrCreate(ns, name); + if (!partType.isValid()) { + throw PartTypeException("Failed to append part type"); + } + return partType; +} + +Query::Condition PartTypeHelper::conditionFromFqName(const QString &fqName) +{ + const auto [ns, name] = parseFqName(fqName); + Query::Condition c; + c.setSubQueryMode(Query::And); + c.addValueCondition(PartType::nsFullColumnName(), Query::Equals, ns); + c.addValueCondition(PartType::nameFullColumnName(), Query::Equals, name); + return c; +} + +Query::Condition PartTypeHelper::conditionFromFqNames(const QStringList &fqNames) +{ + Query::Condition c; + c.setSubQueryMode(Query::Or); + for (const QString &fqName : fqNames) { + c.addCondition(conditionFromFqName(fqName)); + } + return c; +} + +QString PartTypeHelper::fullName(const PartType &type) +{ + return type.ns() + QLatin1String(":") + type.name(); +} diff --git a/src/server/storage/parttypehelper.h b/src/server/storage/parttypehelper.h new file mode 100644 index 0000000..71bc3e4 --- /dev/null +++ b/src/server/storage/parttypehelper.h @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "exception.h" +#include "query.h" + +namespace Akonadi +{ +namespace Server +{ +class PartType; + +AKONADI_EXCEPTION_MAKE_INSTANCE(PartTypeException); + +/** + * Methods for dealing with the PartType table. + */ +namespace PartTypeHelper +{ +/** + * Retrieve (or create) PartType for the given fully qualified name. + * @param fqName Fully qualified name (NS:NAME). + * @throws PartTypeException + */ +PartType fromFqName(const QString &fqName); + +/** + * Convenience overload of the above. + */ +PartType fromFqName(const QByteArray &fqName); + +/** + * Retrieve (or create) PartType for the given namespace and type name. + * @param ns Namespace + * @param typeName Part type name. + * @throws PartTypeException + */ +PartType fromFqName(const QString &ns, const QString &typeName); + +/** + * Returns a query condition that matches the given part. + * @param fqName fully-qualified part type name + * @throws PartTypeException + */ +Query::Condition conditionFromFqName(const QString &fqName); + +/** + * Returns a query condition that matches the given part type list. + * @param fqNames fully qualified part type name list + * @throws PartTypeException + */ +Query::Condition conditionFromFqNames(const QStringList &fqNames); + +/** + * Convenience overload for the above. + */ +template class T> Query::Condition conditionFromFqNames(const T &fqNames) +{ + Query::Condition c; + c.setSubQueryMode(Query::Or); + for (const QByteArray &fqName : fqNames) { + c.addCondition(conditionFromFqName(QLatin1String(fqName))); + } + return c; +} + +/** + * Parses a fully qualified part type name into namespace/name. + * @param fqName fully-qualified part type name + * @throws PartTypeException if @p fqName does not match the NS:NAME schema + * @internal + */ +std::pair parseFqName(const QString &fqName); + +/** + * Returns full part name + */ +QString fullName(const PartType &type); + +} // namespace PartTypeHelper + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/query.cpp b/src/server/storage/query.cpp new file mode 100644 index 0000000..214c568 --- /dev/null +++ b/src/server/storage/query.cpp @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "query.h" + +using namespace Akonadi::Server; +using namespace Akonadi::Server::Query; + +void Condition::addValueCondition(const QString &column, CompareOperator op, const QVariant &value) +{ + Q_ASSERT(!column.isEmpty()); + Condition c; + c.mColumn = column; + c.mCompareOp = op; + c.mComparedValue = value; + mSubConditions << c; +} + +void Condition::addColumnCondition(const QString &column, CompareOperator op, const QString &column2) +{ + Q_ASSERT(!column.isEmpty()); + Q_ASSERT(!column2.isEmpty()); + Condition c; + c.mColumn = column; + c.mComparedColumn = column2; + c.mCompareOp = op; + mSubConditions << c; +} + +Query::Condition::Condition(LogicOperator op) + : mCompareOp(Equals) + , mCombineOp(op) +{ +} + +bool Query::Condition::isEmpty() const +{ + return mSubConditions.isEmpty(); +} + +Condition::List Query::Condition::subConditions() const +{ + return mSubConditions; +} + +void Query::Condition::setSubQueryMode(LogicOperator op) +{ + mCombineOp = op; +} + +void Query::Condition::addCondition(const Condition &condition) +{ + mSubConditions << condition; +} + +Case::Case(const Condition &when, const QString &then, const QString &elseBranch) +{ + addCondition(when, then); + setElse(elseBranch); +} + +Case::Case(const QString &column, CompareOperator op, const QVariant &value, const QString &when, const QString &elseBranch) +{ + addValueCondition(column, op, value, when); + setElse(elseBranch); +} + +void Case::addCondition(const Condition &when, const QString &then) +{ + mWhenThen.append(qMakePair(when, then)); +} + +void Case::addValueCondition(const QString &column, CompareOperator op, const QVariant &value, const QString &then) +{ + Condition when; + when.addValueCondition(column, op, value); + addCondition(when, then); +} + +void Case::addColumnCondition(const QString &column, CompareOperator op, const QString &column2, const QString &then) +{ + Condition when; + when.addColumnCondition(column, op, column2); + addCondition(when, then); +} + +void Case::setElse(const QString &elseBranch) +{ + mElse = elseBranch; +} diff --git a/src/server/storage/query.h b/src/server/storage/query.h new file mode 100644 index 0000000..9918944 --- /dev/null +++ b/src/server/storage/query.h @@ -0,0 +1,145 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace Akonadi +{ +namespace Server +{ +class QueryBuilder; + +/** + Building blocks for SQL queries. + @see QueryBuilder +*/ +namespace Query +{ +/** + Compare operators to be used in query conditions. +*/ +enum CompareOperator { + Equals, + NotEquals, + Is, + IsNot, + Less, + LessOrEqual, + Greater, + GreaterOrEqual, + In, + NotIn, + Like, +}; + +/** + Logic operations used to combine multiple query conditions. +*/ +enum LogicOperator { + And, + Or, +}; + +/** + Sort orders. +*/ +enum SortOrder { + Ascending, + Descending, +}; + +/** + Represents a WHERE condition tree. +*/ +class Condition +{ + friend class Akonadi::Server::QueryBuilder; + +public: + /** A list of conditions. */ + using List = QVector; + + /** + Create an empty condition. + @param op how to combine sub queries. + */ + explicit Condition(LogicOperator op = And); + + /** + Add a WHERE condition which compares a column with a given value. + @param column The column that should be compared. + @param op The operator used for comparison + @param value The value @p column is compared to. + */ + void addValueCondition(const QString &column, CompareOperator op, const QVariant &value); + + /** + Add a WHERE condition which compares a column with another column. + @param column The column that should be compared. + @param op The operator used for comparison. + @param column2 The column @p column is compared to. + */ + void addColumnCondition(const QString &column, CompareOperator op, const QString &column2); + + /** + Add a WHERE condition. Use this method to build hierarchical conditions. + */ + void addCondition(const Condition &condition); + + /** + Set how sub-conditions should be combined, default is And. + */ + void setSubQueryMode(LogicOperator op); + + /** + Returns if there are sub conditions. + */ + bool isEmpty() const; + + /** + Returns the list of sub-conditions. + */ + Condition::List subConditions() const; + +private: + Condition::List mSubConditions; + QString mColumn; + QString mComparedColumn; + QVariant mComparedValue; + CompareOperator mCompareOp; + LogicOperator mCombineOp; + +}; // class Condition + +class Case +{ + friend class Akonadi::Server::QueryBuilder; + +public: + Case(const Condition &when, const QString &then, const QString &elseBranch = QString()); + Case(const QString &column, Query::CompareOperator op, const QVariant &value, const QString &when, const QString &elseBranch = QString()); + + void addCondition(const Condition &when, const QString &then); + void addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, const QString &then); + void addColumnCondition(const QString &column, Query::CompareOperator op, const QString &column2, const QString &then); + + void setElse(const QString &elseBranch); + +private: + QVector> mWhenThen; + QString mElse; +}; + +} // namespace Query +} // namespace Server +} // namespace Akonadi + +Q_DECLARE_TYPEINFO(Akonadi::Server::Query::Condition, Q_MOVABLE_TYPE); + diff --git a/src/server/storage/querybuilder.cpp b/src/server/storage/querybuilder.cpp new file mode 100644 index 0000000..24b1aaa --- /dev/null +++ b/src/server/storage/querybuilder.cpp @@ -0,0 +1,686 @@ +/* + SPDX-FileCopyrightText: 2007-2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "querybuilder.h" +#include "akonadiserver_debug.h" +#include "dbexception.h" + +#ifndef QUERYBUILDER_UNITTEST +#include "storage/datastore.h" +#include "storage/querycache.h" +#include "storage/storagedebugger.h" +#endif + +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi::Server; + +static QLatin1String compareOperatorToString(Query::CompareOperator op) +{ + switch (op) { + case Query::Equals: + return QLatin1String(" = "); + case Query::NotEquals: + return QLatin1String(" <> "); + case Query::Is: + return QLatin1String(" IS "); + case Query::IsNot: + return QLatin1String(" IS NOT "); + case Query::Less: + return QLatin1String(" < "); + case Query::LessOrEqual: + return QLatin1String(" <= "); + case Query::Greater: + return QLatin1String(" > "); + case Query::GreaterOrEqual: + return QLatin1String(" >= "); + case Query::In: + return QLatin1String(" IN "); + case Query::NotIn: + return QLatin1String(" NOT IN "); + case Query::Like: + return QLatin1String(" LIKE "); + } + Q_ASSERT_X(false, "QueryBuilder::compareOperatorToString()", "Unknown compare operator."); + return QLatin1String(""); +} + +static QLatin1String logicOperatorToString(Query::LogicOperator op) +{ + switch (op) { + case Query::And: + return QLatin1String(" AND "); + case Query::Or: + return QLatin1String(" OR "); + } + Q_ASSERT_X(false, "QueryBuilder::logicOperatorToString()", "Unknown logic operator."); + return QLatin1String(""); +} + +static QLatin1String sortOrderToString(Query::SortOrder order) +{ + switch (order) { + case Query::Ascending: + return QLatin1String(" ASC"); + case Query::Descending: + return QLatin1String(" DESC"); + } + Q_ASSERT_X(false, "QueryBuilder::sortOrderToString()", "Unknown sort order."); + return QLatin1String(""); +} + +static void appendJoined(QString *statement, const QStringList &strings, QLatin1String glue = QLatin1String(", ")) +{ + for (int i = 0, c = strings.size(); i < c; ++i) { + *statement += strings.at(i); + if (i + 1 < c) { + *statement += glue; + } + } +} + +QueryBuilder::QueryBuilder(const QString &table, QueryBuilder::QueryType type) + : mTable(table) +#ifndef QUERYBUILDER_UNITTEST + , mDatabaseType(DbType::type(DataStore::self()->database())) + , mQuery(DataStore::self()->database()) +#else + , mDatabaseType(DbType::Unknown) +#endif + , mType(type) + , mIdentificationColumn() + , mLimit(-1) + , mOffset(-1) + , mDistinct(false) +{ + static const QString defaultIdColumn = QStringLiteral("id"); + mIdentificationColumn = defaultIdColumn; +} + +QueryBuilder::QueryBuilder(const QSqlQuery &tableQuery, const QString &tableQueryAlias) + : mTable(tableQueryAlias) + , mTableSubQuery(tableQuery) +#ifndef QUERYBUILDER_UNITTEST + , mDatabaseType(DbType::type(DataStore::self()->database())) + , mQuery(DataStore::self()->database()) +#else + , mDatabaseType(DbType::Unknown) +#endif + , mType(QueryType::Select) + , mIdentificationColumn() + , mLimit(-1) + , mOffset(-1) + , mDistinct(false) +{ + static const QString defaultIdColumn = QStringLiteral("id"); + mIdentificationColumn = defaultIdColumn; +} + +void QueryBuilder::setDatabaseType(DbType::Type type) +{ + mDatabaseType = type; +} + +void QueryBuilder::addJoin(JoinType joinType, const QString &table, const Query::Condition &condition) +{ + Q_ASSERT((joinType == InnerJoin && (mType == Select || mType == Update)) || (joinType == LeftJoin && mType == Select)); + + if (mJoinedTables.contains(table)) { + // InnerJoin is more restrictive than a LeftJoin, hence use that in doubt + mJoins[table].first = qMin(joinType, mJoins.value(table).first); + mJoins[table].second.addCondition(condition); + } else { + mJoins[table] = qMakePair(joinType, condition); + mJoinedTables << table; + } +} + +void QueryBuilder::addJoin(JoinType joinType, const QString &table, const QString &col1, const QString &col2) +{ + Query::Condition condition; + condition.addColumnCondition(col1, Query::Equals, col2); + addJoin(joinType, table, condition); +} + +void QueryBuilder::addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type) +{ + Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); + mRootCondition[type].addValueCondition(column, op, value); +} + +void QueryBuilder::addColumnCondition(const QString &column, Query::CompareOperator op, const QString &column2, ConditionType type) +{ + Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); + mRootCondition[type].addColumnCondition(column, op, column2); +} + +QSqlQuery &QueryBuilder::query() +{ + return mQuery; +} + +void QueryBuilder::sqliteAdaptUpdateJoin(Query::Condition &condition) +{ + // FIXME: This does not cover all cases by far. It however can handle most + // (probably all) of the update-join queries we do in Akonadi and convert them + // properly into a SQLite-compatible query. Better than nothing ;-) + + if (!condition.mSubConditions.isEmpty()) { + for (int i = condition.mSubConditions.count() - 1; i >= 0; --i) { + sqliteAdaptUpdateJoin(condition.mSubConditions[i]); + } + return; + } + + QString table; + if (condition.mColumn.contains(QLatin1Char('.'))) { + table = condition.mColumn.left(condition.mColumn.indexOf(QLatin1Char('.'))); + } else { + return; + } + + if (!mJoinedTables.contains(table)) { + return; + } + + const auto &[type, joinCondition] = mJoins.value(table); + + QueryBuilder qb(table, Select); + qb.addColumn(condition.mColumn); + qb.addCondition(joinCondition); + + // Convert the subquery to string + condition.mColumn.reserve(1024); + condition.mColumn.resize(0); + condition.mColumn += QLatin1String("( "); + qb.buildQuery(&condition.mColumn); + condition.mColumn += QLatin1String(" )"); +} + +void QueryBuilder::buildQuery(QString *statement) +{ + // we add the ON conditions of Inner Joins in a Update query here + // but don't want to change the mRootCondition on each exec(). + Query::Condition whereCondition = mRootCondition[WhereCondition]; + + switch (mType) { + case Select: + // Enable forward-only on all SELECT queries, since we never need to + // iterate backwards. This is a memory optimization. + mQuery.setForwardOnly(true); + *statement += QLatin1String("SELECT "); + if (mDistinct) { + *statement += QLatin1String("DISTINCT "); + } + Q_ASSERT_X(mColumns.count() > 0, "QueryBuilder::exec()", "No columns specified"); + appendJoined(statement, mColumns); + *statement += QLatin1String(" FROM "); + *statement += mTableSubQuery.isValid() + ? getTableQuery(mTableSubQuery, mTable) : mTable; + for (const QString &joinedTable : std::as_const(mJoinedTables)) { + const auto &[joinType, joinCond] = mJoins.value(joinedTable); + switch (joinType) { + case LeftJoin: + *statement += QLatin1String(" LEFT JOIN "); + break; + case InnerJoin: + *statement += QLatin1String(" INNER JOIN "); + break; + } + *statement += joinedTable; + *statement += QLatin1String(" ON "); + buildWhereCondition(statement, joinCond); + } + break; + case Insert: { + *statement += QLatin1String("INSERT INTO "); + *statement += mTable; + *statement += QLatin1String(" ("); + for (int i = 0, c = mColumnValues.size(); i < c; ++i) { + *statement += mColumnValues.at(i).first; + if (i + 1 < c) { + *statement += QLatin1String(", "); + } + } + *statement += QLatin1String(") VALUES ("); + for (int i = 0, c = mColumnValues.size(); i < c; ++i) { + bindValue(statement, mColumnValues.at(i).second); + if (i + 1 < c) { + *statement += QLatin1String(", "); + } + } + *statement += QLatin1Char(')'); + if (mDatabaseType == DbType::PostgreSQL && !mIdentificationColumn.isEmpty()) { + *statement += QLatin1String(" RETURNING ") + mIdentificationColumn; + } + break; + } + case Update: { + // put the ON condition into the WHERE part of the UPDATE query + if (mDatabaseType != DbType::Sqlite) { + for (const QString &table : std::as_const(mJoinedTables)) { + const auto &[joinType, joinCond] = mJoins.value(table); + Q_ASSERT(joinType == InnerJoin); + whereCondition.addCondition(joinCond); + } + } else { + // Note: this will modify the whereCondition + sqliteAdaptUpdateJoin(whereCondition); + } + + *statement += QLatin1String("UPDATE "); + *statement += mTable; + + if (mDatabaseType == DbType::MySQL && !mJoinedTables.isEmpty()) { + // for mysql we list all tables directly + *statement += QLatin1String(", "); + appendJoined(statement, mJoinedTables); + } + + *statement += QLatin1String(" SET "); + Q_ASSERT_X(mColumnValues.count() >= 1, "QueryBuilder::exec()", "At least one column needs to be changed"); + for (int i = 0, c = mColumnValues.size(); i < c; ++i) { + const auto &[column, value] = mColumnValues.at(i); + *statement += column; + *statement += QLatin1String(" = "); + bindValue(statement, value); + if (i + 1 < c) { + *statement += QLatin1String(", "); + } + } + + if (mDatabaseType == DbType::PostgreSQL && !mJoinedTables.isEmpty()) { + // PSQL have this syntax + // FROM t1 JOIN t2 JOIN ... + *statement += QLatin1String(" FROM "); + appendJoined(statement, mJoinedTables, QLatin1String(" JOIN ")); + } + break; + } + case Delete: + *statement += QLatin1String("DELETE FROM "); + *statement += mTable; + break; + default: + Q_ASSERT_X(false, "QueryBuilder::exec()", "Unknown enum value"); + } + + if (!whereCondition.isEmpty()) { + *statement += QLatin1String(" WHERE "); + buildWhereCondition(statement, whereCondition); + } + + if (!mGroupColumns.isEmpty()) { + *statement += QLatin1String(" GROUP BY "); + appendJoined(statement, mGroupColumns); + } + + if (!mRootCondition[HavingCondition].isEmpty()) { + *statement += QLatin1String(" HAVING "); + buildWhereCondition(statement, mRootCondition[HavingCondition]); + } + + if (!mSortColumns.isEmpty()) { + Q_ASSERT_X(mType == Select, "QueryBuilder::exec()", "Order statements are only valid for SELECT queries"); + *statement += QLatin1String(" ORDER BY "); + for (int i = 0, c = mSortColumns.size(); i < c; ++i) { + const auto &[column, order] = mSortColumns.at(i); + *statement += column; + *statement += sortOrderToString(order); + if (i + 1 < c) { + *statement += QLatin1String(", "); + } + } + } + + if (mLimit > 0) { + *statement += QLatin1String(" LIMIT ") + QString::number(mLimit); + if (mOffset > 0) { + *statement += QLatin1String(" OFFSET ") + QString::number(mOffset); + } + } + + if (mType == Select && mForUpdate) { + if (mDatabaseType == DbType::Sqlite) { + // SQLite does not support SELECT ... FOR UPDATE syntax, because it does + // table-level locking + } else { + *statement += QLatin1String(" FOR UPDATE"); + } + } +} + +bool QueryBuilder::exec() +{ + QString statement; + statement.reserve(1024); + buildQuery(&statement); + +#ifndef QUERYBUILDER_UNITTEST + auto query = QueryCache::query(statement); + if (query.has_value()) { + mQuery = *query; + } else { + mQuery.clear(); + if (!mQuery.prepare(statement)) { + qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR while PREPARING QUERY:"; + qCCritical(AKONADISERVER_LOG) << " Error code:" << mQuery.lastError().nativeErrorCode(); + qCCritical(AKONADISERVER_LOG) << " DB error: " << mQuery.lastError().databaseText(); + qCCritical(AKONADISERVER_LOG) << " Error text:" << mQuery.lastError().text(); + qCCritical(AKONADISERVER_LOG) << " Query:" << statement; + return false; + } + QueryCache::insert(statement, mQuery); + } + + // too heavy debug info but worths to have from time to time + // qCDebug(AKONADISERVER_LOG) << "Executing query" << statement; + bool isBatch = false; + for (int i = 0; i < mBindValues.count(); ++i) { + mQuery.bindValue(QLatin1Char(':') + QString::number(i), mBindValues[i]); + if (!isBatch && static_cast(mBindValues[i].type()) == QMetaType::QVariantList) { + isBatch = true; + } + // qCDebug(AKONADISERVER_LOG) << QString::fromLatin1( ":%1" ).arg( i ) << mBindValues[i]; + } + + bool ret; + + if (StorageDebugger::instance()->isSQLDebuggingEnabled()) { + QElapsedTimer t; + t.start(); + if (isBatch) { + ret = mQuery.execBatch(); + } else { + ret = mQuery.exec(); + } + StorageDebugger::instance()->queryExecuted(reinterpret_cast(DataStore::self()), mQuery, t.elapsed()); + } else { + StorageDebugger::instance()->incSequence(); + if (isBatch) { + ret = mQuery.execBatch(); + } else { + ret = mQuery.exec(); + } + } + + if (!ret) { + bool needsRetry = false; + // Handle transaction deadlocks and timeouts by attempting to replay the transaction. + if (mDatabaseType == DbType::PostgreSQL) { + const QString dbError = mQuery.lastError().databaseText(); + if (dbError.contains(QLatin1String("40P01" /* deadlock_detected */))) { + qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; + qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); + needsRetry = true; + } + } else if (mDatabaseType == DbType::MySQL) { + const QString lastErrorStr = mQuery.lastError().nativeErrorCode(); + const int error = lastErrorStr.isEmpty() ? -1 : lastErrorStr.toInt(); + if (error == 1213 /* ER_LOCK_DEADLOCK */) { + qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; + qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); + needsRetry = true; + } else if (error == 1205 /* ER_LOCK_WAIT_TIMEOUT */) { + qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; + qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); + // Not sure retrying helps, maybe error is good enough.... but doesn't hurt to retry a few times before giving up. + needsRetry = true; + } + } else if (mDatabaseType == DbType::Sqlite && !DbType::isSystemSQLite(DataStore::self()->database())) { + const QString lastErrorStr = mQuery.lastError().nativeErrorCode(); + const int error = lastErrorStr.isEmpty() ? -1 : lastErrorStr.toInt(); + if (error == 6 /* SQLITE_LOCKED */) { + qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction deadlock, retrying transaction"; + qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); + DataStore::self()->doRollback(); + needsRetry = true; + } else if (error == 5 /* SQLITE_BUSY */) { + qCWarning(AKONADISERVER_LOG) << "QueryBuilder::exec(): database reported transaction timeout, retrying transaction"; + qCWarning(AKONADISERVER_LOG) << mQuery.lastError().text(); + DataStore::self()->doRollback(); + needsRetry = true; + } + } else if (mDatabaseType == DbType::Sqlite) { + // We can't have a transaction deadlock in SQLite when using driver shipped + // with Qt, because it does not support concurrent transactions and DataStore + // serializes them through a global lock. + } + + if (needsRetry) { + DataStore::self()->transactionKilledByDB(); + throw DbDeadlockException(mQuery); + } + + qCCritical(AKONADISERVER_LOG) << "DATABASE ERROR:"; + qCCritical(AKONADISERVER_LOG) << " Error code:" << mQuery.lastError().nativeErrorCode(); + qCCritical(AKONADISERVER_LOG) << " DB error: " << mQuery.lastError().databaseText(); + qCCritical(AKONADISERVER_LOG) << " Error text:" << mQuery.lastError().text(); + qCCritical(AKONADISERVER_LOG) << " Values:" << mQuery.boundValues(); + qCCritical(AKONADISERVER_LOG) << " Query:" << statement; + return false; + } +#else + mStatement = statement; +#endif + return true; +} + +void QueryBuilder::addColumns(const QStringList &cols) +{ + mColumns << cols; +} + +void QueryBuilder::addColumn(const QString &col) +{ + mColumns << col; +} + +void QueryBuilder::addColumn(const Query::Case &caseStmt) +{ + QString query; + buildCaseStatement(&query, caseStmt); + mColumns.append(query); +} + +void QueryBuilder::addAggregation(const QString &col, const QString &aggregate) +{ + mColumns.append(aggregate + QLatin1Char('(') + col + QLatin1Char(')')); +} + +void QueryBuilder::addAggregation(const Query::Case &caseStmt, const QString &aggregate) +{ + QString query(aggregate + QLatin1Char('(')); + buildCaseStatement(&query, caseStmt); + query += QLatin1Char(')'); + + mColumns.append(query); +} + +void QueryBuilder::bindValue(QString *query, const QVariant &value) +{ + mBindValues << value; + *query += QLatin1Char(':') + QString::number(mBindValues.count() - 1); +} + +void QueryBuilder::buildWhereCondition(QString *query, const Query::Condition &cond) +{ + if (!cond.isEmpty()) { + *query += QLatin1String("( "); + const QLatin1String glue = logicOperatorToString(cond.mCombineOp); + const Query::Condition::List &subConditions = cond.subConditions(); + for (int i = 0, c = subConditions.size(); i < c; ++i) { + buildWhereCondition(query, subConditions.at(i)); + if (i + 1 < c) { + *query += glue; + } + } + *query += QLatin1String(" )"); + } else { + *query += cond.mColumn; + *query += compareOperatorToString(cond.mCompareOp); + if (cond.mComparedColumn.isEmpty()) { + if (cond.mComparedValue.isValid()) { + if (cond.mComparedValue.canConvert(QVariant::List)) { + *query += QLatin1String("( "); + const QVariantList &entries = cond.mComparedValue.toList(); + Q_ASSERT_X(!entries.isEmpty(), "QueryBuilder::buildWhereCondition()", "No values given for IN condition."); + for (int i = 0, c = entries.size(); i < c; ++i) { + bindValue(query, entries.at(i)); + if (i + 1 < c) { + *query += QLatin1String(", "); + } + } + *query += QLatin1String(" )"); + } else { + bindValue(query, cond.mComparedValue); + } + } else { + *query += QLatin1String("NULL"); + } + } else { + *query += cond.mComparedColumn; + } + } +} + +void QueryBuilder::buildCaseStatement(QString *query, const Query::Case &caseStmt) +{ + *query += QLatin1String("CASE "); + Q_FOREACH (const auto &whenThen, caseStmt.mWhenThen) { + *query += QLatin1String("WHEN "); + buildWhereCondition(query, whenThen.first); // When + *query += QLatin1String(" THEN ") + whenThen.second; // then + } + if (!caseStmt.mElse.isEmpty()) { + *query += QLatin1String(" ELSE ") + caseStmt.mElse; + } + *query += QLatin1String(" END"); +} + +void QueryBuilder::setSubQueryMode(Query::LogicOperator op, ConditionType type) +{ + Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); + mRootCondition[type].setSubQueryMode(op); +} + +void QueryBuilder::addCondition(const Query::Condition &condition, ConditionType type) +{ + Q_ASSERT(type == WhereCondition || (type == HavingCondition && mType == Select)); + mRootCondition[type].addCondition(condition); +} + +void QueryBuilder::addSortColumn(const QString &column, Query::SortOrder order) +{ + mSortColumns << qMakePair(column, order); +} + +void QueryBuilder::addGroupColumn(const QString &column) +{ + Q_ASSERT(mType == Select); + mGroupColumns << column; +} + +void QueryBuilder::addGroupColumns(const QStringList &columns) +{ + Q_ASSERT(mType == Select); + mGroupColumns += columns; +} + +void QueryBuilder::setColumnValue(const QString &column, const QVariant &value) +{ + mColumnValues << qMakePair(column, value); +} + +void QueryBuilder::setDistinct(bool distinct) +{ + mDistinct = distinct; +} + +void QueryBuilder::setLimit(int limit, int offset) +{ + mLimit = limit; + mOffset = offset; +} + +void QueryBuilder::setIdentificationColumn(const QString &column) +{ + mIdentificationColumn = column; +} + +qint64 QueryBuilder::insertId() +{ + if (mDatabaseType == DbType::PostgreSQL) { + query().next(); + if (mIdentificationColumn.isEmpty()) { + return 0; // FIXME: Does this make sense? + } + return query().record().value(mIdentificationColumn).toLongLong(); + } else { + const QVariant v = query().lastInsertId(); + if (!v.isValid()) { + return -1; + } + bool ok; + const qint64 insertId = v.toLongLong(&ok); + if (!ok) { + return -1; + } + return insertId; + } + return -1; +} + +void QueryBuilder::setForUpdate(bool forUpdate) +{ + mForUpdate = forUpdate; +} + +QString QueryBuilder::getTable() const +{ + return mTable; +} + +QString QueryBuilder::getTableQuery(const QSqlQuery& query, const QString &alias) +{ + Q_ASSERT_X(query.isValid() && query.isSelect(), "QueryBuilder::getTableQuery", "Table subquery use only for valid SELECT queries"); + + QString tableQuery = query.lastQuery(); + if (tableQuery.isEmpty()) { + qCWarning(AKONADISERVER_LOG) << "Table subquery is empty"; + return tableQuery; + } + + tableQuery.prepend(QLatin1String("( ")); + + const auto boundValues = query.boundValues(); + for (int pos = boundValues.size() - 1; pos >= 0; --pos) { + + const QString key(QLatin1Char(':') + QString::number(pos)); + const auto value = boundValues.value(key); + + QSqlField field(QLatin1String(""), value.type()); + if (value.isNull()) { + field.clear(); + } + else { + field.setValue(value); + } + + const QString formattedValue = query.driver()->formatValue(field); + tableQuery.replace(key, formattedValue); + } + + tableQuery.append(QLatin1String(" ) AS %1").arg(alias)); + + return tableQuery; +} diff --git a/src/server/storage/querybuilder.h b/src/server/storage/querybuilder.h new file mode 100644 index 0000000..311e129 --- /dev/null +++ b/src/server/storage/querybuilder.h @@ -0,0 +1,305 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef AKONADI_QUERYBUILDER_H +#define AKONADI_QUERYBUILDER_H + +#include "dbtype.h" +#include "query.h" + +#include +#include +#include +#include +#include +#include + +#ifdef QUERYBUILDER_UNITTEST +class QueryBuilderTest; +#endif + +namespace Akonadi +{ +namespace Server +{ +/** + Helper class to construct arbitrary SQL queries. +*/ +class QueryBuilder +{ +public: + enum QueryType { + Select, + Insert, + Update, + Delete, + }; + + /** + * When the same table gets joined as both, Inner- and LeftJoin, + * it will be merged into a single InnerJoin since it is more + * restrictive. + */ + enum JoinType { + /// NOTE: only supported for UPDATE and SELECT queries. + InnerJoin, + /// NOTE: only supported for SELECT queries + LeftJoin + }; + + /** + * Defines the place at which a condition should be evaluated. + */ + enum ConditionType { + /// add condition to WHERE part of the query + WhereCondition, + /// add condition to HAVING part of the query + /// NOTE: only supported for SELECT queries + HavingCondition, + + NUM_CONDITIONS + }; + + /** + Creates a new query builder. + + @param table The main table to operate on. + */ + explicit QueryBuilder(const QString &table, QueryType type = Select); + + /** + Creates a new query builder with subquery in FROM cluase for SELECT queries. + @param tableQuery must be a valid select query + @param tableQueryAlias alias name for table query + */ + explicit QueryBuilder(const QSqlQuery &tableQuery, const QString &tableQueryAlias); + + /** + Sets the database which should execute the query. Unfortunately the SQL "standard" + is not interpreted in the same way everywhere... + */ + void setDatabaseType(DbType::Type type); + + /** + Join a table to the query. + + NOTE: make sure the @c JoinType is supported by the current @c QueryType + @param joinType The type of JOIN you want to add. + @param table The table to join. + @param condition the ON condition for this join. + */ + void addJoin(JoinType joinType, const QString &table, const Query::Condition &condition); + + /** + Join a table to the query. + This is a convenience method to create simple joins like e.g. 'LEFT JOIN t ON c1 = c2'. + + NOTE: make sure the @c JoinType is supported by the current @c QueryType + @param joinType The type of JOIN you want to add. + @param table The table to join. + @param col1 The first column for the ON statement. + @param col2 The second column for the ON statement. + */ + void addJoin(JoinType joinType, const QString &table, const QString &col1, const QString &col2); + + /** + Adds the given columns to a select query. + @param cols The columns you want to select. + */ + void addColumns(const QStringList &cols); + + /** + Adds the given column to a select query. + @param col The column to add. + */ + void addColumn(const QString &col); + + /** + * Adds the given case statement to a select query. + * @param caseStmt The case statement to add. + */ + void addColumn(const Query::Case &caseStmt); + + /** + * Adds an aggregation statement. + * @param col The column to aggregate on + * @param aggregate The aggregation function. + */ + void addAggregation(const QString &col, const QString &aggregate); + + /** + * Adds and aggregation statement with CASE + * @param caseStmt The case statement to aggregate on + * @param aggregate The aggregation function. + */ + void addAggregation(const Query::Case &caseStmt, const QString &aggregate); + + /** + Add a WHERE or HAVING condition which compares a column with a given value. + @param column The column that should be compared. + @param op The operator used for comparison + @param value The value @p column is compared to. + @param type Defines whether this condition should be part of the WHERE or the HAVING + part of the query. Defaults to WHERE. + */ + void addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type = WhereCondition); + + /** + Add a WHERE or HAVING condition which compares a column with another column. + @param column The column that should be compared. + @param op The operator used for comparison. + @param column2 The column @p column is compared to. + @param type Defines whether this condition should be part of the WHERE or the HAVING + part of the query. Defaults to WHERE. + */ + void addColumnCondition(const QString &column, Query::CompareOperator op, const QString &column2, ConditionType type = WhereCondition); + + /** + Add a WHERE condition. Use this to build hierarchical conditions. + @param condition The condition that the resultset should satisfy. + @param type Defines whether this condition should be part of the WHERE or the HAVING + part of the query. Defaults to WHERE. + */ + void addCondition(const Query::Condition &condition, ConditionType type = WhereCondition); + + /** + Define how WHERE or HAVING conditions are combined. + @todo Give this method a better name. + @param op The logical operator that should be used to combine the conditions. + @param type Defines whether the operator should be used for WHERE or for HAVING + conditions. Defaults to WHERE conditions. + */ + void setSubQueryMode(Query::LogicOperator op, ConditionType type = WhereCondition); + + /** + Add sort column. + @param column Name of the column to sort. + @param order Sort order + */ + void addSortColumn(const QString &column, Query::SortOrder order = Query::Ascending); + + /** + Add a GROUP BY column. + NOTE: Only supported in SELECT queries. + @param column Name of the column to use for grouping. + */ + void addGroupColumn(const QString &column); + + /** + Add list of columns to GROUP BY. + NOTE: Only supported in SELECT queries. + @param columns Names of columns to use for grouping. + */ + void addGroupColumns(const QStringList &columns); + + /** + Sets a column to the given value (only valid for INSERT and UPDATE queries). + @param column Column to change. + @param value The value @p column should be set to. + */ + void setColumnValue(const QString &column, const QVariant &value); + + /** + * Specify whether duplicates should be included in the result. + * @param distinct @c true to remove duplicates, @c false is the default + */ + void setDistinct(bool distinct); + + /** + * Limits the amount of retrieved rows. + * @param limit the maximum number of rows to retrieve. + * @param offset offset of the first row to retrieve. + * The default value for @p offset is -1, indicating no offset. + * @note This has no effect on anything but SELECT queries. + */ + void setLimit(int limit, int offset=-1); + + /** + * Sets the column used for identification in an INSERT statement. + * The default is "id", only change this on tables without such a column + * (usually n:m helper tables). + * @param column Name of the identification column, empty string to disable this. + * @note This only affects PostgreSQL. + * @see insertId() + */ + void setIdentificationColumn(const QString &column); + + /** + Returns the query, only valid after exec(). + */ + QSqlQuery &query(); + + /** + Executes the query, returns true on success. + */ + bool exec(); + + /** + Returns the ID of the newly created record (only valid for INSERT queries) + @note This will assert when being used with setIdentificationColumn() called + with an empty string. + @returns -1 if invalid + */ + qint64 insertId(); + + /** + Indicate to the database to acquire an exclusive lock on the rows already during + SELECT statement. + + Only makes sense in SELECT queries. + */ + void setForUpdate(bool forUpdate = true); + + /** + Returns the name of the main table or subquery. + */ + QString getTable() const; + +private: + void buildQuery(QString *query); + void bindValue(QString *query, const QVariant &value); + void buildWhereCondition(QString *query, const Query::Condition &cond); + void buildCaseStatement(QString *query, const Query::Case &caseStmt); + QString getTableQuery(const QSqlQuery &query, const QString &alias); + + /** + * SQLite does not support JOINs with UPDATE, so we have to convert it into + * subqueries + */ + void sqliteAdaptUpdateJoin(Query::Condition &cond); + +private: + QString mTable; + QSqlQuery mTableSubQuery; + DbType::Type mDatabaseType; + Query::Condition mRootCondition[NUM_CONDITIONS]; + QSqlQuery mQuery; + QueryType mType; + QStringList mColumns; + QVector mBindValues; + QVector> mSortColumns; + QStringList mGroupColumns; + QVector> mColumnValues; + QString mIdentificationColumn; + + // we must make sure that the tables are joined in the correct order + // QMap sorts by key which might invalidate the queries + QStringList mJoinedTables; + QMap> mJoins; + int mLimit; + int mOffset; + bool mDistinct; + bool mForUpdate = false; +#ifdef QUERYBUILDER_UNITTEST + QString mStatement; + friend class ::QueryBuilderTest; +#endif +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/querycache.cpp b/src/server/storage/querycache.cpp new file mode 100644 index 0000000..e8c9298 --- /dev/null +++ b/src/server/storage/querycache.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "querycache.h" +#include "datastore.h" +#include "dbtype.h" + +#include +#include +#include +#include + +#include +#include + +using namespace std::chrono_literals; +using namespace Akonadi; +using namespace Akonadi::Server; + +namespace +{ +// After these seconds without activity the cache is cleaned +constexpr auto CleanupTimeout = 60s; +constexpr int MaxCacheSize = 50; + +/// LRU cache with limited size and auto-cleanup after given +/// period of time +class Cache +{ +public: + Cache() + { + QObject::connect(&m_cleanupTimer, &QTimer::timeout, std::bind(&Cache::cleanup, this)); + m_cleanupTimer.setSingleShot(true); + } + + std::optional query(const QString &queryStatement) + { + m_cleanupTimer.start(CleanupTimeout); + auto it = m_keys.find(queryStatement); + if (it == m_keys.end()) { + return std::nullopt; + } + + auto node = **it; + m_queries.erase(*it); + m_queries.push_front(node); + *it = m_queries.begin(); + return node.query; + } + + void insert(const QString &queryStatement, const QSqlQuery &query) + { + if (m_queries.size() >= MaxCacheSize) { + m_keys.remove(m_queries.back().queryStatement); + m_queries.pop_back(); + } + + m_queries.emplace_front(Node{queryStatement, query}); + m_keys.insert(queryStatement, m_queries.begin()); + } + + void cleanup() + { + m_keys.clear(); + m_queries.clear(); + } + +public: // public, this is just a helper class + struct Node { + QString queryStatement; + QSqlQuery query; + }; + std::list m_queries; + QHash::iterator> m_keys; + QTimer m_cleanupTimer; +}; + +QThreadStorage g_queryCache; + +Cache *perThreadCache() +{ + if (!g_queryCache.hasLocalData()) { + g_queryCache.setLocalData(new Cache()); + } + + return g_queryCache.localData(); +} + +} // namespace + +std::optional QueryCache::query(const QString &queryStatement) +{ + return perThreadCache()->query(queryStatement); +} + +void QueryCache::insert(const QString &queryStatement, const QSqlQuery &query) +{ + if (DbType::type(DataStore::self()->database()) != DbType::Sqlite) { + perThreadCache()->insert(queryStatement, query); + } +} + +void QueryCache::clear() +{ + if (!g_queryCache.hasLocalData()) { + return; + } + + g_queryCache.localData()->cleanup(); +} diff --git a/src/server/storage/querycache.h b/src/server/storage/querycache.h new file mode 100644 index 0000000..545abe6 --- /dev/null +++ b/src/server/storage/querycache.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2012 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include + +class QString; +class QSqlQuery; + +namespace Akonadi +{ +namespace Server +{ +/** + * A per-thread cache (should be per session, but that'S the same for us) prepared + * query cache. + */ +namespace QueryCache +{ +/// Returns the cached (and prepared) query for @p queryStatement +std::optional query(const QString &queryStatement); + +/// Insert @p query into the cache for @p queryStatement. +void insert(const QString &queryStatement, const QSqlQuery &query); + +/// Clears all queries from current thread +void clear(); + +} // namespace QueryCache + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/queryhelper.cpp b/src/server/storage/queryhelper.cpp new file mode 100644 index 0000000..b22f5d1 --- /dev/null +++ b/src/server/storage/queryhelper.cpp @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "queryhelper.h" + +#include "storage/querybuilder.h" + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void QueryHelper::setToQuery(const ImapSet &set, const QString &column, QueryBuilder &qb) +{ + Query::Condition cond(Query::Or); + Q_FOREACH (const ImapInterval &i, set.intervals()) { + if (i.hasDefinedBegin() && i.hasDefinedEnd()) { + if (i.size() == 1) { + cond.addValueCondition(column, Query::Equals, i.begin()); + } else { + if (i.begin() != 1) { // 1 is our standard lower bound, so we don't have to check for it explicitly + Query::Condition subCond(Query::And); + subCond.addValueCondition(column, Query::GreaterOrEqual, i.begin()); + subCond.addValueCondition(column, Query::LessOrEqual, i.end()); + cond.addCondition(subCond); + } else { + cond.addValueCondition(column, Query::LessOrEqual, i.end()); + } + } + } else if (i.hasDefinedBegin()) { + if (i.begin() != 1) { // 1 is our standard lower bound, so we don't have to check for it explicitly + cond.addValueCondition(column, Query::GreaterOrEqual, i.begin()); + } + } else if (i.hasDefinedEnd()) { + cond.addValueCondition(column, Query::LessOrEqual, i.end()); + } + } + if (!cond.isEmpty()) { + qb.addCondition(cond); + } +} diff --git a/src/server/storage/queryhelper.h b/src/server/storage/queryhelper.h new file mode 100644 index 0000000..f360478 --- /dev/null +++ b/src/server/storage/queryhelper.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +class QString; + +namespace Akonadi +{ +class ImapSet; + +namespace Server +{ +class QueryBuilder; + +/** + Helper methods for common query tasks. +*/ +namespace QueryHelper +{ +/** + Add conditions to @p qb for the given uid set @p set applied to @p column. +*/ +void setToQuery(const ImapSet &set, const QString &column, QueryBuilder &qb); + +} // namespace QueryHelper + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/schema-header.xsl b/src/server/storage/schema-header.xsl new file mode 100644 index 0000000..bb0f78d --- /dev/null +++ b/src/server/storage/schema-header.xsl @@ -0,0 +1,33 @@ + + + + + + +#ifndef _H +#define _H + +#include "src/server/storage/schema.h" + +namespace Akonadi { +namespace Server { + +class : public Schema +{ + public: + QVector<TableDescription> tables() Q_DECL_OVERRIDE; + QVector<RelationDescription> relations() Q_DECL_OVERRIDE; +}; + +} // namespace Server +} // namespace Akonadi + +#endif + + + diff --git a/src/server/storage/schema-source.xsl b/src/server/storage/schema-source.xsl new file mode 100644 index 0000000..8ded9e2 --- /dev/null +++ b/src/server/storage/schema-source.xsl @@ -0,0 +1,149 @@ + + + + + + + + + + + + + +#include ".h" + +using namespace Akonadi::Server; + +QVector<TableDescription> ::tables() +{ + QVector<TableDescription> tabs; + tabs.reserve(); + + { + TableDescription t; + t.name = QStringLiteral("Table"); + + t.columns.reserve(); + + { + ColumnDescription c; + c.name = QStringLiteral(""); + c.type = QStringLiteral(""); + + c.size = ; + + + c.allowNull = ; + + + c.isAutoIncrement = ; + + + c.isPrimaryKey = ; + + + c.isUnique = ; + + + c.isEnum = true; + + + c.refTable = QStringLiteral(""); + + + c.refColumn = QStringLiteral(""); + + + c.defaultValue = QStringLiteral(""); + + + c.onUpdate = ColumnDescription::; + + + c.onDelete = ColumnDescription::; + + + c.noUpdate = ; + + + + c.enumValueMap = { + + + { QStringLiteral("::"), + + + + }, + + + }; + + + t.columns.push_back(c); + } + + + + + + t + + + + + + t.data.reserve(); + + { + const QStringList columns = QStringLiteral("").split( QLatin1Char( ',' ), Qt::SkipEmptyParts ); + const QStringList values = QStringLiteral("").split( QLatin1Char( ',' ), Qt::SkipEmptyParts ); + Q_ASSERT( columns.count() == values.count() ); + + DataDescription d; + for ( int i = 0; i < columns.size(); ++i ) { + d.data.insert( columns.at( i ), values.at( i ) ); + } + t.data.push_back(d); + } + + + + tabs.push_back(t); + } + + return tabs; +} + +QVector<RelationDescription> ::relations() +{ + QVector<RelationDescription> rels; + rels.reserve(); + + { + RelationDescription r; + r.firstTable = QStringLiteral(""); + r.firstColumn = QStringLiteral(""); + r.secondTable = QStringLiteral(""); + r.secondColumn = QStringLiteral(""); + + + r + + + rels.push_back(r); + + } + + return rels; +} + + + + diff --git a/src/server/storage/schema.h b/src/server/storage/schema.h new file mode 100644 index 0000000..3830781 --- /dev/null +++ b/src/server/storage/schema.h @@ -0,0 +1,38 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include "schematypes.h" + +namespace Akonadi +{ +namespace Server +{ +/** Methods to access the desired database schema (@see DbInspector for accessing + the actual database schema). + */ +class Schema +{ +public: + inline virtual ~Schema() = default; + + /** List of tables in the schema. */ + virtual QVector tables() = 0; + + /** List of relations (N:M helper tables) in the schema. */ + virtual QVector relations() = 0; + +protected: + explicit Schema() = default; + +private: + Q_DISABLE_COPY_MOVE(Schema) +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/schema.xsl b/src/server/storage/schema.xsl new file mode 100644 index 0000000..f915f65 --- /dev/null +++ b/src/server/storage/schema.xsl @@ -0,0 +1,61 @@ + + + + + + + + + +header + +MySchema + +schema + + + + .indexes.reserve(); + + { + IndexDescription idx; + idx.name = QStringLiteral(""); + idx.columns = QStringLiteral("").split(QLatin1Char( ',' ), Qt::SkipEmptyParts); + + idx.isUnique = ; + + + idx.sort = QStringLiteral(""); + + + .indexes.push_back(idx); + } + + + + + + +/* + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + + + + + + + + + + + + + + diff --git a/src/server/storage/schematypes.cpp b/src/server/storage/schematypes.cpp new file mode 100644 index 0000000..d655764 --- /dev/null +++ b/src/server/storage/schematypes.cpp @@ -0,0 +1,82 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#include "schematypes.h" + +#include + +using namespace Akonadi::Server; + +ColumnDescription::ColumnDescription() + : size(-1) + , allowNull(true) + , isAutoIncrement(false) + , isPrimaryKey(false) + , isUnique(false) + , isEnum(false) + , onUpdate(Cascade) + , onDelete(Cascade) + , noUpdate(false) +{ +} + +IndexDescription::IndexDescription() + : isUnique(false) +{ +} + +DataDescription::DataDescription() +{ +} + +TableDescription::TableDescription() +{ +} + +int TableDescription::primaryKeyColumnCount() const +{ + return std::count_if(columns.constBegin(), columns.constEnd(), [](const ColumnDescription &col) { + return col.isPrimaryKey; + }); +} + +RelationDescription::RelationDescription() +{ +} + +RelationTableDescription::RelationTableDescription(const RelationDescription &relation) + : TableDescription() +{ + name = relation.firstTable + relation.secondTable + QStringLiteral("Relation"); + + columns.reserve(2); + ColumnDescription column; + column.type = QStringLiteral("qint64"); + column.allowNull = false; + column.isPrimaryKey = true; + column.onUpdate = ColumnDescription::Cascade; + column.onDelete = ColumnDescription::Cascade; + column.name = relation.firstTable + QLatin1Char('_') + relation.firstColumn; + column.refTable = relation.firstTable; + column.refColumn = relation.firstColumn; + columns.push_back(column); + IndexDescription index; + index.name = QStringLiteral("%1Index").arg(column.name); + index.columns = QStringList{column.name}; + index.isUnique = false; + indexes.push_back(index); + + column.name = relation.secondTable + QLatin1Char('_') + relation.secondColumn; + column.refTable = relation.secondTable; + column.refColumn = relation.secondColumn; + columns.push_back(column); + index.name = QStringLiteral("%1Index").arg(column.name); + index.columns = QStringList{column.name}; + indexes.push_back(index); + + indexes += relation.indexes; +} diff --git a/src/server/storage/schematypes.h b/src/server/storage/schematypes.h new file mode 100644 index 0000000..a146df9 --- /dev/null +++ b/src/server/storage/schematypes.h @@ -0,0 +1,120 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * SPDX-FileCopyrightText: 2013 Volker Krause * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +namespace Akonadi +{ +namespace Server +{ +/** + * @short A helper class that describes a column of a table for the DbInitializer + */ +class ColumnDescription +{ +public: + ColumnDescription(); + + enum ReferentialAction { + Cascade, + Restrict, + SetNull, + }; + + QString name; + QString type; + int size; + bool allowNull; + bool isAutoIncrement; + bool isPrimaryKey; + bool isUnique; + bool isEnum; + QString refTable; + QString refColumn; + QString defaultValue; + ReferentialAction onUpdate; + ReferentialAction onDelete; + bool noUpdate; + + QMap enumValueMap; +}; + +/** + * @short A helper class that describes indexes of a table for the DbInitializer + */ +class IndexDescription +{ +public: + IndexDescription(); + + QString name; + QStringList columns; + bool isUnique; + QString sort; +}; + +/** + * @short A helper class that describes the predefined data of a table for the DbInitializer + */ +class DataDescription +{ +public: + DataDescription(); + + /** + * Key contains the column name, value the data. + */ + QMap data; +}; + +/** + * @short A helper class that describes a table for the DbInitializer + */ +class TableDescription +{ +public: + TableDescription(); + int primaryKeyColumnCount() const; + + QString name; + QVector columns; + QVector indexes; + QVector data; +}; + +/** + * @short A helper class that describes the relation between two tables for the DbInitializer + */ +class RelationDescription +{ +public: + RelationDescription(); + + QString firstTable; + QString firstColumn; + QString secondTable; + QString secondColumn; + QVector indexes; +}; + +/** + * @short TableDescription constructed based on RelationDescription + */ +class RelationTableDescription : public TableDescription +{ +public: + explicit RelationTableDescription(const RelationDescription &relation); +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/selectquerybuilder.h b/src/server/storage/selectquerybuilder.h new file mode 100644 index 0000000..ff7c2cd --- /dev/null +++ b/src/server/storage/selectquerybuilder.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef AKONADI_SELECTQUERYBUILDER_H +#define AKONADI_SELECTQUERYBUILDER_H + +#include "storage/querybuilder.h" + +namespace Akonadi +{ +namespace Server +{ +/** + Helper class for creating and executing database SELECT queries. +*/ +template class SelectQueryBuilder : public QueryBuilder +{ +public: + /** + Creates a new query builder. + */ + inline SelectQueryBuilder() + : QueryBuilder(T::tableName(), Select) + { + addColumns(T::fullColumnNames()); + } + + /** + Returns the result of this SELECT query. + */ + QVector result() + { + return T::extractResult(query()); + } +}; + +} // namespace Server +} // namespace Akonadi + +#endif diff --git a/src/server/storage/storagedebugger.cpp b/src/server/storage/storagedebugger.cpp new file mode 100644 index 0000000..747f363 --- /dev/null +++ b/src/server/storage/storagedebugger.cpp @@ -0,0 +1,223 @@ +/* + * SPDX-FileCopyrightText: 2013 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "storagedebugger.h" +#include "storagedebuggeradaptor.h" + +#include +#include +#include + +#include + +Akonadi::Server::StorageDebugger *Akonadi::Server::StorageDebugger::mSelf = nullptr; +QMutex Akonadi::Server::StorageDebugger::mMutex; + +using namespace Akonadi::Server; + +Q_DECLARE_METATYPE(QList>) +Q_DECLARE_METATYPE(DbConnection) +Q_DECLARE_METATYPE(QVector) + +QDBusArgument &operator<<(QDBusArgument &arg, const DbConnection &con) +{ + arg.beginStructure(); + arg << con.id << con.name << con.start << con.trxName << con.transactionStart; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, DbConnection &con) +{ + arg.beginStructure(); + arg >> con.id >> con.name >> con.start >> con.trxName >> con.transactionStart; + arg.endStructure(); + return arg; +} + +namespace +{ +QVector::Iterator findConnection(QVector &vec, qint64 id) +{ + return std::find_if(vec.begin(), vec.end(), [id](const DbConnection &con) { + return con.id == id; + }); +} + +} // namespace + +StorageDebugger *StorageDebugger::instance() +{ + mMutex.lock(); + if (mSelf == nullptr) { + mSelf = new StorageDebugger(); + } + mMutex.unlock(); + + return mSelf; +} + +StorageDebugger::StorageDebugger() +{ + qDBusRegisterMetaType>>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + new StorageDebuggerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/storageDebug"), this, QDBusConnection::ExportAdaptors); +} + +StorageDebugger::~StorageDebugger() = default; + +void StorageDebugger::enableSQLDebugging(bool enable) +{ + mEnabled = enable; +} + +void StorageDebugger::writeToFile(const QString &file) +{ + mFile = std::make_unique(file); + mFile->open(QIODevice::WriteOnly); +} + +void StorageDebugger::addConnection(qint64 id, const QString &name) +{ + QMutexLocker locker(&mMutex); + const qint64 timestamp = QDateTime::currentMSecsSinceEpoch(); + mConnections.push_back({id, name, timestamp, QString(), 0LL}); + if (mEnabled) { + Q_EMIT connectionOpened(id, timestamp, name); + } +} + +void StorageDebugger::removeConnection(qint64 id) +{ + QMutexLocker locker(&mMutex); + auto con = findConnection(mConnections, id); + if (con == mConnections.end()) { + return; + } + mConnections.erase(con); + + if (mEnabled) { + Q_EMIT connectionClosed(id, QDateTime::currentMSecsSinceEpoch()); + } +} + +void StorageDebugger::changeConnection(qint64 id, const QString &name) +{ + QMutexLocker locker(&mMutex); + auto con = findConnection(mConnections, id); + if (con == mConnections.end()) { + return; + } + con->name = name; + + if (mEnabled) { + Q_EMIT connectionChanged(id, name); + } +} + +void StorageDebugger::addTransaction(qint64 connectionId, const QString &name, uint duration, const QString &error) +{ + QMutexLocker locker(&mMutex); + auto con = findConnection(mConnections, connectionId); + if (con == mConnections.end()) { + return; + } + con->trxName = name; + con->transactionStart = QDateTime::currentMSecsSinceEpoch(); + + if (mEnabled) { + Q_EMIT transactionStarted(connectionId, name, con->transactionStart, duration, error); + } +} + +void StorageDebugger::removeTransaction(qint64 connectionId, bool commit, uint duration, const QString &error) +{ + QMutexLocker locker(&mMutex); + auto con = findConnection(mConnections, connectionId); + if (con == mConnections.end()) { + return; + } + con->trxName.clear(); + con->transactionStart = 0; + + if (mEnabled) { + Q_EMIT transactionFinished(connectionId, commit, QDateTime::currentMSecsSinceEpoch(), duration, error); + } +} + +QVector StorageDebugger::connections() const +{ + return mConnections; +} + +void StorageDebugger::queryExecuted(qint64 connectionId, const QSqlQuery &query, int duration) +{ + if (!mEnabled) { + return; + } + + const qint64 seq = ++mSequence; + if (query.lastError().isValid()) { + Q_EMIT queryExecuted(seq, + connectionId, + QDateTime::currentMSecsSinceEpoch(), + duration, + query.executedQuery(), + query.boundValues(), + 0, + QList>(), + query.lastError().text()); + return; + } + + QSqlQuery q(query); + QList result; + + if (q.first()) { + const QSqlRecord record = q.record(); + QVariantList row; + const int numRecords = record.count(); + row.reserve(numRecords); + for (int i = 0; i < numRecords; ++i) { + row << record.fieldName(i); + } + result << row; + + int cnt = 0; + do { + const QSqlRecord record = q.record(); + QVariantList row; + const int numRecords = record.count(); + row.reserve(numRecords); + for (int i = 0; i < numRecords; ++i) { + row << record.value(i); + } + result << row; + } while (q.next() && ++cnt < 1000); + } + + const int querySize = query.isSelect() ? query.size() : query.numRowsAffected(); + Q_EMIT queryExecuted(seq, + connectionId, + QDateTime::currentMSecsSinceEpoch(), + duration, + query.executedQuery(), + query.boundValues(), + querySize, + result, + QString()); + + if (mFile && mFile->isOpen()) { + QTextStream out(mFile.get()); + out << query.executedQuery() << " " << duration << "ms\n"; + } + + // Reset the query + q.seek(-1, false); +} diff --git a/src/server/storage/storagedebugger.h b/src/server/storage/storagedebugger.h new file mode 100644 index 0000000..8976d9f --- /dev/null +++ b/src/server/storage/storagedebugger.h @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2013 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +class QSqlQuery; +class QDBusArgument; + +struct DbConnection { + qint64 id; + QString name; + qint64 start; + QString trxName; + qint64 transactionStart; +}; + +QDBusArgument &operator<<(QDBusArgument &arg, const DbConnection &con); +QDBusArgument &operator>>(QDBusArgument &arg, DbConnection &con); + +namespace Akonadi +{ +namespace Server +{ +class StorageDebugger : public QObject +{ + Q_OBJECT + +public: + static StorageDebugger *instance(); + + ~StorageDebugger() override; + + void addConnection(qint64 id, const QString &name); + void removeConnection(qint64 id); + void changeConnection(qint64 id, const QString &name); + void addTransaction(qint64 connectionId, const QString &name, uint duration, const QString &error); + void removeTransaction(qint64 connectionId, bool commit, uint duration, const QString &error); + + void enableSQLDebugging(bool enable); + inline bool isSQLDebuggingEnabled() const + { + return mEnabled; + } + + void queryExecuted(qint64 connectionId, const QSqlQuery &query, int duration); + + inline void incSequence() + { + ++mSequence; + } + + void writeToFile(const QString &file); + + Q_SCRIPTABLE QVector connections() const; + +Q_SIGNALS: + void connectionOpened(qint64 id, qint64 timestamp, const QString &name); + void connectionChanged(qint64 id, const QString &name); + void connectionClosed(qint64 id, qint64 timestamp); + + void transactionStarted(qint64 connectionId, const QString &name, qint64 timestamp, uint duration, const QString &error); + void transactionFinished(qint64 connectionId, bool commit, qint64 timestamp, uint duration, const QString &error); + + void queryExecuted(double sequence, + qint64 connectionId, + qint64 timestamp, + uint duration, + const QString &query, + const QMap &values, + int resultsCount, + const QList> &result, + const QString &error); + +private: + StorageDebugger(); + + static StorageDebugger *mSelf; + static QMutex mMutex; + + std::unique_ptr mFile; + + std::atomic_bool mEnabled = {false}; + std::atomic_int64_t mSequence = {0}; + QVector mConnections; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/tagqueryhelper.cpp b/src/server/storage/tagqueryhelper.cpp new file mode 100644 index 0000000..9ac1d57 --- /dev/null +++ b/src/server/storage/tagqueryhelper.cpp @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagqueryhelper.h" + +#include "commandcontext.h" +#include "handler.h" +#include "storage/querybuilder.h" +#include "storage/queryhelper.h" + +#include +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +void TagQueryHelper::remoteIdToQuery(const QStringList &rids, const CommandContext &context, QueryBuilder &qb) +{ + qb.addJoin(QueryBuilder::InnerJoin, TagRemoteIdResourceRelation::tableName(), Tag::idFullColumnName(), TagRemoteIdResourceRelation::tagIdFullColumnName()); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + + if (rids.size() == 1) { + qb.addValueCondition(TagRemoteIdResourceRelation::remoteIdFullColumnName(), Query::Equals, rids.first()); + } else { + qb.addValueCondition(TagRemoteIdResourceRelation::remoteIdFullColumnName(), Query::In, rids); + } +} + +void TagQueryHelper::gidToQuery(const QStringList &gids, const CommandContext &context, QueryBuilder &qb) +{ + if (context.resource().isValid()) { + qb.addJoin(QueryBuilder::InnerJoin, + TagRemoteIdResourceRelation::tableName(), + Tag::idFullColumnName(), + TagRemoteIdResourceRelation::tagIdFullColumnName()); + qb.addValueCondition(TagRemoteIdResourceRelation::resourceIdFullColumnName(), Query::Equals, context.resource().id()); + } + + if (gids.size() == 1) { + qb.addValueCondition(Tag::gidFullColumnName(), Query::Equals, gids.first()); + } else { + qb.addValueCondition(Tag::gidFullColumnName(), Query::In, gids); + } +} + +void TagQueryHelper::scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb) +{ + if (scope.scope() == Scope::Uid) { + QueryHelper::setToQuery(scope.uidSet(), Tag::idFullColumnName(), qb); + return; + } + + if (scope.scope() == Scope::Gid) { + TagQueryHelper::gidToQuery(scope.gidSet(), context, qb); + return; + } + + if (scope.scope() == Scope::Rid) { + if (!context.resource().isValid()) { + throw HandlerException("Operations based on remote identifiers require a resource or collection context"); + } + + if (scope.scope() == Scope::Rid) { + TagQueryHelper::remoteIdToQuery(scope.ridSet(), context, qb); + } + } + + throw HandlerException("HRID tag operations are not permitted"); +} diff --git a/src/server/storage/tagqueryhelper.h b/src/server/storage/tagqueryhelper.h new file mode 100644 index 0000000..2cc4476 --- /dev/null +++ b/src/server/storage/tagqueryhelper.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2015 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "entities.h" + +namespace Akonadi +{ +class Scope; + +namespace Server +{ +class CommandContext; +class QueryBuilder; + +/** + Helper methods to generate WHERE clauses for item queries based on the item set + used in the protocol. +*/ +namespace TagQueryHelper +{ +/** + Add conditions to @p qb for the given remote identifier @p rid. + The rid context is taken from @p context. +*/ +void remoteIdToQuery(const QStringList &rids, const CommandContext &context, QueryBuilder &qb); +void gidToQuery(const QStringList &gids, const CommandContext &context, QueryBuilder &qb); + +/** + Add conditions to @p qb for the given item operation scope @p scope. + The rid context is taken from @p context, if none is specified an exception is thrown. +*/ +void scopeToQuery(const Scope &scope, const CommandContext &context, QueryBuilder &qb); +} + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storage/transaction.cpp b/src/server/storage/transaction.cpp new file mode 100644 index 0000000..46c1821 --- /dev/null +++ b/src/server/storage/transaction.cpp @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "transaction.h" +#include "storage/datastore.h" + +using namespace Akonadi::Server; + +Transaction::Transaction(DataStore *db, const QString &name, bool beginTransaction) + : mDb(db) + , mName(name) + , mCommitted(false) +{ + if (beginTransaction) { + mDb->beginTransaction(mName); + } +} + +Transaction::~Transaction() +{ + if (!mCommitted) { + mDb->rollbackTransaction(); + } +} + +bool Transaction::commit() +{ + mCommitted = true; + return mDb->commitTransaction(); +} + +void Transaction::begin() +{ + mDb->beginTransaction(mName); +} diff --git a/src/server/storage/transaction.h b/src/server/storage/transaction.h new file mode 100644 index 0000000..b90bcd1 --- /dev/null +++ b/src/server/storage/transaction.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Server +{ +class DataStore; + +/** + Helper class for DataStore transaction handling. + Works similar to QMutexLocker. + Supports command-local and session-global transactions. +*/ +class Transaction +{ +public: + /** + Starts a new transaction. The transaction will automatically rolled back + on destruction if it hasn't been committed explicitly before. + If there is already a global transaction in progress, this one will be used + instead of creating a new one. + @param db The corresponding DataStore. You must not delete @p db during + the lifetime of a Transaction object. + @param name A name of the transaction. Used for debugging. + @param beginTransaction if false, the transaction won't be started, until begin is explicitly called. The default is to begin the transaction right away. + */ + explicit Transaction(DataStore *db, const QString &name, bool beginTransaction = true); + + /** + Rolls back the transaction if it hasn't been committed explicitly. + This also happens if a global transaction is used. + */ + ~Transaction(); + + /** + Commits the transaction. Returns true on success. + If a global transaction is used, nothing happens, global transactions have + to be committed explicitly. + */ + bool commit(); + + void begin(); + +private: + Q_DISABLE_COPY(Transaction) + DataStore *mDb; + QString mName; + bool mCommitted; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/storagejanitor.cpp b/src/server/storagejanitor.cpp new file mode 100644 index 0000000..23f231b --- /dev/null +++ b/src/server/storagejanitor.cpp @@ -0,0 +1,863 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "storagejanitor.h" +#include "agentmanagerinterface.h" +#include "akonadi.h" +#include "akonadiserver_debug.h" +#include "entities.h" +#include "resourcemanager.h" +#include "search/searchmanager.h" +#include "search/searchrequest.h" +#include "storage/collectionstatistics.h" +#include "storage/datastore.h" +#include "storage/dbconfig.h" +#include "storage/parthelper.h" +#include "storage/queryhelper.h" +#include "storage/selectquerybuilder.h" +#include "storage/transaction.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +using namespace Akonadi; +using namespace Akonadi::Server; + +StorageJanitor::StorageJanitor(AkonadiServer &akonadi) + : AkThread(QStringLiteral("StorageJanitor"), QThread::IdlePriority) + , m_lostFoundCollectionId(-1) + , m_akonadi(akonadi) +{ +} + +StorageJanitor::~StorageJanitor() +{ + quitThread(); +} + +void StorageJanitor::init() +{ + AkThread::init(); + + QDBusConnection conn = QDBusConnection::sessionBus(); + conn.registerService(DBus::serviceName(DBus::StorageJanitor)); + conn.registerObject(QStringLiteral(AKONADI_DBUS_STORAGEJANITOR_PATH), + this, + QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals); +} + +void StorageJanitor::quit() +{ + QDBusConnection conn = QDBusConnection::sessionBus(); + conn.unregisterObject(QStringLiteral(AKONADI_DBUS_STORAGEJANITOR_PATH), QDBusConnection::UnregisterTree); + conn.unregisterService(DBus::serviceName(DBus::StorageJanitor)); + conn.disconnectFromBus(conn.name()); + + // Make sure all children are deleted within context of this thread + qDeleteAll(children()); + + AkThread::quit(); +} + +void StorageJanitor::check() // implementation of `akonadictl fsck` +{ + m_lostFoundCollectionId = -1; // start with a fresh one each time + + inform("Looking for resources in the DB not matching a configured resource..."); + findOrphanedResources(); + + inform("Looking for collections not belonging to a valid resource..."); + findOrphanedCollections(); + + inform("Checking collection tree consistency..."); + const Collection::List cols = Collection::retrieveAll(); + std::for_each(cols.begin(), cols.end(), [this](const Collection &col) { + checkPathToRoot(col); + }); + + inform("Looking for items not belonging to a valid collection..."); + findOrphanedItems(); + + inform("Looking for item parts not belonging to a valid item..."); + findOrphanedParts(); + + inform("Looking for item flags not belonging to a valid item..."); + findOrphanedPimItemFlags(); + + inform("Looking for overlapping external parts..."); + findOverlappingParts(); + + inform("Verifying external parts..."); + verifyExternalParts(); + + inform("Checking size treshold changes..."); + checkSizeTreshold(); + + inform("Looking for dirty objects..."); + findDirtyObjects(); + + inform("Looking for rid-duplicates not matching the content mime-type of the parent collection"); + findRIDDuplicates(); + + inform("Migrating parts to new cache hierarchy..."); + migrateToLevelledCacheHierarchy(); + + inform("Checking search index consistency..."); + findOrphanSearchIndexEntries(); + + inform("Flushing collection statistics memory cache..."); + m_akonadi.collectionStatistics().expireCache(); + + inform("Making sure virtual search resource and collections exist"); + ensureSearchCollection(); + + /* TODO some ideas for further checks: + * the collection tree is non-cyclic + * content type constraints of collections are not violated + * find unused flags + * find unused mimetypes + * check for dead entries in relation tables + * check if part size matches file size + */ + + inform("Consistency check done."); + + Q_EMIT done(); +} + +qint64 StorageJanitor::lostAndFoundCollection() +{ + if (m_lostFoundCollectionId > 0) { + return m_lostFoundCollectionId; + } + + Transaction transaction(DataStore::self(), QStringLiteral("JANITOR LOST+FOUND")); + Resource lfRes = Resource::retrieveByName(QStringLiteral("akonadi_lost+found_resource")); + if (!lfRes.isValid()) { + lfRes.setName(QStringLiteral("akonadi_lost+found_resource")); + if (!lfRes.insert()) { + qCCritical(AKONADISERVER_LOG) << "Failed to create lost+found resource!"; + } + } + + Collection lfRoot; + SelectQueryBuilder qb; + qb.addValueCondition(Collection::resourceIdFullColumnName(), Query::Equals, lfRes.id()); + qb.addValueCondition(Collection::parentIdFullColumnName(), Query::Is, QVariant()); + if (!qb.exec()) { + qCCritical(AKONADISERVER_LOG) << "Failed to query top level collections"; + return -1; + } + const Collection::List cols = qb.result(); + if (cols.size() > 1) { + qCCritical(AKONADISERVER_LOG) << "More than one top-level lost+found collection!?"; + } else if (cols.size() == 1) { + lfRoot = cols.first(); + } else { + lfRoot.setName(QStringLiteral("lost+found")); + lfRoot.setResourceId(lfRes.id()); + lfRoot.setCachePolicyLocalParts(QStringLiteral("ALL")); + lfRoot.setCachePolicyCacheTimeout(-1); + lfRoot.setCachePolicyInherit(false); + if (!lfRoot.insert()) { + qCCritical(AKONADISERVER_LOG) << "Failed to create lost+found root."; + } + DataStore::self()->notificationCollector()->collectionAdded(lfRoot, lfRes.name().toUtf8()); + } + + Collection lfCol; + lfCol.setName(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))); + lfCol.setResourceId(lfRes.id()); + lfCol.setParentId(lfRoot.id()); + if (!lfCol.insert()) { + qCCritical(AKONADISERVER_LOG) << "Failed to create lost+found collection!"; + } + + const auto retrieveAll = MimeType::retrieveAll(); + for (const MimeType &mt : retrieveAll) { + lfCol.addMimeType(mt); + } + + DataStore::self()->notificationCollector()->collectionAdded(lfCol, lfRes.name().toUtf8()); + + transaction.commit(); + m_lostFoundCollectionId = lfCol.id(); + return m_lostFoundCollectionId; +} + +void StorageJanitor::findOrphanedResources() +{ + SelectQueryBuilder qbres; + OrgFreedesktopAkonadiAgentManagerInterface iface(DBus::serviceName(DBus::Control), QStringLiteral("/AgentManager"), QDBusConnection::sessionBus(), this); + if (!iface.isValid()) { + inform(QStringLiteral("ERROR: Couldn't talk to %1").arg(DBus::Control)); + return; + } + const QStringList knownResources = iface.agentInstances(); + if (knownResources.isEmpty()) { + inform(QStringLiteral("ERROR: no known resources. This must be a mistake?")); + return; + } + qbres.addValueCondition(Resource::nameFullColumnName(), Query::NotIn, QVariant(knownResources)); + qbres.addValueCondition(Resource::idFullColumnName(), Query::NotEquals, 1); // skip akonadi_search_resource + if (!qbres.exec()) { + inform("Failed to query known resources, skipping test"); + return; + } + // qCDebug(AKONADISERVER_LOG) << "SQL:" << qbres.query().lastQuery(); + const Resource::List orphanResources = qbres.result(); + const int orphanResourcesSize(orphanResources.size()); + if (orphanResourcesSize > 0) { + QStringList resourceNames; + resourceNames.reserve(orphanResourcesSize); + for (const Resource &resource : orphanResources) { + resourceNames.append(resource.name()); + } + inform(QStringLiteral("Found %1 orphan resources: %2").arg(orphanResourcesSize).arg(resourceNames.join(QLatin1Char(',')))); + for (const QString &resourceName : std::as_const(resourceNames)) { + inform(QStringLiteral("Removing resource %1").arg(resourceName)); + m_akonadi.resourceManager().removeResourceInstance(resourceName); + } + } +} + +void StorageJanitor::findOrphanedCollections() +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::LeftJoin, Resource::tableName(), Collection::resourceIdFullColumnName(), Resource::idFullColumnName()); + qb.addValueCondition(Resource::idFullColumnName(), Query::Is, QVariant()); + + if (!qb.exec()) { + inform("Failed to query orphaned collections, skipping test"); + return; + } + const Collection::List orphans = qb.result(); + if (!orphans.isEmpty()) { + inform(QLatin1String("Found ") + QString::number(orphans.size()) + QLatin1String(" orphan collections.")); + // TODO: attach to lost+found resource + } +} + +void StorageJanitor::checkPathToRoot(const Collection &col) +{ + if (col.parentId() == 0) { + return; + } + const Collection parent = col.parent(); + if (!parent.isValid()) { + inform(QLatin1String("Collection \"") + col.name() + QLatin1String("\" (id: ") + QString::number(col.id()) + QLatin1String(") has no valid parent.")); + // TODO fix that by attaching to a top-level lost+found folder + return; + } + + if (col.resourceId() != parent.resourceId()) { + inform(QLatin1String("Collection \"") + col.name() + QLatin1String("\" (id: ") + QString::number(col.id()) + + QLatin1String(") belongs to a different resource than its parent.")); + // can/should we actually fix that? + } + + checkPathToRoot(parent); +} + +void StorageJanitor::findOrphanedItems() +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::LeftJoin, Collection::tableName(), PimItem::collectionIdFullColumnName(), Collection::idFullColumnName()); + qb.addValueCondition(Collection::idFullColumnName(), Query::Is, QVariant()); + if (!qb.exec()) { + inform("Failed to query orphaned items, skipping test"); + return; + } + const PimItem::List orphans = qb.result(); + if (!orphans.isEmpty()) { + inform(QLatin1String("Found ") + QString::number(orphans.size()) + QLatin1String(" orphan items.")); + // Attach to lost+found collection + Transaction transaction(DataStore::self(), QStringLiteral("JANITOR ORPHANS")); + QueryBuilder qb(PimItem::tableName(), QueryBuilder::Update); + qint64 col = lostAndFoundCollection(); + if (col == -1) { + return; + } + qb.setColumnValue(PimItem::collectionIdColumn(), col); + QVector imapIds; + imapIds.reserve(orphans.count()); + for (const PimItem &item : std::as_const(orphans)) { + imapIds.append(item.id()); + } + ImapSet set; + set.add(imapIds); + QueryHelper::setToQuery(set, PimItem::idFullColumnName(), qb); + if (qb.exec() && transaction.commit()) { + inform(QLatin1String("Moved orphan items to collection ") + QString::number(col)); + } else { + inform(QLatin1String("Error moving orphan items to collection ") + QString::number(col) + QLatin1String(" : ") + qb.query().lastError().text()); + } + } +} + +void StorageJanitor::findOrphanedParts() +{ + SelectQueryBuilder qb; + qb.addJoin(QueryBuilder::LeftJoin, PimItem::tableName(), Part::pimItemIdFullColumnName(), PimItem::idFullColumnName()); + qb.addValueCondition(PimItem::idFullColumnName(), Query::Is, QVariant()); + if (!qb.exec()) { + inform("Failed to query orphaned parts, skipping test"); + return; + } + const Part::List orphans = qb.result(); + if (!orphans.isEmpty()) { + inform(QLatin1String("Found ") + QString::number(orphans.size()) + QLatin1String(" orphan parts.")); + // TODO: create lost+found items for those? delete? + } +} + +void StorageJanitor::findOrphanedPimItemFlags() +{ + QueryBuilder sqb(PimItemFlagRelation::tableName(), QueryBuilder::Select); + sqb.addColumn(PimItemFlagRelation::leftFullColumnName()); + sqb.addJoin(QueryBuilder::LeftJoin, PimItem::tableName(), PimItemFlagRelation::leftFullColumnName(), PimItem::idFullColumnName()); + sqb.addValueCondition(PimItem::idFullColumnName(), Query::Is, QVariant()); + if (!sqb.exec()) { + inform("Failed to query orphaned item flags, skipping test"); + return; + } + QVector imapIds; + int count = 0; + while (sqb.query().next()) { + ++count; + imapIds.append(sqb.query().value(0).toInt()); + } + sqb.query().finish(); + if (count > 0) { + ImapSet set; + set.add(imapIds); + QueryBuilder qb(PimItemFlagRelation::tableName(), QueryBuilder::Delete); + QueryHelper::setToQuery(set, PimItemFlagRelation::leftFullColumnName(), qb); + if (!qb.exec()) { + qCCritical(AKONADISERVER_LOG) << "Error:" << qb.query().lastError().text(); + return; + } + + inform(QLatin1String("Found and deleted ") + QString::number(count) + QLatin1String(" orphan pim item flags.")); + } +} + +void StorageJanitor::findOverlappingParts() +{ + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::dataColumn()); + qb.addColumn(QLatin1String("count(") + Part::idColumn() + QLatin1String(") as cnt")); + qb.addValueCondition(Part::storageColumn(), Query::Equals, Part::External); + qb.addValueCondition(Part::dataColumn(), Query::IsNot, QVariant()); + qb.addGroupColumn(Part::dataColumn()); + qb.addValueCondition(QLatin1String("count(") + Part::idColumn() + QLatin1String(")"), Query::Greater, 1, QueryBuilder::HavingCondition); + if (!qb.exec()) { + inform("Failed to query overlapping parts, skipping test"); + return; + } + + int count = 0; + while (qb.query().next()) { + ++count; + inform(QLatin1String("Found overlapping part data: ") + qb.query().value(0).toString()); + // TODO: uh oh, this is bad, how do we recover from that? + } + qb.query().finish(); + + if (count > 0) { + inform(QLatin1String("Found ") + QString::number(count) + QLatin1String(" overlapping parts - bad.")); + } +} + +void StorageJanitor::verifyExternalParts() +{ + QSet existingFiles; + QSet usedFiles; + + // list all files + const QString dataDir = StandardDirs::saveDir("data", QStringLiteral("file_db_data")); + QDirIterator it(dataDir, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + existingFiles.insert(it.next()); + } + existingFiles.remove(dataDir + QDir::separator() + QLatin1Char('.')); + existingFiles.remove(dataDir + QDir::separator() + QLatin1String("..")); + inform(QLatin1String("Found ") + QString::number(existingFiles.size()) + QLatin1String(" external files.")); + + // list all parts from the db which claim to have an associated file + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::dataColumn()); + qb.addColumn(Part::pimItemIdColumn()); + qb.addColumn(Part::idColumn()); + qb.addValueCondition(Part::storageColumn(), Query::Equals, Part::External); + qb.addValueCondition(Part::dataColumn(), Query::IsNot, QVariant()); + if (!qb.exec()) { + inform("Failed to query existing parts, skipping test"); + return; + } + while (qb.query().next()) { + const auto filename = qb.query().value(0).toByteArray(); + const auto pimItemId = qb.query().value(1).value(); + const auto partId = qb.query().value(2).value(); + QString partPath; + if (!filename.isEmpty()) { + partPath = ExternalPartStorage::resolveAbsolutePath(filename); + } else { + partPath = ExternalPartStorage::resolveAbsolutePath(ExternalPartStorage::nameForPartId(partId)); + } + if (existingFiles.contains(partPath)) { + usedFiles.insert(partPath); + } else { + inform(QLatin1String("Cleaning up missing external file: ") + partPath + QLatin1String(" for item: ") + QString::number(pimItemId) + + QLatin1String(" on part: ") + QString::number(partId)); + + Part part; + part.setId(partId); + part.setPimItemId(pimItemId); + part.setData(QByteArray()); + part.setDatasize(0); + part.setStorage(Part::Internal); + part.update(); + } + } + qb.query().finish(); + inform(QLatin1String("Found ") + QString::number(usedFiles.size()) + QLatin1String(" external parts.")); + + // see what's left and move it to lost+found + const QSet unreferencedFiles = existingFiles - usedFiles; + if (!unreferencedFiles.isEmpty()) { + const QString lfDir = StandardDirs::saveDir("data", QStringLiteral("file_lost+found")); + for (const QString &file : unreferencedFiles) { + inform(QLatin1String("Found unreferenced external file: ") + file); + const QFileInfo f(file); + QFile::rename(file, lfDir + QDir::separator() + f.fileName()); + } + inform(QStringLiteral("Moved %1 unreferenced files to lost+found.").arg(unreferencedFiles.size())); + } else { + inform("Found no unreferenced external files."); + } +} + +void StorageJanitor::findDirtyObjects() +{ + SelectQueryBuilder cqb; + cqb.setSubQueryMode(Query::Or); + cqb.addValueCondition(Collection::remoteIdColumn(), Query::Is, QVariant()); + cqb.addValueCondition(Collection::remoteIdColumn(), Query::Equals, QString()); + if (!cqb.exec()) { + inform("Failed to query collections without RID, skipping test"); + return; + } + const Collection::List ridLessCols = cqb.result(); + for (const Collection &col : ridLessCols) { + inform(QLatin1String("Collection \"") + col.name() + QLatin1String("\" (id: ") + QString::number(col.id()) + QLatin1String(") has no RID.")); + } + inform(QLatin1String("Found ") + QString::number(ridLessCols.size()) + QLatin1String(" collections without RID.")); + + SelectQueryBuilder iqb1; + iqb1.setSubQueryMode(Query::Or); + iqb1.addValueCondition(PimItem::remoteIdColumn(), Query::Is, QVariant()); + iqb1.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, QString()); + if (!iqb1.exec()) { + inform("Failed to query items without RID, skipping test"); + return; + } + const PimItem::List ridLessItems = iqb1.result(); + for (const PimItem &item : ridLessItems) { + inform(QLatin1String("Item \"") + QString::number(item.id()) + QLatin1String("\" in collection \"") + QString::number(item.collectionId()) + + QLatin1String("\" has no RID.")); + } + inform(QLatin1String("Found ") + QString::number(ridLessItems.size()) + QLatin1String(" items without RID.")); + + SelectQueryBuilder iqb2; + iqb2.addValueCondition(PimItem::dirtyColumn(), Query::Equals, true); + iqb2.addValueCondition(PimItem::remoteIdColumn(), Query::IsNot, QVariant()); + iqb2.addSortColumn(PimItem::idFullColumnName()); + if (!iqb2.exec()) { + inform("Failed to query dirty items, skipping test"); + return; + } + const PimItem::List dirtyItems = iqb2.result(); + for (const PimItem &item : dirtyItems) { + inform(QLatin1String("Item \"") + QString::number(item.id()) + QLatin1String("\" has RID and is dirty.")); + } + inform(QLatin1String("Found ") + QString::number(dirtyItems.size()) + QLatin1String(" dirty items.")); +} + +void StorageJanitor::findRIDDuplicates() +{ + QueryBuilder qb(Collection::tableName(), QueryBuilder::Select); + qb.addColumn(Collection::idColumn()); + qb.addColumn(Collection::nameColumn()); + qb.exec(); + + while (qb.query().next()) { + const auto colId = qb.query().value(0).value(); + const QString name = qb.query().value(1).toString(); + inform(QStringLiteral("Checking ") + name); + + QueryBuilder duplicates(PimItem::tableName(), QueryBuilder::Select); + duplicates.addColumn(PimItem::remoteIdColumn()); + duplicates.addColumn(QStringLiteral("count(") + PimItem::idColumn() + QStringLiteral(") as cnt")); + duplicates.addValueCondition(PimItem::remoteIdColumn(), Query::IsNot, QVariant()); + duplicates.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, colId); + duplicates.addGroupColumn(PimItem::remoteIdColumn()); + duplicates.addValueCondition(QStringLiteral("count(") + PimItem::idColumn() + QLatin1Char(')'), Query::Greater, 1, QueryBuilder::HavingCondition); + duplicates.exec(); + + Akonadi::Server::Collection col = Akonadi::Server::Collection::retrieveById(colId); + const QVector contentMimeTypes = col.mimeTypes(); + QVariantList contentMimeTypesVariantList; + contentMimeTypesVariantList.reserve(contentMimeTypes.count()); + for (const Akonadi::Server::MimeType &mimeType : contentMimeTypes) { + contentMimeTypesVariantList << mimeType.id(); + } + while (duplicates.query().next()) { + const QString rid = duplicates.query().value(0).toString(); + + Query::Condition condition(Query::And); + condition.addValueCondition(PimItem::remoteIdColumn(), Query::Equals, rid); + condition.addValueCondition(PimItem::mimeTypeIdColumn(), Query::NotIn, contentMimeTypesVariantList); + condition.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, colId); + + QueryBuilder items(PimItem::tableName(), QueryBuilder::Select); + items.addColumn(PimItem::idColumn()); + items.addCondition(condition); + if (!items.exec()) { + inform(QStringLiteral("Error while deleting duplicates: ") + items.query().lastError().text()); + continue; + } + QVariantList itemsIds; + while (items.query().next()) { + itemsIds.push_back(items.query().value(0)); + } + items.query().finish(); + if (itemsIds.isEmpty()) { + // the mimetype filter may have dropped some entries from the + // duplicates query + continue; + } + + inform(QStringLiteral("Found duplicates ") + rid); + + SelectQueryBuilder parts; + parts.addValueCondition(Part::pimItemIdFullColumnName(), Query::In, QVariant::fromValue(itemsIds)); + parts.addValueCondition(Part::storageFullColumnName(), Query::Equals, static_cast(Part::External)); + if (parts.exec()) { + const auto partsList = parts.result(); + for (const auto &part : partsList) { + bool exists = false; + const auto filename = ExternalPartStorage::resolveAbsolutePath(part.data(), &exists); + if (exists) { + QFile::remove(filename); + } + } + } + + items = QueryBuilder(PimItem::tableName(), QueryBuilder::Delete); + items.addCondition(condition); + if (!items.exec()) { + inform(QStringLiteral("Error while deleting duplicates ") + items.query().lastError().text()); + } + } + duplicates.query().finish(); + } + qb.query().finish(); +} + +void StorageJanitor::vacuum() +{ + const DbType::Type dbType = DbType::type(DataStore::self()->database()); + if (dbType == DbType::MySQL || dbType == DbType::PostgreSQL) { + inform("vacuuming database, that'll take some time and require a lot of temporary disk space..."); + Q_FOREACH (const QString &table, allDatabaseTables()) { + inform(QStringLiteral("optimizing table %1...").arg(table)); + + QString queryStr; + if (dbType == DbType::MySQL) { + queryStr = QLatin1String("OPTIMIZE TABLE ") + table; + } else if (dbType == DbType::PostgreSQL) { + queryStr = QLatin1String("VACUUM FULL ANALYZE ") + table; + } else { + continue; + } + QSqlQuery q(DataStore::self()->database()); + if (!q.exec(queryStr)) { + qCCritical(AKONADISERVER_LOG) << "failed to optimize table" << table << ":" << q.lastError().text(); + } + } + inform("vacuum done"); + } else { + inform("Vacuum not supported for this database backend."); + } + + Q_EMIT done(); +} + +void StorageJanitor::checkSizeTreshold() +{ + { + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::idFullColumnName()); + qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::Internal); + qb.addValueCondition(Part::datasizeFullColumnName(), Query::Greater, DbConfig::configuredDatabase()->sizeThreshold()); + if (!qb.exec()) { + inform("Failed to query parts larger than treshold, skipping test"); + return; + } + + QSqlQuery query = qb.query(); + inform(QStringLiteral("Found %1 parts to be moved to external files").arg(query.size())); + + while (query.next()) { + Transaction transaction(DataStore::self(), QStringLiteral("JANITOR CHECK SIZE THRESHOLD")); + Part part = Part::retrieveById(query.value(0).toLongLong()); + const QByteArray name = ExternalPartStorage::nameForPartId(part.id()); + const QString partPath = ExternalPartStorage::resolveAbsolutePath(name); + QFile f(partPath); + if (f.exists()) { + qCDebug(AKONADISERVER_LOG) << "External payload file" << name << "already exists"; + // That however is not a critical issue, since the part is not external, + // so we can safely overwrite it + } + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qCCritical(AKONADISERVER_LOG) << "Failed to open file" << name << "for writing"; + continue; + } + if (f.write(part.data()) != part.datasize()) { + qCCritical(AKONADISERVER_LOG) << "Failed to write data to payload file" << name; + f.remove(); + continue; + } + + part.setData(name); + part.setStorage(Part::External); + if (!part.update() || !transaction.commit()) { + qCCritical(AKONADISERVER_LOG) << "Failed to update database entry of part" << part.id(); + f.remove(); + continue; + } + + inform(QStringLiteral("Moved part %1 from database into external file %2").arg(part.id()).arg(QString::fromLatin1(name))); + } + query.finish(); + } + + { + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::idFullColumnName()); + qb.addValueCondition(Part::storageFullColumnName(), Query::Equals, Part::External); + qb.addValueCondition(Part::datasizeFullColumnName(), Query::Less, DbConfig::configuredDatabase()->sizeThreshold()); + if (!qb.exec()) { + inform("Failed to query parts smaller than treshold, skipping test"); + return; + } + + QSqlQuery query = qb.query(); + inform(QStringLiteral("Found %1 parts to be moved to database").arg(query.size())); + + while (query.next()) { + Transaction transaction(DataStore::self(), QStringLiteral("JANITOR CHECK SIZE THRESHOLD 2")); + Part part = Part::retrieveById(query.value(0).toLongLong()); + const QString partPath = ExternalPartStorage::resolveAbsolutePath(part.data()); + QFile f(partPath); + if (!f.exists()) { + qCCritical(AKONADISERVER_LOG) << "Part file" << part.data() << "does not exist"; + continue; + } + if (!f.open(QIODevice::ReadOnly)) { + qCCritical(AKONADISERVER_LOG) << "Failed to open part file" << part.data() << "for reading"; + continue; + } + + part.setStorage(Part::Internal); + part.setData(f.readAll()); + if (part.data().size() != part.datasize()) { + qCCritical(AKONADISERVER_LOG) << "Sizes of" << part.id() << "data don't match"; + continue; + } + if (!part.update() || !transaction.commit()) { + qCCritical(AKONADISERVER_LOG) << "Failed to update database entry of part" << part.id(); + continue; + } + + f.close(); + f.remove(); + inform(QStringLiteral("Moved part %1 from external file into database").arg(part.id())); + } + query.finish(); + } +} + +void StorageJanitor::migrateToLevelledCacheHierarchy() +{ + QueryBuilder qb(Part::tableName(), QueryBuilder::Select); + qb.addColumn(Part::idColumn()); + qb.addColumn(Part::dataColumn()); + qb.addValueCondition(Part::storageColumn(), Query::Equals, Part::External); + if (!qb.exec()) { + inform("Failed to query external payload parts, skipping test"); + return; + } + + QSqlQuery query = qb.query(); + while (query.next()) { + const qint64 id = query.value(0).toLongLong(); + const QByteArray data = query.value(1).toByteArray(); + const QString fileName = QString::fromUtf8(data); + bool oldExists = false; + bool newExists = false; + // Resolve the current path + const QString currentPath = ExternalPartStorage::resolveAbsolutePath(fileName, &oldExists); + // Resolve the new path with legacy fallback disabled, so that it always + // returns the new levelled-cache path, even when the old one exists + const QString newPath = ExternalPartStorage::resolveAbsolutePath(fileName, &newExists, false); + if (!oldExists) { + qCCritical(AKONADISERVER_LOG) << "Old payload part does not exist, skipping part" << fileName; + continue; + } + if (currentPath != newPath) { + if (newExists) { + qCCritical(AKONADISERVER_LOG) << "Part is in legacy location, but the destination file already exists, skipping part" << fileName; + continue; + } + + QFile f(currentPath); + if (!f.rename(newPath)) { + qCCritical(AKONADISERVER_LOG) << "Failed to move part from" << currentPath << " to " << newPath << ":" << f.errorString(); + continue; + } + inform(QStringLiteral("Migrated part %1 to new levelled cache").arg(id)); + } + } + query.finish(); +} + +void StorageJanitor::findOrphanSearchIndexEntries() +{ + QueryBuilder qb(Collection::tableName(), QueryBuilder::Select); + qb.addSortColumn(Collection::idColumn(), Query::Ascending); + qb.addColumn(Collection::idColumn()); + qb.addColumn(Collection::isVirtualColumn()); + if (!qb.exec()) { + inform("Failed to query collections, skipping test"); + return; + } + + QDBusInterface iface(DBus::agentServiceName(QStringLiteral("akonadi_indexing_agent"), DBus::Agent), + QStringLiteral("/"), + QStringLiteral("org.freedesktop.Akonadi.Indexer"), + QDBusConnection::sessionBus()); + if (!iface.isValid()) { + inform("Akonadi Indexing Agent is not running, skipping test"); + return; + } + + QSqlQuery query = qb.query(); + while (query.next()) { + const qint64 colId = query.value(0).toLongLong(); + // Skip virtual collections, they are not indexed + if (query.value(1).toBool()) { + inform(QStringLiteral("Skipping virtual Collection %1").arg(colId)); + continue; + } + + inform(QStringLiteral("Checking Collection %1 search index...").arg(colId)); + SearchRequest req("StorageJanitor", m_akonadi.searchManager(), m_akonadi.agentSearchManager()); + req.setStoreResults(true); + req.setCollections({colId}); + req.setRemoteSearch(false); + req.setQuery(QStringLiteral("{ }")); // empty query to match all + QStringList mts; + Collection col; + col.setId(colId); + const auto colMts = col.mimeTypes(); + if (colMts.isEmpty()) { + // No mimetypes means we don't know which search store to look into, + // skip it. + continue; + } + mts.reserve(colMts.count()); + for (const auto &mt : colMts) { + mts << mt.name(); + } + req.setMimeTypes(mts); + req.exec(); + auto searchResults = req.results(); + + QueryBuilder iqb(PimItem::tableName(), QueryBuilder::Select); + iqb.addColumn(PimItem::idColumn()); + iqb.addValueCondition(PimItem::collectionIdColumn(), Query::Equals, colId); + if (!iqb.exec()) { + inform(QStringLiteral("Failed to query items in collection %1").arg(colId)); + continue; + } + + QSqlQuery itemQuery = iqb.query(); + while (itemQuery.next()) { + searchResults.remove(itemQuery.value(0).toLongLong()); + } + itemQuery.finish(); + + if (!searchResults.isEmpty()) { + inform(QStringLiteral("Collection %1 search index contains %2 orphan items. Scheduling reindexing").arg(colId).arg(searchResults.count())); + iface.call(QDBus::NoBlock, QStringLiteral("reindexCollection"), colId); + } + } + query.finish(); +} + +void StorageJanitor::ensureSearchCollection() +{ + static const auto searchResourceName = QStringLiteral("akonadi_search_resource"); + + auto searchResource = Resource::retrieveByName(searchResourceName); + if (!searchResource.isValid()) { + searchResource.setName(searchResourceName); + searchResource.setIsVirtual(true); + if (!searchResource.insert()) { + inform(QStringLiteral("Failed to create Search resource.")); + return; + } + } + + auto searchCols = Collection::retrieveFiltered(Collection::resourceIdColumn(), searchResource.id()); + if (searchCols.isEmpty()) { + Collection searchCol; + searchCol.setId(1); + searchCol.setName(QStringLiteral("Search")); + searchCol.setResource(searchResource); + searchCol.setIndexPref(Collection::False); + searchCol.setIsVirtual(true); + if (!searchCol.insert()) { + inform(QStringLiteral("Failed to create Search Collection")); + return; + } + } +} + +void StorageJanitor::inform(const char *msg) +{ + inform(QLatin1String(msg)); +} + +void StorageJanitor::inform(const QString &msg) +{ + qCDebug(AKONADISERVER_LOG) << msg; + Q_EMIT information(msg); +} diff --git a/src/server/storagejanitor.h b/src/server/storagejanitor.h new file mode 100644 index 0000000..3772ab1 --- /dev/null +++ b/src/server/storagejanitor.h @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akthread.h" + +#include + +namespace Akonadi +{ +namespace Server +{ +class Collection; +class AkonadiServer; + +/** + * Various database checking/maintenance features. + */ +class StorageJanitor : public AkThread +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.Janitor") + +public: + explicit StorageJanitor(AkonadiServer &mAkonadi); + ~StorageJanitor() override; + +public Q_SLOTS: + /** Triggers a consistency check of the internal storage. */ + Q_SCRIPTABLE Q_NOREPLY void check(); + /** Triggers a vacuuming of the database, that is compacting of unused space. */ + Q_SCRIPTABLE Q_NOREPLY void vacuum(); + +Q_SIGNALS: + /** Sends informational messages to a possible UI for this. */ + Q_SCRIPTABLE void information(const QString &msg); + Q_SCRIPTABLE void done(); + +protected: + void init() override; + void quit() override; + +private: + void inform(const char *msg); + void inform(const QString &msg); + /** Create a lost+found collection if necessary. */ + qint64 lostAndFoundCollection(); + + /** + * Look for resources in the DB not existing in reality. + */ + void findOrphanedResources(); + + /** + * Look for collections belonging to non-existent resources. + */ + void findOrphanedCollections(); + + /** + * Verifies there is a path from @p col to the root of the collection tree + * and that everything along that path belongs to the same resource. + */ + void checkPathToRoot(const Collection &col); + + /** + * Look for items belonging to non-existing collections. + */ + void findOrphanedItems(); + + /** + * Look for parts belonging to non-existing items. + */ + void findOrphanedParts(); + + /** + * Look for item flags belonging to non-existing items. + */ + void findOrphanedPimItemFlags(); + + /** + * Look for parts referring to the same external file. + */ + void findOverlappingParts(); + + /** + * Verify fs and db part state. + */ + void verifyExternalParts(); + + /** + * Look for dirty objects. + */ + void findDirtyObjects(); + + /** + * Look for duplicates by RID. + * + * ..and remove the one that doesn't match the parent collections content mimetype. + */ + void findRIDDuplicates(); + + /** + * Check whether part sizes match what's in database. + * + * If SizeTreshold has change, it will move parts from or to database + * where necessary. + */ + void checkSizeTreshold(); + + /** + * Check if all external payload files are migrated to the levelled folder + * hierarchy and migrates them if necessary + */ + void migrateToLevelledCacheHierarchy(); + + /** + * Check if the search index contains any entries that refer to Akonadi + * Items that no longer exist in the DB. + */ + void findOrphanSearchIndexEntries(); + + /** + * Make sure that the "Search" collection in the virtual search resource + * exists. It is only created during database initialization, so if user + * somehow manages to delete it, their search would be completely borked. + */ + void ensureSearchCollection(); + +private: + qint64 m_lostFoundCollectionId; + AkonadiServer &m_akonadi; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/tracer.cpp b/src/server/tracer.cpp new file mode 100644 index 0000000..d8112d1 --- /dev/null +++ b/src/server/tracer.cpp @@ -0,0 +1,163 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ +#include "tracer.h" + +#include +#include + +#include "traceradaptor.h" + +#include "akonadiserver_debug.h" +#include "dbustracer.h" +#include "filetracer.h" + +#include + +// #define DEFAULT_TRACER QLatin1String( "dbus" ) +#define DEFAULT_TRACER QStringLiteral("null") + +using namespace Akonadi; +using namespace Akonadi::Server; + +Tracer::Tracer() + : mSettings(std::make_unique(Akonadi::StandardDirs::serverConfigFile(), QSettings::IniFormat)) +{ + activateTracer(currentTracer()); + + new TracerAdaptor(this); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/tracing"), this, QDBusConnection::ExportAdaptors); +} + +Tracer::~Tracer() = default; + +void Tracer::beginConnection(const QString &identifier, const QString &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->beginConnection(identifier, msg); + } +} + +void Tracer::endConnection(const QString &identifier, const QString &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->endConnection(identifier, msg); + } +} + +void Tracer::connectionInput(const QString &identifier, const QByteArray &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->connectionInput(identifier, msg); + } +} + +void Akonadi::Server::Tracer::connectionInput(const QString &identifier, qint64 tag, const Protocol::CommandPtr &cmd) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + if (mTracerBackend->connectionFormat() == TracerInterface::Json) { + QJsonObject json; + json[QStringLiteral("tag")] = tag; + Akonadi::Protocol::toJson(cmd.data(), json); + + QJsonDocument doc(json); + + mTracerBackend->connectionInput(identifier, doc.toJson(QJsonDocument::Indented)); + } else { + mTracerBackend->connectionInput(identifier, QByteArray::number(tag) + ' ' + Protocol::debugString(cmd).toUtf8()); + } + } +} + +void Tracer::connectionOutput(const QString &identifier, const QByteArray &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->connectionOutput(identifier, msg); + } +} + +void Tracer::connectionOutput(const QString &identifier, qint64 tag, const Protocol::CommandPtr &cmd) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + if (mTracerBackend->connectionFormat() == TracerInterface::Json) { + QJsonObject json; + json[QStringLiteral("tag")] = tag; + Protocol::toJson(cmd.data(), json); + QJsonDocument doc(json); + + mTracerBackend->connectionOutput(identifier, doc.toJson(QJsonDocument::Indented)); + } else { + mTracerBackend->connectionOutput(identifier, QByteArray::number(tag) + ' ' + Protocol::debugString(cmd).toUtf8()); + } + } +} + +void Tracer::signal(const QString &signalName, const QString &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->signal(signalName, msg); + } +} + +void Tracer::signal(const char *signalName, const QString &msg) +{ + signal(QLatin1String(signalName), msg); +} + +void Tracer::warning(const QString &componentName, const QString &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->warning(componentName, msg); + } +} + +void Tracer::error(const QString &componentName, const QString &msg) +{ + QMutexLocker locker(&mMutex); + if (mTracerBackend) { + mTracerBackend->error(componentName, msg); + } +} + +void Tracer::error(const char *componentName, const QString &msg) +{ + error(QLatin1String(componentName), msg); +} + +QString Tracer::currentTracer() const +{ + QMutexLocker locker(&mMutex); + return mSettings->value(QStringLiteral("Debug/Tracer"), DEFAULT_TRACER).toString(); +} + +void Tracer::activateTracer(const QString &type) +{ + QMutexLocker locker(&mMutex); + + if (type == QLatin1String("file")) { + const QString file = mSettings->value(QStringLiteral("Debug/File"), QStringLiteral("/dev/null")).toString(); + mTracerBackend = std::make_unique(file); + } else if (type == QLatin1String("dbus")) { + mTracerBackend = std::make_unique(); + } else if (type == QLatin1String("null")) { + mTracerBackend.reset(); + } else { + qCCritical(AKONADISERVER_LOG) << "Unknown tracer type" << type; + mTracerBackend.reset(); + return; + } + + mSettings->setValue(QStringLiteral("Debug/Tracer"), type); + mSettings->sync(); +} diff --git a/src/server/tracer.h b/src/server/tracer.h new file mode 100644 index 0000000..a012525 --- /dev/null +++ b/src/server/tracer.h @@ -0,0 +1,156 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#include "tracerinterface.h" +#include + +#include + +class QSettings; + +namespace Akonadi +{ +namespace Protocol +{ +class Command; +using CommandPtr = QSharedPointer; +} + +namespace Server +{ +/** + * The global tracer instance where all akonadi components can + * send their tracing information to. + * + * The tracer will forward these information to the configured backends. + */ +class Tracer : public QObject, public TracerInterface +{ + Q_OBJECT + +public: + explicit Tracer(); + + /** + * Destroys the global tracer instance. + */ + ~Tracer() override; + + template + typename std::enable_if::value>::type connectionOutput(const QString &identifier, qint64 tag, const T &cmd) + { + QByteArray msg; + if (mTracerBackend->connectionFormat() == TracerInterface::Json) { + QJsonObject json; + json[QStringLiteral("tag")] = tag; + cmd.toJson(json); + QJsonDocument doc(json); + + msg = doc.toJson(QJsonDocument::Indented); + } else { + msg = QByteArray::number(tag) + ' ' + Protocol::debugString(cmd).toUtf8(); + } + connectionOutput(identifier, msg); + } + + /** + * Returns the currently activated tracer type. + */ + QString currentTracer() const; + +public Q_SLOTS: + /** + * This method is called whenever a new data (imap) connection to the akonadi server + * is established. + * + * @param identifier The unique identifier for this connection. All input and output + * messages for this connection will have the same identifier. + * + * @param msg A message specific string. + */ + void beginConnection(const QString &identifier, const QString &msg) override; + + /** + * This method is called whenever a data (imap) connection to akonadi server is + * closed. + * + * @param identifier The unique identifier of this connection. + * @param msg A message specific string. + */ + void endConnection(const QString &identifier, const QString &msg) override; + + /** + * This method is called whenever the akonadi server retrieves some data from the + * outside. + * + * @param identifier The unique identifier of the connection on which the data + * is retrieved. + * @param msg A message specific string. + */ + void connectionInput(const QString &identifier, const QByteArray &msg) override; + + void connectionInput(const QString &identifier, qint64 tag, const Protocol::CommandPtr &cmd); + + /** + * This method is called whenever the akonadi server sends some data out to a client. + * + * @param identifier The unique identifier of the connection on which the + * data is send. + * @param msg A message specific string. + */ + void connectionOutput(const QString &identifier, const QByteArray &msg) override; + + void connectionOutput(const QString &identifier, qint64 tag, const Protocol::CommandPtr &cmd); + + /** + * This method is called whenever a dbus signal is emitted on the bus. + * + * @param signalName The name of the signal being sent. + * @param msg A message specific string. + */ + void signal(const QString &signalName, const QString &msg) override; + + /** + Convenience method with internal toLatin1 cast to compile with QT_NO_CAST_FROM_ASCII. + */ + void signal(const char *signalName, const QString &msg); + + /** + * This method is called whenever a component wants to output a warning. + */ + void warning(const QString &componentName, const QString &msg) override; + + /** + * This method is called whenever a component wants to output an error. + */ + void error(const QString &componentName, const QString &msg) override; + + /** + * Convenience method for QT_NO_CAST_FROM_ASCII usage. + */ + void error(const char *componentName, const QString &msg); + + /** + * Activates the given tracer type. + */ + void activateTracer(const QString &type); + +private: + mutable QMutex mMutex; + std::unique_ptr mTracerBackend; + std::unique_ptr mSettings; +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/tracerinterface.h b/src/server/tracerinterface.h new file mode 100644 index 0000000..2667047 --- /dev/null +++ b/src/server/tracerinterface.h @@ -0,0 +1,108 @@ +/*************************************************************************** + * SPDX-FileCopyrightText: 2006 Tobias Koenig * + * * + * SPDX-License-Identifier: LGPL-2.0-or-later * + ***************************************************************************/ + +#pragma once + +#include + +class QByteArray; +class QString; + +namespace Akonadi +{ +namespace Server +{ +/** + * This interface can be reimplemented to deliver tracing information + * of the akonadi server to the outside. + * + * Possible implementations: + * - log file + * - dbus signals + * - live gui + */ +class TracerInterface +{ +public: + enum ConnectionFormat { + DebugString, + Json, + }; + + virtual ~TracerInterface() = default; + + /** + * This method is called whenever a new data (imap) connection to the akonadi server + * is established. + * + * @param identifier The unique identifier for this connection. All input and output + * messages for this connection will have the same identifier. + * + * @param msg A message specific string. + */ + virtual void beginConnection(const QString &identifier, const QString &msg) = 0; + + /** + * This method is called whenever a data (imap) connection to akonadi server is + * closed. + * + * @param identifier The unique identifier of this connection. + * @param msg A message specific string. + */ + virtual void endConnection(const QString &identifier, const QString &msg) = 0; + + /** + * This method is called whenever the akonadi server retrieves some data from the + * outside. + * + * @param identifier The unique identifier of the connection on which the data + * is retrieved. + * @param msg A message specific string. + */ + virtual void connectionInput(const QString &identifier, const QByteArray &msg) = 0; + + /** + * This method is called whenever the akonadi server sends some data out to a client. + * + * @param identifier The unique identifier of the connection on which the + * data is send. + * @param msg A message specific string. + */ + virtual void connectionOutput(const QString &identifier, const QByteArray &msg) = 0; + + /** + * This method is called whenever a dbus signal is emitted on the bus. + * + * @param signalName The name of the signal being sent. + * @param msg A message specific string. + */ + virtual void signal(const QString &signalName, const QString &msg) = 0; + + /** + * This method is called whenever a component wants to output a warning. + */ + virtual void warning(const QString &componentName, const QString &msg) = 0; + + /** + * This method is called whenever a component wants to output an error. + */ + virtual void error(const QString &componentName, const QString &msg) = 0; + + virtual ConnectionFormat connectionFormat() const + { + return DebugString; + } + +protected: + explicit TracerInterface() = default; + +private: + Q_DISABLE_COPY_MOVE(TracerInterface) +}; + +} // namespace Server +} // namespace Akonadi + diff --git a/src/server/utils.cpp b/src/server/utils.cpp new file mode 100644 index 0000000..33e83e8 --- /dev/null +++ b/src/server/utils.cpp @@ -0,0 +1,231 @@ +/* + * SPDX-FileCopyrightText: 2010 Tobias Koenig + * SPDX-FileCopyrightText: 2014 Daniel Vrátil + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + */ + +#include "utils.h" +#include "akonadiserver_debug.h" +#include "instance_p.h" + +#include + +#include +#include +#include +#include + +#if !defined(Q_OS_WIN) +#include +#include +#include +#include +#include + +static QString akonadiSocketDirectory(); +static bool checkSocketDirectory(const QString &path); +static bool createSocketDirectory(const QString &link); +#endif + +#ifdef Q_OS_LINUX +#include +#include +#include +#include +#endif + +using namespace Akonadi; +using namespace Akonadi::Server; + +QString Utils::preferredSocketDirectory(const QString &defaultDirectory, int fnLengthHint) +{ + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadWrite); + const QSettings serverSettings(serverConfigFile, QSettings::IniFormat); + +#if defined(Q_OS_WIN) + const QString socketDir = serverSettings.value(QLatin1String("Connection/SocketDirectory"), defaultDirectory).toString(); +#else + QString socketDir = defaultDirectory; + if (!serverSettings.contains(QStringLiteral("Connection/SocketDirectory"))) { + // if no socket directory is defined, use the symlinked from /tmp + socketDir = akonadiSocketDirectory(); + + if (socketDir.isEmpty()) { // if that does not work, fall back on default + socketDir = defaultDirectory; + } + } else { + socketDir = serverSettings.value(QStringLiteral("Connection/SocketDirectory"), defaultDirectory).toString(); + } + + if (socketDir.contains(QLatin1String("$USER"))) { + const QString userName = QString::fromLocal8Bit(qgetenv("USER")); + if (!userName.isEmpty()) { + socketDir.replace(QLatin1String("$USER"), userName); + } + } + + if (socketDir[0] != QLatin1Char('/')) { + QDir::home().mkdir(socketDir); + socketDir = QDir::homePath() + QLatin1Char('/') + socketDir; + } + + QFileInfo dirInfo(socketDir); + if (!dirInfo.exists()) { + QDir::home().mkpath(dirInfo.absoluteFilePath()); + } + + const std::size_t totalLength = socketDir.length() + 1 + fnLengthHint; + const std::size_t maxLen = sizeof(sockaddr_un::sun_path); + if (totalLength >= maxLen) { + qCCritical(AKONADISERVER_LOG) << "akonadiSocketDirectory() length of" << totalLength << "is longer than the system limit" << maxLen; + } +#endif + return socketDir; +} + +#if !defined(Q_OS_WIN) +QString akonadiSocketDirectory() +{ + const QString hostname = QHostInfo::localHostName(); + + if (hostname.isEmpty()) { + qCCritical(AKONADISERVER_LOG) << "QHostInfo::localHostName() failed"; + return QString(); + } + + const QString identifier = Instance::hasIdentifier() ? Instance::identifier() : QStringLiteral("default"); + const QString link = StandardDirs::saveDir("data") + QStringLiteral("/socket-%1-%2").arg(hostname, identifier); + + if (checkSocketDirectory(link)) { + return QFileInfo(link).symLinkTarget(); + } + + if (createSocketDirectory(link)) { + return QFileInfo(link).symLinkTarget(); + } + + qCCritical(AKONADISERVER_LOG) << "Could not create socket directory for Akonadi."; + return QString(); +} + +static bool checkSocketDirectory(const QString &path) +{ + QFileInfo info(path); + + if (!info.exists()) { + return false; + } + + if (info.isSymLink()) { + info = QFileInfo(info.symLinkTarget()); + } + + if (!info.isDir()) { + return false; + } + + if (info.ownerId() != getuid()) { + return false; + } + + return true; +} + +static bool createSocketDirectory(const QString &link) +{ + const QString directory = StandardDirs::saveDir("runtime"); + + if (!QDir().mkpath(directory)) { + qCCritical(AKONADISERVER_LOG) << "Creating socket directory with name" << directory << "failed:" << strerror(errno); + return false; + } + + QFile::remove(link); + + if (!QFile::link(directory, link)) { + qCCritical(AKONADISERVER_LOG) << "Creating symlink from" << directory << "to" << link << "failed"; + return false; + } + + return true; +} +#endif + +QString Utils::getDirectoryFileSystem(const QString &directory) +{ +#ifndef Q_OS_LINUX + Q_UNUSED(directory) + return QString(); +#else + QString bestMatchPath; + QString bestMatchFS; + + FILE *mtab = setmntent("/etc/mtab", "r"); + if (!mtab) { + return QString(); + } + while (mntent *mnt = getmntent(mtab)) { + if (qstrcmp(mnt->mnt_type, MNTTYPE_IGNORE) == 0) { + continue; + } + + const QString dir = QString::fromLocal8Bit(mnt->mnt_dir); + if (!directory.startsWith(dir) || dir.length() < bestMatchPath.length()) { + continue; + } + + bestMatchPath = dir; + bestMatchFS = QString::fromLocal8Bit(mnt->mnt_type); + } + + endmntent(mtab); + + return bestMatchFS; +#endif +} + +void Utils::disableCoW(const QString &path) +{ +#ifndef Q_OS_LINUX + Q_UNUSED(path) +#else + qCDebug(AKONADISERVER_LOG) << "Detected Btrfs, disabling copy-on-write on database files"; + + // from linux/fs.h, so that Akonadi does not depend on Linux header files +#ifndef FS_IOC_GETFLAGS +#define FS_IOC_GETFLAGS _IOR('f', 1, long) +#endif +#ifndef FS_IOC_SETFLAGS +#define FS_IOC_SETFLAGS _IOW('f', 2, long) +#endif + + // Disable COW on file +#ifndef FS_NOCOW_FL +#define FS_NOCOW_FL 0x00800000 +#endif + + ulong flags = 0; + const int fd = open(qPrintable(path), O_RDONLY); + if (fd == -1) { + qCWarning(AKONADISERVER_LOG) << "Failed to open" << path << "to modify flags (" << errno << ")"; + return; + } + + if (ioctl(fd, FS_IOC_GETFLAGS, &flags) == -1) { + qCWarning(AKONADISERVER_LOG) << "ioctl error: failed to get file flags (" << errno << ")"; + close(fd); + return; + } + if (!(flags & FS_NOCOW_FL)) { + flags |= FS_NOCOW_FL; + if (ioctl(fd, FS_IOC_SETFLAGS, &flags) == -1) { + qCWarning(AKONADISERVER_LOG) << "ioctl error: failed to set file flags (" << errno << ")"; + close(fd); + return; + } + } + close(fd); +#endif +} diff --git a/src/server/utils.h b/src/server/utils.h new file mode 100644 index 0000000..d106d64 --- /dev/null +++ b/src/server/utils.h @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2010 Tobias Koenig + * + * SPDX-License-Identifier: LGPL-2.0-or-later + * + */ + +#pragma once + +#include +#include + +#include "storage/datastore.h" +#include "storage/dbtype.h" + +namespace Akonadi +{ +namespace Server +{ +namespace Utils +{ +/** + * Converts a QVariant to a QString depending on its internal type. + */ +static inline QString variantToString(const QVariant &variant) +{ + if (variant.type() == QVariant::String) { + return variant.toString(); + } else if (variant.type() == QVariant::ByteArray) { + return QString::fromUtf8(variant.toByteArray()); + } else { + qWarning("Unable to convert variant of type %s to QString", variant.typeName()); + Q_ASSERT(false); + return QString(); + } +} + +/** + * Converts a QVariant to a QByteArray depending on its internal type. + */ +static inline QByteArray variantToByteArray(const QVariant &variant) +{ + if (variant.type() == QVariant::String) { + return variant.toString().toUtf8(); + } else if (variant.type() == QVariant::ByteArray) { + return variant.toByteArray(); + } else { + qWarning("Unable to convert variant of type %s to QByteArray", variant.typeName()); + Q_ASSERT(false); + return QByteArray(); + } +} + +static inline QDateTime variantToDateTime(const QVariant &variant) +{ + if (variant.canConvert(QVariant::DateTime)) { + // MySQL and SQLite backends read the datetime from the database and + // assume it's local time. We stored it as UTC though, so we just need + // to change the interpretation in QDateTime. + // PostgreSQL on the other hand reads the datetime and assumes it's + // UTC(?) and converts it to local time via QDateTime::toLocalTime(), + // so we need to convert it back to UTC manually. + switch (DbType::type(DataStore::self()->database())) { + case DbType::MySQL: + case DbType::Sqlite: { + QDateTime dt = variant.toDateTime(); + dt.setTimeSpec(Qt::UTC); + return dt; + } + case DbType::PostgreSQL: + return variant.toDateTime().toUTC(); + default: + Q_UNREACHABLE(); + } + } else { + qWarning("Unable to convert variant of type %s to QDateTime", variant.typeName()); + Q_ASSERT(false); + return QDateTime(); + } +} + +/** + * Returns the socket @p directory that is passed to this method or the one + * the user has overwritten via the config file. + * The passed @p fnLengthHint will also ensure the absolute file path length of the + * directory + separator + hint would not overflow the system limitation. + */ +QString preferredSocketDirectory(const QString &directory, int fnLengthHint = -1); + +/** + * Returns name of filesystem that @p directory is stored on. This + * only works on Linux and returns empty string on other platforms or when it's + * unable to detect the filesystem. + */ +QString getDirectoryFileSystem(const QString &directory); + +/** + * Disables filesystem copy-on-write feature on given file or directory. + * Only works on Linux and does nothing on other platforms. + * + * It was tested only with Btrfs but in theory can be called on any FS that + * supports NOCOW. + */ +void disableCoW(const QString &path); + +} // namespace Utils +} // namespace Server +} // namespace Akonadi + diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt new file mode 100644 index 0000000..13b9cff --- /dev/null +++ b/src/shared/CMakeLists.txt @@ -0,0 +1,32 @@ +add_library(akonadi_shared STATIC) +target_sources(akonadi_shared PRIVATE + akapplication.cpp + akdebug.cpp + akremotelog.cpp +) + +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(akonadi_shared PROPERTIES UNITY_BUILD ON) +endif() + +target_include_directories(akonadi_shared INTERFACE $) + +target_link_libraries(akonadi_shared +PUBLIC + KF5AkonadiPrivate + Qt::Core + KF5::Crash +) + +ecm_generate_headers(shared_HEADERS + HEADER_NAMES + VectorHelper + REQUIRED_HEADERS shared_HEADERS +) + +# shared is not generally a public library, so install only the useful +# public stuff to core +install(FILES + ${shared_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiCore COMPONENT Devel +) diff --git a/src/shared/akapplication.cpp b/src/shared/akapplication.cpp new file mode 100644 index 0000000..35f2334 --- /dev/null +++ b/src/shared/akapplication.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akapplication.h" +#include "akdebug.h" +#include "akremotelog.h" + +#include +#include + +#include +#include + +#include + +AkApplicationBase *AkApplicationBase::sInstance = nullptr; + +AkApplicationBase::AkApplicationBase(std::unique_ptr app, const QLoggingCategory &loggingCategory) + : QObject(nullptr) + , mApp(std::move(app)) + , mLoggingCategory(loggingCategory) +{ + Q_ASSERT(!sInstance); + sInstance = this; + + QCoreApplication::setApplicationName(QStringLiteral("Akonadi")); + QCoreApplication::setApplicationVersion(QStringLiteral(AKONADI_FULL_VERSION)); + mCmdLineParser.addHelpOption(); + mCmdLineParser.addVersionOption(); +} + +AkApplicationBase::~AkApplicationBase() +{ +} + +AkApplicationBase *AkApplicationBase::instance() +{ + Q_ASSERT(sInstance); + return sInstance; +} + +void AkApplicationBase::init() +{ + akInit(mApp->applicationName()); + akInitRemoteLog(); + + if (!QDBusConnection::sessionBus().isConnected()) { + qFatal("D-Bus session bus is not available!"); + } + + // there doesn't seem to be a signal to indicate that the session bus went down, so lets use polling for now + auto timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &AkApplicationBase::pollSessionBus); + timer->start(10 * 1000); +} + +void AkApplicationBase::setDescription(const QString &desc) +{ + mCmdLineParser.setApplicationDescription(desc); +} + +void AkApplicationBase::parseCommandLine() +{ + const QCommandLineOption instanceOption(QStringList() << QStringLiteral("instance"), + QStringLiteral("Namespace for starting multiple Akonadi instances in the same user session"), + QStringLiteral("name")); + mCmdLineParser.addOption(instanceOption); + const QCommandLineOption verboseOption(QStringLiteral("verbose"), QStringLiteral("Make Akonadi very chatty")); + mCmdLineParser.addOption(verboseOption); + + mCmdLineParser.process(QCoreApplication::arguments()); + + if (mCmdLineParser.isSet(instanceOption)) { + Akonadi::Instance::setIdentifier(mCmdLineParser.value(instanceOption)); + } + if (mCmdLineParser.isSet(verboseOption)) { + akMakeVerbose(mLoggingCategory.categoryName()); + } +} + +void AkApplicationBase::pollSessionBus() const +{ + if (!QDBusConnection::sessionBus().isConnected()) { + qCritical("D-Bus session bus went down - quitting"); + mApp->quit(); + } +} + +void AkApplicationBase::addCommandLineOptions(const QCommandLineOption &option) +{ + mCmdLineParser.addOption(option); +} + +void AkApplicationBase::addPositionalCommandLineOption(const QString &name, const QString &description, const QString &syntax) +{ + mCmdLineParser.addPositionalArgument(name, description, syntax); +} + +void AkApplicationBase::printUsage() const +{ + std::cout << qPrintable(mCmdLineParser.helpText()) << std::endl; +} + +int AkApplicationBase::exec() +{ + return mApp->exec(); +} + +QString akGetEnv(const char *name, const QString &defaultValue) +{ + const QString v = QString::fromLocal8Bit(qgetenv(name)); + return !v.isEmpty() ? v : defaultValue; +} diff --git a/src/shared/akapplication.h b/src/shared/akapplication.h new file mode 100644 index 0000000..2850f30 --- /dev/null +++ b/src/shared/akapplication.h @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +class QCoreApplication; +class QApplication; +class QGuiApplication; + +/** + * D-Bus session bus monitoring and command line handling. + */ +class AkApplicationBase : public QObject +{ + Q_OBJECT +public: + ~AkApplicationBase(); + void parseCommandLine(); + void setDescription(const QString &desc); + + void addCommandLineOptions(const QCommandLineOption &option); + void addPositionalCommandLineOption(const QString &name, const QString &description = QString(), const QString &syntax = QString()); + const QCommandLineParser &commandLineArguments() const + { + return mCmdLineParser; + } + + void printUsage() const; + + /** Returns the AkApplication instance */ + static AkApplicationBase *instance(); + + /** Forward to Q[Core]Application for convenience. */ + int exec(); + +protected: + AkApplicationBase(std::unique_ptr app, const QLoggingCategory &loggingCategory); + void init(); + + std::unique_ptr mApp; + +private Q_SLOTS: + void pollSessionBus() const; + +private: + QString mInstanceId; + const QLoggingCategory &mLoggingCategory; + static AkApplicationBase *sInstance; + + QCommandLineParser mCmdLineParser; +}; + +template class AkApplicationImpl : public AkApplicationBase +{ +public: + AkApplicationImpl(int &argc, char **argv, const QLoggingCategory &loggingCategory = *QLoggingCategory::defaultCategory()) + : AkApplicationBase(std::make_unique(argc, argv), loggingCategory) + { + init(); + } +}; + +template class AkUniqueApplicationImpl : public AkApplicationBase +{ +public: + AkUniqueApplicationImpl(int &argc, char **argv, const QString &serviceName, const QLoggingCategory &loggingCategory = *QLoggingCategory::defaultCategory()) + : AkApplicationBase(std::make_unique(argc, argv), loggingCategory) + { + registerUniqueServiceOrTerminate(serviceName, loggingCategory); + init(); + } + +private: + void registerUniqueServiceOrTerminate(const QString &serviceName, const QLoggingCategory &log) + { + auto bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + qCCritical(log, "Session bus not found. Is DBus running?"); + exit(1); + } + + if (!bus.registerService(serviceName)) { + // We couldn't register. Most likely, it's already running. + const QString lastError = bus.lastError().message(); + if (lastError.isEmpty()) { + qCInfo(log, "Service %s already registered, terminating now.", qUtf8Printable(serviceName)); + exit(0); // already running, so it's OK. Terminate now. + } else { + qCCritical(log, "Unable to register service as %s due to an error: %s", qUtf8Printable(serviceName), qUtf8Printable(lastError)); + exit(1); // :( + } + } + } +}; + +/** + * Returns the contents of @p name environment variable if it is defined, + * or @p defaultValue otherwise. + */ +QString akGetEnv(const char *name, const QString &defaultValue = QString()); + +using AkCoreApplication = AkApplicationImpl; +using AkApplication = AkApplicationImpl; +using AkGuiApplication = AkApplicationImpl; +using AkUniqueGuiApplication = AkUniqueApplicationImpl; diff --git a/src/shared/akdebug.cpp b/src/shared/akdebug.cpp new file mode 100644 index 0000000..02a93e6 --- /dev/null +++ b/src/shared/akdebug.cpp @@ -0,0 +1,246 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + Inspired by kdelibs/kdecore/io/kdebug.h + SPDX-FileCopyrightText: 1997 Matthias Kalle Dalheimer + SPDX-FileCopyrightText: 2002 Holger Freyther + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akdebug.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +class FileDebugStream : public QIODevice +{ + Q_OBJECT +public: + FileDebugStream() + : mType(QtCriticalMsg) + { + open(WriteOnly); + } + + bool isSequential() const override + { + return true; + } + qint64 readData(char * /*data*/, qint64 /*maxlen*/) override + { + return 0; + } + qint64 readLineData(char * /*data*/, qint64 /*maxlen*/) override + { + return 0; + } + + qint64 writeData(const char *data, qint64 len) override + { + if (!mFileName.isEmpty()) { + QFile outputFile(mFileName); + outputFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Unbuffered); + outputFile.write(data, len); + outputFile.putChar('\n'); + outputFile.close(); + } + + return len; + } + + void setFileName(const QString &fileName) + { + mFileName = fileName; + } + + void setType(QtMsgType type) + { + mType = type; + } + +private: + QString mFileName; + QtMsgType mType; +}; + +class DebugPrivate +{ +public: + DebugPrivate() + : origHandler(nullptr) + { + } + + ~DebugPrivate() + { + qInstallMessageHandler(origHandler); + file.close(); + } + + static QString errorLogFileName(const QString &name) + { + return Akonadi::StandardDirs::saveDir("data") + QDir::separator() + name + QLatin1String(".error"); + } + + QString errorLogFileName() const + { + return errorLogFileName(name); + } + + void log(QtMsgType type, const QMessageLogContext &context, const QString &msg) + { + QMutexLocker locker(&mutex); +#ifdef QT_NO_DEBUG_OUTPUT + if (type == QtDebugMsg) { + return; + } +#endif + QByteArray buf; + QTextStream str(&buf); + str << QDateTime::currentDateTime().toString(Qt::ISODate) << " ["; + switch (type) { + case QtDebugMsg: + str << "DEBUG"; + break; + case QtInfoMsg: + str << "INFO "; + break; + case QtWarningMsg: + str << "WARN "; + break; + case QtFatalMsg: + str << "FATAL"; + break; + case QtCriticalMsg: + str << "CRITICAL"; + break; + } + str << "] " << context.category << ": "; + if (context.file && *context.file && context.line) { + str << context.file << ":" << context.line << ": "; + } + if (context.function && *context.function) { + str << context.function << ": "; + } + str << msg << "\n"; + str.flush(); + file.write(buf.constData(), buf.size()); + file.flush(); + + if (origHandler) { + origHandler(type, context, msg); + } + } + + void setName(const QString &appName) + { + name = appName; + + if (file.isOpen()) { + file.close(); + } + QFileInfo finfo(errorLogFileName()); + if (!finfo.absoluteDir().exists()) { + QDir().mkpath(finfo.absolutePath()); + } + file.setFileName(errorLogFileName()); + file.open(QIODevice::WriteOnly | QIODevice::Unbuffered); + } + + void setOrigHandler(QtMessageHandler origHandler_) + { + origHandler = origHandler_; + } + + void setOrigCategoryFilter(QLoggingCategory::CategoryFilter origFilter_) + { + origFilter = origFilter_; + } + + QMutex mutex; + QFile file; + QString name; + QtMessageHandler origHandler; + QLoggingCategory::CategoryFilter origFilter; + QByteArray loggingCategory; +}; + +Q_GLOBAL_STATIC(DebugPrivate, sInstance) // NOLINT(readability-redundant-member-init) + +void akMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + switch (type) { + case QtDebugMsg: + case QtInfoMsg: + case QtWarningMsg: + case QtCriticalMsg: + sInstance()->log(type, context, msg); + break; + case QtFatalMsg: + sInstance()->log(QtInfoMsg, context, msg); + abort(); + } +} + +void akCategoryFilter(QLoggingCategory *category) +{ + if ((qstrcmp(category->categoryName(), sInstance()->loggingCategory) == 0) || (qstrcmp(category->categoryName(), "org.kde.pim.akonadiprivate") == 0)) { + category->setEnabled(QtDebugMsg, true); + category->setEnabled(QtInfoMsg, true); + category->setEnabled(QtWarningMsg, true); + category->setEnabled(QtCriticalMsg, true); + category->setEnabled(QtFatalMsg, true); + } else if (sInstance()->origFilter) { + sInstance()->origFilter(category); + } +} + +void akInit(const QString &appName) +{ + KCrash::initialize(); + + const QString name = QFileInfo(appName).fileName(); + const auto errorLogFile = DebugPrivate::errorLogFileName(name); + QFileInfo infoOld(errorLogFile + QLatin1String(".old")); + if (infoOld.exists()) { + QFile fileOld(infoOld.absoluteFilePath()); + const bool success = fileOld.remove(); + if (!success) { + qFatal("Cannot remove old log file '%s': %s", qUtf8Printable(fileOld.fileName()), qUtf8Printable(fileOld.errorString())); + } + } + + QFileInfo info(errorLogFile); + if (info.exists()) { + QFile file(info.absoluteFilePath()); + const QString oldName = errorLogFile + QLatin1String(".old"); + const bool success = file.copy(oldName); + if (!success) { + qFatal("Cannot rename log file '%s' to '%s': %s", qUtf8Printable(file.fileName()), qUtf8Printable(oldName), qUtf8Printable(file.errorString())); + } + } + + QtMessageHandler origHandler = qInstallMessageHandler(akMessageHandler); + sInstance()->setName(name); + sInstance()->setOrigHandler(origHandler); +} + +void akMakeVerbose(const QByteArray &category) +{ + sInstance()->loggingCategory = category; + QLoggingCategory::CategoryFilter oldFilter = QLoggingCategory::installFilter(akCategoryFilter); + sInstance()->setOrigCategoryFilter(oldFilter); +} + +#include "akdebug.moc" diff --git a/src/shared/akdebug.h b/src/shared/akdebug.h new file mode 100644 index 0000000..b9ec80e --- /dev/null +++ b/src/shared/akdebug.h @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +/** + * Init and rotate error logs. + */ +void akInit(const QString &appName); + +void akMakeVerbose(const QByteArray &category); + diff --git a/src/shared/akhelpers.h b/src/shared/akhelpers.h new file mode 100644 index 0000000..09851f3 --- /dev/null +++ b/src/shared/akhelpers.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +namespace Akonadi +{ +static const auto IsNull = [](auto ptr) { + return !(bool)ptr; +}; +static const auto IsNotNull = [](auto ptr) { + return (bool)ptr; +}; + +} // namespace Akonadi + diff --git a/src/shared/akqt.h b/src/shared/akqt.h new file mode 100644 index 0000000..ee2723c --- /dev/null +++ b/src/shared/akqt.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +/// Helper integration between Akonadi and Qt + +namespace Akonadi +{ +template auto akPrivSlot(DPtr &&dptr, Slot &&slot) +{ + return [&dptr, &slot](auto &&...args) { + (dptr->*slot)(std::forward(args)...); + }; +} + +} // namespace + +inline QString operator""_qs(const char16_t *str, std::size_t len) +{ + return QString(reinterpret_cast(str), len); +} + +constexpr QStringView operator""_qsv(const char16_t *str, std::size_t len) +{ + return QStringView(str, len); +} + diff --git a/src/shared/akranges.h b/src/shared/akranges.h new file mode 100644 index 0000000..bd51111 --- /dev/null +++ b/src/shared/akranges.h @@ -0,0 +1,478 @@ +/* + SPDX-FileCopyrightText: 2018-2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akhelpers.h" +#include "aktraits.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace AkRanges +{ +namespace detail +{ +template)> OutContainer copyContainer(const RangeLike &range) +{ + OutContainer rv; + rv.reserve(range.size()); + for (auto &&v : range) { + rv.push_back(std::move(v)); + } + return rv; +} + +template)> OutContainer copyContainer(const RangeLike &range) +{ + OutContainer rv; + rv.reserve(range.size()); + for (const auto &v : range) { + rv.insert(v); // Qt containers lack move-enabled insert() overload + } + return rv; +} + +template OutContainer copyAssocContainer(const RangeList &range) +{ + OutContainer rv; + for (const auto &v : range) { + rv.insert(v.first, v.second); // Qt containers lack move-enabled insert() overload + } + return rv; +} + +template struct IteratorTrait { + using iterator_category = typename Iterator::iterator_category; + using value_type = typename Iterator::value_type; + using difference_type = typename Iterator::difference_type; + using pointer = typename Iterator::pointer; + using reference = typename Iterator::reference; +}; + +// Without QT_STRICT_ITERATORS QVector and QList iterators do not satisfy STL +// iterator concepts since they are nothing more but typedefs to T* - for those +// we need to provide custom traits. +template struct IteratorTrait { + // QTypedArrayData::iterator::iterator_category + using iterator_category = std::random_access_iterator_tag; + using value_type = Iterator; + using difference_type = int; + using pointer = Iterator *; + using reference = Iterator &; +}; + +template struct IteratorTrait { + using iterator_category = std::random_access_iterator_tag; + using value_type = Iterator; + using difference_type = int; + using pointer = const Iterator *; + using reference = const Iterator &; +}; + +template struct IteratorBase { +public: + using iterator_category = typename IteratorTrait::iterator_category; + using value_type = typename IteratorTrait::value_type; + using difference_type = typename IteratorTrait::difference_type; + using pointer = typename IteratorTrait::pointer; + using reference = typename IteratorTrait::reference; + + IteratorBase(const IteratorBase &other) + : mIter(other.mIter) + , mRange(other.mRange) + { + } + + IterImpl &operator++() + { + ++static_cast(this)->mIter; + return *static_cast(this); + } + + IterImpl operator++(int) + { + auto ret = *static_cast(this); + ++static_cast(this)->mIter; + return ret; + } + + bool operator==(const IterImpl &other) const + { + return mIter == other.mIter; + } + + bool operator!=(const IterImpl &other) const + { + return !(*static_cast(this) == other); + } + + bool operator<(const IterImpl &other) const + { + return mIter < other.mIter; + } + + auto operator-(const IterImpl &other) const + { + return mIter - other.mIter; + } + + auto operator*() const + { + return *mIter; + } + +protected: + IteratorBase(const Iterator &iter, const RangeLike &range) + : mIter(iter) + , mRange(range) + { + } + IteratorBase(const Iterator &iter, RangeLike &&range) + : mIter(iter) + , mRange(std::move(range)) + { + } + + Iterator mIter; + RangeLike mRange; +}; + +template +struct TransformIterator : public IteratorBase, RangeLike> { +private: + template struct ResultOf; + + template struct ResultOf { + using type = R; + }; + + template using FuncHelper = decltype(std::invoke(std::declval()...))(Ts...); + using IteratorValueType = typename ResultOf::value_type>>::type; + +public: + using value_type = IteratorValueType; + using pointer = IteratorValueType *; // FIXME: preserve const-ness + using reference = const IteratorValueType &; // FIXME: preserve const-ness + + TransformIterator(const Iterator &iter, const TransformFn &fn, const RangeLike &range) + : IteratorBase, RangeLike>(iter, range) + , mFn(fn) + { + } + + auto operator*() const + { + return std::invoke(mFn, *this->mIter); + } + +private: + TransformFn mFn; +}; + +template +class FilterIterator : public IteratorBase, RangeLike> +{ +public: + FilterIterator(const Iterator &iter, const Iterator &end, const Predicate &predicate, const RangeLike &range) + : IteratorBase(iter, range) + , mPredicate(predicate) + , mEnd(end) + { + while (this->mIter != mEnd && !std::invoke(mPredicate, *this->mIter)) { + ++this->mIter; + } + } + + auto &operator++() + { + if (this->mIter != mEnd) { + do { + ++this->mIter; + } while (this->mIter != mEnd && !std::invoke(mPredicate, *this->mIter)); + } + return *this; + } + + auto operator++(int) + { + auto it = *this; + ++(*this); + return it; + } + +private: + Predicate mPredicate; + Iterator mEnd; +}; + +template +class AssociativeContainerIterator : public IteratorBase, Container, Iterator> +{ +public: + using value_type = std::remove_const_t::type>>; + using pointer = std::add_pointer_t; + using reference = std::add_lvalue_reference_t; + + AssociativeContainerIterator(const Iterator &iter, const Container &container) + : IteratorBase, Container, Iterator>(iter, container) + { + } + + auto operator*() const + { + return std::get(*this->mIter); + } +}; + +template using AssociativeContainerKeyIterator = AssociativeContainerIterator; +template using AssociativeContainerValueIterator = AssociativeContainerIterator; + +template struct Range { +public: + using iterator = Iterator; + using const_iterator = Iterator; + using value_type = typename detail::IteratorTrait::value_type; + + Range(Iterator &&begin, Iterator &&end) + : mBegin(std::move(begin)) + , mEnd(std::move(end)) + { + } + + Iterator begin() const + { + return mBegin; + } + + Iterator cbegin() const + { + return mBegin; + } + + Iterator end() const + { + return mEnd; + } + + Iterator cend() const + { + return mEnd; + } + + auto size() const + { + return std::distance(mBegin, mEnd); + } + +private: + Iterator mBegin; + Iterator mEnd; +}; + +template using IsRange = typename std::is_same>; + +// Tags + +template class Cont> struct ToTag_ { + template using OutputContainer = Cont; +}; + +template class Cont> struct ToAssocTag_ { + template using OuputContainer = Cont; +}; + +struct ValuesTag_ { +}; +struct KeysTag_ { +}; + +template struct TransformTag_ { + UnaryOperation mFn; +}; + +template struct FilterTag_ { + UnaryPredicate mFn; +}; + +template struct ForEachTag_ { + UnaryOperation mFn; +}; + +template struct AllTag_ { + UnaryPredicate mFn; +}; + +template struct AnyTag_ { + UnaryPredicate mFn; +}; + +template struct NoneTag_ { + UnaryPredicate mFn; +}; + +} // namespace detail +} // namespace AkRanges + +// Generic operator| for To_<> convertor +template class OutContainer, typename T = typename RangeLike::value_type> +auto operator|(const RangeLike &range, AkRanges::detail::ToTag_) -> OutContainer +{ + using namespace AkRanges::detail; + return copyContainer>(range); +} + +// Specialization for case when InContainer and OutContainer are identical +// Create a copy, but for Qt container this is very cheap due to implicit sharing. +template class InContainer, typename T> auto operator|(const InContainer &in, AkRanges::detail::ToTag_) -> InContainer +{ + return in; +} + +// Generic operator| for ToAssoc_<> convertor +template class OutContainer, typename T = typename RangeLike::value_type> +auto operator|(const RangeLike &range, AkRanges::detail::ToAssocTag_) -> OutContainer +{ + using namespace AkRanges::detail; + return copyAssocContainer>(range); +} + +// Generic operator| for transform() +template auto operator|(const RangeLike &range, AkRanges::detail::TransformTag_ t) +{ + using namespace AkRanges::detail; + using OutIt = TransformIterator; + return Range(OutIt(std::cbegin(range), t.mFn, range), OutIt(std::cend(range), t.mFn, range)); +} + +// Generic operator| for filter() +template auto operator|(const RangeLike &range, AkRanges::detail::FilterTag_ p) +{ + using namespace AkRanges::detail; + using OutIt = FilterIterator; + return Range(OutIt(std::cbegin(range), std::cend(range), p.mFn, range), OutIt(std::cend(range), std::cend(range), p.mFn, range)); +} + +// Generic operator| for foreach() +template auto operator|(const RangeLike &range, AkRanges::detail::ForEachTag_ op) +{ + std::for_each(std::cbegin(range), std::cend(range), [op = std::move(op)](const auto &val) mutable { + std::invoke(op.mFn, val); + }); + return range; +} + +// Generic operator| for all +template auto operator|(const RangeLike &range, AkRanges::detail::AllTag_ p) +{ + return std::all_of(std::cbegin(range), std::cend(range), p.mFn); +} + +// Generic operator| for any +template auto operator|(const RangeLike &range, AkRanges::detail::AnyTag_ p) +{ + return std::any_of(std::cbegin(range), std::cend(range), p.mFn); +} + +// Generic operator| for none +template auto operator|(const RangeLike &range, AkRanges::detail::NoneTag_ p) +{ + return std::none_of(std::cbegin(range), std::cend(range), p.mFn); +} + +// Generic operator| for keys +template auto operator|(const AssocContainer &in, AkRanges::detail::KeysTag_) +{ + using namespace AkRanges::detail; + using OutIt = AssociativeContainerKeyIterator; + return Range(OutIt(in.constKeyValueBegin(), in), OutIt(in.constKeyValueEnd(), in)); +} + +// Generic operator| for values +template auto operator|(const AssocContainer &in, AkRanges::detail::ValuesTag_) +{ + using namespace AkRanges::detail; + using OutIt = AssociativeContainerValueIterator; + return Range(OutIt(in.constKeyValueBegin(), in), OutIt(in.constKeyValueEnd(), in)); +} + +namespace AkRanges +{ +namespace Actions +{ +/// Non-lazily convert given range or container to QVector +static constexpr auto toQVector = detail::ToTag_{}; +/// Non-lazily convert given range or container to QSet +static constexpr auto toQSet = detail::ToTag_{}; +/// Non-lazily convert given range or container to QList +static constexpr auto toQList = detail::ToTag_{}; +/// Non-lazily convert given range or container of pairs to QMap +static constexpr auto toQMap = detail::ToAssocTag_{}; +/// Non-lazily convert given range or container of pairs to QHash +static constexpr auto toQHash = detail::ToAssocTag_{}; + +/// Non-lazily call UnaryOperation for each element of the container or range +template detail::ForEachTag_ forEach(UnaryOperation &&op) +{ + return detail::ForEachTag_{std::forward(op)}; +} + +/// Non-lazily check that all elements in the range satisfy given predicate +template detail::AllTag_ all(UnaryPredicate &&pred) +{ + return detail::AllTag_{std::forward(pred)}; +} + +/// Non-lazily check that at least one element in range satisfies the given predicate +template detail::AnyTag_ any(UnaryPredicate &&pred) +{ + return detail::AnyTag_{std::forward(pred)}; +} + +/// Non-lazily check that none of the elements in the range satisfies the given predicate +template detail::NoneTag_ none(UnaryPredicate &&pred) +{ + return detail::NoneTag_{std::forward(pred)}; +} + +} // namespace Action + +namespace Views +{ +/// Lazily extract values from an associative container +static constexpr auto values = detail::ValuesTag_{}; +/// Lazily extract keys from an associative container +static constexpr auto keys = detail::KeysTag_{}; + +/// Lazily transform each element of a range or container using given transformation +template detail::TransformTag_ transform(UnaryOperation &&op) +{ + return detail::TransformTag_{std::forward(op)}; +} + +/// Lazily filters a range or container by applying given predicate on each element +template detail::FilterTag_ filter(UnaryPredicate &&pred) +{ + return detail::FilterTag_{std::forward(pred)}; +} + +/// Create a range, a view on a container from the given pair fo iterators +template> detail::Range range(Iterator1 begin, Iterator2 end) +{ + return detail::Range(std::move(begin), std::move(end)); +} + +} // namespace View + +} // namespace AkRanges + diff --git a/src/shared/akremotelog.cpp b/src/shared/akremotelog.cpp new file mode 100644 index 0000000..688ca11 --- /dev/null +++ b/src/shared/akremotelog.cpp @@ -0,0 +1,202 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akremotelog.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#define AKONADICONSOLE_SERVICE "org.kde.akonadiconsole" +#define AKONADICONSOLE_LOGGER_PATH "/logger" +#define AKONADICONSOLE_LOGGER_INTERFACE "org.kde.akonadiconsole.logger" + +namespace +{ +class RemoteLogger : public QObject +{ + Q_OBJECT +public: + explicit RemoteLogger() + : mWatcher(akonadiConsoleServiceName(), + QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration) + { + connect(qApp, &QCoreApplication::aboutToQuit, this, &RemoteLogger::deleteLater); + + sInstance = this; + + // Don't do remote logging for Akonadi Console because it deadlocks it + if (QCoreApplication::applicationName() == QLatin1String("akonadiconsole")) { + return; + } + + connect(&mWatcher, &QDBusServiceWatcher::serviceRegistered, this, &RemoteLogger::serviceRegistered); + connect(&mWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &RemoteLogger::serviceUnregistered); + + mOldHandler = qInstallMessageHandler(dbusLogger); + } + + ~RemoteLogger() + { + sInstance = nullptr; + + QLoggingCategory::installFilter(mOldFilter); + qInstallMessageHandler(mOldHandler); + + mEnabled = false; + } + + static RemoteLogger *self() + { + return sInstance; + } + +private Q_SLOTS: + void serviceRegistered(const QString &service) + { + mAkonadiConsoleInterface = std::make_unique(service, + QStringLiteral(AKONADICONSOLE_LOGGER_PATH), + QStringLiteral(AKONADICONSOLE_LOGGER_INTERFACE), + QDBusConnection::sessionBus(), + this); + if (!mAkonadiConsoleInterface->isValid()) { + mAkonadiConsoleInterface.reset(); + return; + } + + connect(mAkonadiConsoleInterface.get(), // clazy:exclude=old-style-connect + SIGNAL(enabledChanged(bool)), + this, + SLOT(onAkonadiConsoleLoggingEnabled(bool))); + + QTimer::singleShot(0, this, [this]() { + auto watcher = new QDBusPendingCallWatcher(mAkonadiConsoleInterface->asyncCall(QStringLiteral("enabled"))); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; + } + onAkonadiConsoleLoggingEnabled(reply.argumentAt<0>()); + }); + }); + } + + void serviceUnregistered(const QString & /*unused*/) + { + onAkonadiConsoleLoggingEnabled(false); + mAkonadiConsoleInterface.reset(); + } + + void onAkonadiConsoleLoggingEnabled(bool enabled) + { + if (mEnabled == enabled) { + return; + } + + mEnabled = enabled; + if (mEnabled) { + // FIXME: Qt calls our categoryFilter from installFilter() but at that + // point we cannot refer to mOldFilter yet (as we only receive it after + // this call returns. So we set our category filter twice: once to get + // the original Qt filter and second time to force our category filter + // to be called when we already know the old filter. + mOldFilter = QLoggingCategory::installFilter(categoryFilter); + QLoggingCategory::installFilter(categoryFilter); + } else { + QLoggingCategory::installFilter(mOldFilter); + mOldFilter = nullptr; + } + } + +private: + QString akonadiConsoleServiceName() + { + QString service = QStringLiteral(AKONADICONSOLE_SERVICE); + if (Akonadi::Instance::hasIdentifier()) { + service += QStringLiteral("-%1").arg(Akonadi::Instance::identifier()); + } + return service; + } + + static void categoryFilter(QLoggingCategory *cat) + { + auto const that = self(); + if (!that) { + return; + } + + if (qstrncmp(cat->categoryName(), "org.kde.pim.", 12) == 0) { + cat->setEnabled(QtDebugMsg, true); + cat->setEnabled(QtInfoMsg, true); + cat->setEnabled(QtWarningMsg, true); + cat->setEnabled(QtCriticalMsg, true); + } else if (that->mOldFilter) { + that->mOldFilter(cat); + } + } + + static void dbusLogger(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) + { + auto const that = self(); + if (!that) { + return; + } + + // Log to previous logger + that->mOldHandler(type, ctx, msg); + + if (that->mEnabled) { + that->mAkonadiConsoleInterface->asyncCallWithArgumentList(QStringLiteral("message"), + QList{QDateTime::currentMSecsSinceEpoch(), + qAppName(), + qApp->applicationPid(), + static_cast(type), + QString::fromUtf8(ctx.category), + QString::fromUtf8(ctx.file), + QString::fromUtf8(ctx.function), + ctx.line, + ctx.version, + msg}); + } + } + +private: + QDBusServiceWatcher mWatcher; + QLoggingCategory::CategoryFilter mOldFilter = nullptr; + QtMessageHandler mOldHandler = nullptr; + std::unique_ptr mAkonadiConsoleInterface; + bool mEnabled = false; + + static RemoteLogger *sInstance; +}; + +RemoteLogger *RemoteLogger::sInstance = nullptr; + +} // namespace + +void akInitRemoteLog() +{ + Q_ASSERT(qApp->thread() == QThread::currentThread()); + + if (!RemoteLogger::self()) { + new RemoteLogger(); + } +} + +#include "akremotelog.moc" diff --git a/src/shared/akremotelog.h b/src/shared/akremotelog.h new file mode 100644 index 0000000..c3b3270 --- /dev/null +++ b/src/shared/akremotelog.h @@ -0,0 +1,10 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +void akInitRemoteLog(); + diff --git a/src/shared/akscopeguard.h b/src/shared/akscopeguard.h new file mode 100644 index 0000000..ae1f5b7 --- /dev/null +++ b/src/shared/akscopeguard.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace Akonadi +{ +class AkScopeGuard +{ +public: + template + AkScopeGuard(U &&fun) + : mFun(std::move(fun)) + { + } + + AkScopeGuard(const AkScopeGuard &) = delete; + AkScopeGuard(AkScopeGuard &&) = default; + AkScopeGuard &operator=(const AkScopeGuard &) = delete; + AkScopeGuard &operator=(AkScopeGuard &&) = delete; + + ~AkScopeGuard() + { + mFun(); + } + +private: + std::function mFun; +}; + +} // namespace Akonadi + diff --git a/src/shared/akstd.h b/src/shared/akstd.h new file mode 100644 index 0000000..631db9e --- /dev/null +++ b/src/shared/akstd.h @@ -0,0 +1,19 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +/// A glue between Qt and the standard library + +namespace std +{ +} + diff --git a/src/shared/aktest.h b/src/shared/aktest.h new file mode 100644 index 0000000..e5eb566 --- /dev/null +++ b/src/shared/aktest.h @@ -0,0 +1,130 @@ +/* + SPDX-FileCopyrightText: 2011 Volker Krause + SPDX-FileCopyrightText: 2014 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akapplication.h" +#include + +#include + +#include + +#define AKTEST_MAIN(TestObject) \ + int main(int argc, char **argv) \ + { \ + qputenv("XDG_DATA_HOME", ".local-unit-test/share"); \ + qputenv("XDG_CONFIG_HOME", ".config-unit-test"); \ + AkCoreApplication app(argc, argv); \ + KCrash::setDrKonqiEnabled(false); \ + app.parseCommandLine(); \ + TestObject tc; \ + return QTest::qExec(&tc, argc, argv); \ + } + +#define AKTEST_FAKESERVER_MAIN(TestObject) \ + int main(int argc, char **argv) \ + { \ + AkCoreApplication app(argc, argv); \ + KCrash::setDrKonqiEnabled(false); \ + app.addCommandLineOptions(QCommandLineOption(QStringLiteral("no-cleanup"), QStringLiteral("Don't clean up the temporary runtime environment"))); \ + app.parseCommandLine(); \ + TestObject tc; \ + return QTest::qExec(&tc, argc, argv); \ + } + +#define AKCOMPARE(actual, expected) \ + do { \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return false; \ + } while (false) + +#define AKVERIFY(statement) \ + do { \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ + return false; \ + } while (false) + +inline void akTestSetInstanceIdentifier(const QString &instanceId) +{ + Akonadi::Instance::setIdentifier(instanceId); +} + +#include + +namespace QTest +{ +template<> char *toString(const Akonadi::Protocol::ItemChangeNotificationPtr &msg) +{ + return qstrdup(qPrintable(Akonadi::Protocol::debugString(msg))); +} +} + +namespace AkTest +{ +enum NtfField { + NtfType = (1 << 0), + NtfOperation = (1 << 1), + NtfSession = (1 << 2), + NtfEntities = (1 << 3), + NtfResource = (1 << 5), + NtfCollection = (1 << 6), + NtfDestResource = (1 << 7), + NtfDestCollection = (1 << 8), + NtfAddedFlags = (1 << 9), + NtfRemovedFlags = (1 << 10), + NtfAddedTags = (1 << 11), + NtfRemovedTags = (1 << 12), + + NtfFlags = NtfAddedFlags | NtfRemovedTags, + NtfTags = NtfAddedTags | NtfRemovedTags, + NtfAll = NtfType | NtfOperation | NtfSession | NtfEntities | NtfResource | NtfCollection | NtfDestResource | NtfDestCollection | NtfFlags | NtfTags +}; +using NtfFields = QFlags; + +bool compareNotifications(const Akonadi::Protocol::ItemChangeNotificationPtr &actual, + const Akonadi::Protocol::ItemChangeNotificationPtr &expected, + const NtfFields fields = NtfAll) +{ + if (fields & NtfOperation) { + AKCOMPARE(actual->operation(), expected->operation()); + } + if (fields & NtfSession) { + AKCOMPARE(actual->sessionId(), expected->sessionId()); + } + if (fields & NtfEntities) { + AKCOMPARE(actual->items(), expected->items()); + } + if (fields & NtfResource) { + AKCOMPARE(actual->resource(), expected->resource()); + } + if (fields & NtfCollection) { + AKCOMPARE(actual->parentCollection(), expected->parentCollection()); + } + if (fields & NtfDestResource) { + AKCOMPARE(actual->destinationResource(), expected->destinationResource()); + } + if (fields & NtfDestCollection) { + AKCOMPARE(actual->parentDestCollection(), expected->parentDestCollection()); + } + if (fields & NtfAddedFlags) { + AKCOMPARE(actual->addedFlags(), expected->addedFlags()); + } + if (fields & NtfRemovedFlags) { + AKCOMPARE(actual->removedFlags(), expected->removedFlags()); + } + if (fields & NtfAddedTags) { + AKCOMPARE(actual->addedTags(), expected->addedTags()); + } + if (fields & NtfRemovedTags) { + AKCOMPARE(actual->removedTags(), expected->removedTags()); + } + + return true; +} +} + diff --git a/src/shared/aktraits.h b/src/shared/aktraits.h new file mode 100644 index 0000000..a594af3 --- /dev/null +++ b/src/shared/aktraits.h @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2019 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace AkTraits +{ +namespace detail +{ +template struct conjunction : std::true_type { +}; +template struct conjunction : T { +}; +template struct conjunction : std::conditional_t, T> { +}; + +#define DECLARE_HAS_MEBER_TYPE(type_name) \ + template> struct hasMember_##type_name { \ + static constexpr bool value = false; \ + }; \ + \ + template struct hasMember_##type_name> : std::true_type { \ + }; + +DECLARE_HAS_MEBER_TYPE(value_type) + +/// TODO: Use Boost TTI instead? +#define DECLARE_HAS_METHOD_GENERIC_IMPL(name, fun, sign) \ + template struct hasMethod_##name { \ + public: \ + template struct helperClass; \ + \ + using True = char; \ + using False = struct { \ + char dummy_[2]; \ + }; \ + \ + template static True helper(helperClass *); \ + template static False helper(...); \ + \ + public: \ + static constexpr bool value = sizeof(helper(nullptr)) == sizeof(True); \ + }; + +#define DECLARE_HAS_METHOD_GENERIC_CONST(fun, R, ...) DECLARE_HAS_METHOD_GENERIC_IMPL(fun##_const, fun, R (T::*)(__VA_ARGS__) const) + +#define DECLARE_HAS_METHOD_GENERIC(fun, R, ...) DECLARE_HAS_METHOD_GENERIC_IMPL(fun, fun, R (T::*)(__VA_ARGS__)) + +DECLARE_HAS_METHOD_GENERIC_CONST(size, int, void) +DECLARE_HAS_METHOD_GENERIC(push_back, void, const typename T::value_type &) +DECLARE_HAS_METHOD_GENERIC(insert, typename T::iterator, const typename T::value_type &) +DECLARE_HAS_METHOD_GENERIC(reserve, void, int) + +#define DECLARE_HAS_FUNCTION(name, fun) \ + template struct has_##name { \ + template struct helperClass; \ + \ + using True = char; \ + using False = struct { \ + char dummy_[2]; \ + }; \ + \ + template static True helper(helperClass()))> *); \ + template static False helper(...); \ + \ + public: \ + static constexpr bool value = sizeof(helper(nullptr)) == sizeof(True); \ + }; + +// For some obscure reason QVector::begin() actually has a default +// argument, but QList::begin() does not, thus a regular hasMethod_* check +// won't cut it here. Instead we check whether the container object can be +// used with std::begin() and std::end() helpers. +// Check for constness can be performed by passing "const T" to the type. +DECLARE_HAS_FUNCTION(begin, std::begin) +DECLARE_HAS_FUNCTION(end, std::end) + +/// This is a very incomplete set of Container named requirement, but I'm +/// too lazy to implement all of them, but this should be good enough to match +/// regular Qt containers and /not/ match arbitrary non-container types +template +struct isContainer + : conjunction, hasMember_value_type, has_begin, has_begin, has_end, has_end, hasMethod_size_const> { +}; + +/// Matches anything that is a container and has push_back() method. +template struct isAppendable : conjunction, hasMethod_push_back> { +}; + +/// Matches anything that is a container and has insert() method. +template struct isInsertable : conjunction, hasMethod_insert> { +}; + +/// Matches anything that is a container and has reserve() method. +template struct isReservable : conjunction, hasMethod_reserve> { +}; +} + +template constexpr bool isAppendable = detail::isAppendable::value; + +template constexpr bool isInsertable = detail::isInsertable::value; + +template constexpr bool isReservable = detail::isReservable::value; + +} // namespace AkTraits + +#define AK_PP_CAT_(X, Y) X##Y +#define AK_PP_CAT(X, Y) AK_PP_CAT_(X, Y) + +#define AK_REQUIRES(...) bool AK_PP_CAT(_ak_requires_, __LINE__) = false, std::enable_if_t < AK_PP_CAT(_ak_requires_, __LINE__) || (__VA_ARGS__) > * = nullptr + diff --git a/src/shared/vectorhelper.h b/src/shared/vectorhelper.h new file mode 100644 index 0000000..a747f81 --- /dev/null +++ b/src/shared/vectorhelper.h @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2015-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +namespace Akonadi +{ +template class Container> QVector valuesToVector(const Container &container) +{ + QVector values; + values.reserve(container.size()); + for (const auto &value : container) { + values.append(value); + } + return values; +} + +template QSet vectorToSet(const QVector &container) +{ + QSet set; + set.reserve(container.size()); + for (const auto &value : container) { + set.insert(value); + } + return set; +} + +template class Container> QVector setToVector(const Container &container) +{ + QVector values; + values.reserve(container.size()); + for (const auto &value : container) { + values.append(value); + } + return values; +} + +} // namespace Akonadi + diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt new file mode 100644 index 0000000..af9fb58 --- /dev/null +++ b/src/widgets/CMakeLists.txt @@ -0,0 +1,201 @@ +configure_file(akonadiwidgetstests_export.h.in "${CMAKE_CURRENT_BINARY_DIR}/akonadiwidgetstests_export.h") + +add_library(KF5AkonadiWidgets) +add_library(KF5::AkonadiWidgets ALIAS KF5AkonadiWidgets) + + +target_sources(KF5AkonadiWidgets PRIVATE + actionstatemanager.cpp + agentactionmanager.cpp + agentconfigurationdialog.cpp + agentconfigurationwidget.cpp + agentinstancewidget.cpp + agenttypedialog.cpp + agenttypewidget.cpp + cachepolicypage.cpp + collectioncombobox.cpp + collectiondialog.cpp + collectiongeneralpropertiespage.cpp + collectionmaintenancepage.cpp + collectionpropertiesdialog.cpp + collectionpropertiespage.cpp + collectionrequester.cpp + collectionstatisticsdelegate.cpp + collectionview.cpp + conflictresolvedialog.cpp + controlgui.cpp + dragdropmanager.cpp + entitylistview.cpp + entitytreeview.cpp + erroroverlay.cpp + etmviewstatesaver.cpp + itemview.cpp + manageaccountwidget.cpp + progressspinnerdelegate.cpp + recentcollectionaction.cpp + renamefavoritedialog.cpp + standardactionmanager.cpp + selftestdialog.cpp + subscriptiondialog.cpp + tageditwidget.cpp + tagmanagementdialog.cpp + tagselectioncombobox.cpp + tagselectiondialog.cpp + tagwidget.cpp + tagselectwidget.cpp +) + +ecm_qt_declare_logging_category(KF5AkonadiWidgets HEADER akonadiwidgets_debug.h IDENTIFIER AKONADIWIDGETS_LOG CATEGORY_NAME org.kde.pim.akonadiwidgets + DESCRIPTION "akonadi (Akonadi Widget Library)" + OLD_CATEGORY_NAMES akonadiwidgets_log + EXPORT AKONADI + ) + + +set(akonadiwidgets_UI + cachepolicypage.ui + collectiongeneralpropertiespage.ui + collectionmaintenancepage.ui + controlprogressindicator.ui + erroroverlay.ui + manageaccountwidget.ui + renamefavoritedialog.ui + selftestdialog.ui + subscriptiondialog.ui + tageditwidget.ui + tagmanagementdialog.ui + tagselectiondialog.ui + tagwidget.ui +) + +ecm_generate_headers(AkonadiWidgets_HEADERS + HEADER_NAMES + AgentActionManager + AgentConfigurationDialog + AgentConfigurationWidget + AgentInstanceWidget + AgentTypeDialog + AgentTypeWidget + CollectionComboBox + CollectionDialog + CollectionPropertiesDialog + CollectionPropertiesPage + CollectionMaintenancePage + CollectionRequester + CollectionStatisticsDelegate + CollectionView + ControlGui + EntityListView + EntityTreeView + ETMViewStateSaver + ItemView + ManageAccountWidget + StandardActionManager + SubscriptionDialog + TagManagementDialog + TagSelectionComboBox + TagSelectionDialog + TagEditWidget + TagWidget + TagSelectWidget + REQUIRED_HEADERS AkonadiWidgets_HEADERS +) + +ki18n_wrap_ui(KF5AkonadiWidgets ${akonadiwidgets_UI}) +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(KF5AkonadiWidgets PROPERTIES UNITY_BUILD ON) +endif() + +generate_export_header(KF5AkonadiWidgets BASE_NAME akonadiwidgets) + + +target_include_directories(KF5AkonadiWidgets INTERFACE "$") + +target_link_libraries(KF5AkonadiWidgets +PUBLIC + KF5::AkonadiCore + KF5::ItemModels + Qt::Widgets + KF5::ConfigWidgets +PRIVATE + KF5::I18n + KF5::IconThemes + KF5::XmlGui + KF5::ItemViews + Qt::Sql + KF5::AkonadiPrivate + KF5::WindowSystem +) + +set_target_properties(KF5AkonadiWidgets PROPERTIES + VERSION ${AKONADI_VERSION} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiWidgets +) + +ecm_generate_pri_file(BASE_NAME AkonadiWidgets + LIB_NAME KF5AkonadiWidgets + DEPS "AkonadiCore KItemModels" FILENAME_VAR PRI_FILENAME +) + +install(TARGETS + KF5AkonadiWidgets + EXPORT KF5AkonadiTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadiwidgets_export.h + ${AkonadiWidgets_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiWidgets COMPONENT Devel +) + +install(FILES + ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR} +) + + +######### Build and install QtDesigner plugin ############# + +if(BUILD_DESIGNERPLUGIN) + include(ECMAddQtDesignerPlugin) + ecm_qtdesignerplugin_widget(Akonadi::AgentInstanceWidget + INCLUDE_FILE "agentinstancewidget.h" + TOOLTIP "Akonadi Agent Instance Settings (Akonadi)" + WHATSTHIS "A widget to configure akonadi instance." + GROUP "Input (KDE-PIM)" + ) + ecm_qtdesignerplugin_widget(Akonadi::EntityTreeView + INCLUDE_FILE "entitytreeview.h" + TOOLTIP "A view to show an item/collection tree provided by an EntityTreeModel (Akonadi)" + WHATSTHIS "A view to show an item/collection tree provided by an EntityTreeModel." + GROUP "Input (KDE-PIM)" + ) + ecm_qtdesignerplugin_widget(Akonadi::TagSelectWidget + INCLUDE_FILE "tagselectwidget.h" + TOOLTIP "A Widget to select tag (Akonadi)" + WHATSTHIS "A Widget to select tag." + GROUP "Input (KDE-PIM)" + ) + ecm_qtdesignerplugin_widget(Akonadi::ManageAccountWidget + INCLUDE_FILE "manageaccountwidget.h" + TOOLTIP "A Widget to Manage Akonadi Account (KDE-PIM)" + WHATSTHIS "A Widget to Manage Akonadi Account." + GROUP "Input (KDE-PIM)" + ) + + ecm_add_qtdesignerplugin(akonadiwidgets + NAME AkonadiWidgets + OUTPUT_NAME akonadiwidgets + WIDGETS + Akonadi::AgentInstanceWidget + Akonadi::EntityTreeView + Akonadi::TagSelectWidget + Akonadi::ManageAccountWidget + LINK_LIBRARIES + KF5::AkonadiCore + KF5::AkonadiWidgets + INSTALL_DESTINATION "${KDE_INSTALL_QTPLUGINDIR}/designer" + COMPONENT Devel + ) +endif() diff --git a/src/widgets/actionstatemanager.cpp b/src/widgets/actionstatemanager.cpp new file mode 100644 index 0000000..46c38b7 --- /dev/null +++ b/src/widgets/actionstatemanager.cpp @@ -0,0 +1,401 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "actionstatemanager_p.h" + +#include "agentmanager.h" +#include "collectionutils.h" +#include "entitydeletedattribute.h" +#include "pastehelper_p.h" +#include "specialcollectionattribute.h" +#include "standardactionmanager.h" + +#include +#include + +using namespace Akonadi; + +static bool canCreateSubCollection(const Collection &collection) +{ + if (!(collection.rights() & Collection::CanCreateCollection)) { + return false; + } + + if (!collection.contentMimeTypes().contains(Collection::mimeType()) && !collection.contentMimeTypes().contains(Collection::virtualMimeType())) { + return false; + } + + return true; +} + +static inline bool canContainItems(const Collection &collection) +{ + if (collection.contentMimeTypes().isEmpty()) { + return false; + } + + if ((collection.contentMimeTypes().count() == 1) + && ((collection.contentMimeTypes().at(0) == Collection::mimeType()) || (collection.contentMimeTypes().at(0) == Collection::virtualMimeType()))) { + return false; + } + + return true; +} + +void ActionStateManager::setReceiver(QObject *object) +{ + mReceiver = object; +} + +void ActionStateManager::updateState(const Collection::List &collections, const Collection::List &favoriteCollections, const Item::List &items) +{ + const int collectionCount = collections.count(); + const bool singleCollectionSelected = (collectionCount == 1); + const bool multipleCollectionsSelected = (collectionCount > 1); + const bool atLeastOneCollectionSelected = (singleCollectionSelected || multipleCollectionsSelected); + + const int itemCount = items.count(); + const bool singleItemSelected = (itemCount == 1); + const bool multipleItemsSelected = (itemCount > 1); + const bool atLeastOneItemSelected = (singleItemSelected || multipleItemsSelected); + + const bool listOfCollectionNotEmpty = !collections.isEmpty(); + bool canDeleteCollections = listOfCollectionNotEmpty; + if (canDeleteCollections) { + for (const Collection &collection : collections) { + // do we have the necessary rights? + if (!(collection.rights() & Collection::CanDeleteCollection)) { + canDeleteCollections = false; + break; + } + + if (isRootCollection(collection)) { + canDeleteCollections = false; + break; + } + + if (isResourceCollection(collection)) { + canDeleteCollections = false; + break; + } + } + } + + bool canCutCollections = canDeleteCollections; // we must be able to delete for cutting + for (const Collection &collection : collections) { + if (isSpecialCollection(collection)) { + canCutCollections = false; + break; + } + + if (!isFolderCollection(collection)) { + canCutCollections = false; + break; + } + } + + const bool canMoveCollections = canCutCollections; // we must be able to cut for moving + + bool canCopyCollections = listOfCollectionNotEmpty; + if (canCopyCollections) { + for (const Collection &collection : collections) { + if (isRootCollection(collection)) { + canCopyCollections = false; + break; + } + + if (!isFolderCollection(collection)) { + canCopyCollections = false; + break; + } + } + } + bool canAddToFavoriteCollections = listOfCollectionNotEmpty; + if (canAddToFavoriteCollections) { + for (const Collection &collection : collections) { + if (isRootCollection(collection)) { + canAddToFavoriteCollections = false; + break; + } + + if (isFavoriteCollection(collection)) { + canAddToFavoriteCollections = false; + break; + } + + if (!isFolderCollection(collection)) { + canAddToFavoriteCollections = false; + break; + } + + if (!canContainItems(collection)) { + canAddToFavoriteCollections = false; + break; + } + } + } + + bool collectionsAreFolders = listOfCollectionNotEmpty; + + for (const Collection &collection : collections) { + if (!isFolderCollection(collection)) { + collectionsAreFolders = false; + break; + } + } + + bool collectionsAreInTrash = false; + for (const Collection &collection : collections) { + if (collection.hasAttribute()) { + collectionsAreInTrash = true; + break; + } + } + + bool atLeastOneCollectionCanHaveItems = false; + for (const Collection &collection : collections) { + if (collectionCanHaveItems(collection)) { + atLeastOneCollectionCanHaveItems = true; + break; + } + } + for (const Collection &collection : favoriteCollections) { + if (collectionCanHaveItems(collection)) { + atLeastOneCollectionCanHaveItems = true; + break; + } + } + + const Collection collection = (!collections.isEmpty() ? collections.first() : Collection()); + + // collection specific actions + enableAction(StandardActionManager::CreateCollection, + singleCollectionSelected && // we can create only inside one collection + canCreateSubCollection(collection)); // we need the necessary rights + + enableAction(StandardActionManager::DeleteCollections, canDeleteCollections); + + enableAction(StandardActionManager::CopyCollections, canCopyCollections); + + enableAction(StandardActionManager::CutCollections, canCutCollections); + + enableAction(StandardActionManager::CopyCollectionToMenu, canCopyCollections); + + enableAction(StandardActionManager::MoveCollectionToMenu, canMoveCollections); + + enableAction(StandardActionManager::MoveCollectionsToTrash, atLeastOneCollectionSelected && canMoveCollections && !collectionsAreInTrash); + + enableAction(StandardActionManager::RestoreCollectionsFromTrash, atLeastOneCollectionSelected && canMoveCollections && collectionsAreInTrash); + + enableAction(StandardActionManager::CopyCollectionToDialog, canCopyCollections); + + enableAction(StandardActionManager::MoveCollectionToDialog, canMoveCollections); + + enableAction(StandardActionManager::CollectionProperties, + singleCollectionSelected && // we can only configure one collection at a time + !isRootCollection(collection)); // we can not configure the root collection + + enableAction(StandardActionManager::SynchronizeCollections, atLeastOneCollectionCanHaveItems); // it must be a valid folder collection + + enableAction(StandardActionManager::SynchronizeCollectionsRecursive, + atLeastOneCollectionSelected && collectionsAreFolders); // it must be a valid folder collection +#ifndef QT_NO_CLIPBOARD + enableAction(StandardActionManager::Paste, + singleCollectionSelected && // we can paste only into a single collection + PasteHelper::canPaste(QApplication::clipboard()->mimeData(), collection, Qt::CopyAction)); // there must be data on the clipboard +#else + enableAction(StandardActionManager::Paste, false); // no support for clipboard -> no paste +#endif + + // favorite collections specific actions + enableAction(StandardActionManager::AddToFavoriteCollections, canAddToFavoriteCollections); + + const bool canRemoveFromFavoriteCollections = !favoriteCollections.isEmpty(); + enableAction(StandardActionManager::RemoveFromFavoriteCollections, canRemoveFromFavoriteCollections); + + enableAction(StandardActionManager::RenameFavoriteCollection, favoriteCollections.count() == 1); // we can rename only one collection at a time + + // resource specific actions + int resourceCollectionCount = 0; + bool canDeleteResources = true; + bool canConfigureResource = true; + bool canSynchronizeResources = true; + for (const Collection &collection : collections) { + if (isResourceCollection(collection)) { + resourceCollectionCount++; + + // check that the 'NoConfig' flag is not set for the resource + if (hasResourceCapability(collection, QStringLiteral("NoConfig"))) { + canConfigureResource = false; + } + } else { + // we selected a non-resource collection + canDeleteResources = false; + canConfigureResource = false; + canSynchronizeResources = false; + } + } + + if (resourceCollectionCount == 0) { + // not a single resource collection has been selected + canDeleteResources = false; + canConfigureResource = false; + canSynchronizeResources = false; + } + + enableAction(StandardActionManager::CreateResource, true); + enableAction(StandardActionManager::DeleteResources, canDeleteResources); + enableAction(StandardActionManager::ResourceProperties, canConfigureResource); + enableAction(StandardActionManager::SynchronizeResources, canSynchronizeResources); + enableAction(StandardActionManager::SynchronizeCollectionTree, canSynchronizeResources); + + if (collectionsAreInTrash) { + updateAlternatingAction(StandardActionManager::MoveToTrashRestoreCollectionAlternative); + // updatePluralLabel( StandardActionManager::MoveToTrashRestoreCollectionAlternative, collectionCount ); + } else { + updateAlternatingAction(StandardActionManager::MoveToTrashRestoreCollection); + } + enableAction(StandardActionManager::MoveToTrashRestoreCollection, atLeastOneCollectionSelected && canMoveCollections); + + // item specific actions + bool canDeleteItems = (!items.isEmpty()); // TODO: fixme + for (const Item &item : std::as_const(items)) { + const Collection parentCollection = item.parentCollection(); + if (!parentCollection.isValid()) { + continue; + } + + canDeleteItems = canDeleteItems && (parentCollection.rights() & Collection::CanDeleteItem); + } + + bool itemsAreInTrash = false; + for (const Item &item : std::as_const(items)) { + if (item.hasAttribute()) { + itemsAreInTrash = true; + break; + } + } + + enableAction(StandardActionManager::CopyItems, atLeastOneItemSelected); // we need items to work with + + enableAction(StandardActionManager::CutItems, + atLeastOneItemSelected && // we need items to work with + canDeleteItems); // we need the necessary rights + + enableAction(StandardActionManager::DeleteItems, + atLeastOneItemSelected && // we need items to work with + canDeleteItems); // we need the necessary rights + + enableAction(StandardActionManager::CopyItemToMenu, atLeastOneItemSelected); // we need items to work with + + enableAction(StandardActionManager::MoveItemToMenu, + atLeastOneItemSelected && // we need items to work with + canDeleteItems); // we need the necessary rights + + enableAction(StandardActionManager::MoveItemsToTrash, atLeastOneItemSelected && canDeleteItems && !itemsAreInTrash); + + enableAction(StandardActionManager::RestoreItemsFromTrash, atLeastOneItemSelected && itemsAreInTrash); + + enableAction(StandardActionManager::CopyItemToDialog, atLeastOneItemSelected); // we need items to work with + + enableAction(StandardActionManager::MoveItemToDialog, + atLeastOneItemSelected && // we need items to work with + canDeleteItems); // we need the necessary rights + + if (itemsAreInTrash) { + updateAlternatingAction(StandardActionManager::MoveToTrashRestoreItemAlternative); + // updatePluralLabel( StandardActionManager::MoveToTrashRestoreItemAlternative, itemCount ); + } else { + updateAlternatingAction(StandardActionManager::MoveToTrashRestoreItem); + } + enableAction(StandardActionManager::MoveToTrashRestoreItem, + atLeastOneItemSelected && // we need items to work with + canDeleteItems); // we need the necessary rights + + // update the texts of the actions + updatePluralLabel(StandardActionManager::CopyCollections, collectionCount); + updatePluralLabel(StandardActionManager::CopyItems, itemCount); + updatePluralLabel(StandardActionManager::DeleteItems, itemCount); + updatePluralLabel(StandardActionManager::CutItems, itemCount); + updatePluralLabel(StandardActionManager::CutCollections, collectionCount); + updatePluralLabel(StandardActionManager::DeleteCollections, collectionCount); + updatePluralLabel(StandardActionManager::SynchronizeCollections, collectionCount); + updatePluralLabel(StandardActionManager::SynchronizeCollectionsRecursive, collectionCount); + updatePluralLabel(StandardActionManager::DeleteResources, resourceCollectionCount); + updatePluralLabel(StandardActionManager::SynchronizeResources, resourceCollectionCount); + updatePluralLabel(StandardActionManager::SynchronizeCollectionTree, resourceCollectionCount); +} + +bool ActionStateManager::isRootCollection(const Collection &collection) const +{ + return CollectionUtils::isRoot(collection); +} + +bool ActionStateManager::isResourceCollection(const Collection &collection) const +{ + return CollectionUtils::isResource(collection); +} + +bool ActionStateManager::isFolderCollection(const Collection &collection) const +{ + return (CollectionUtils::isFolder(collection) || CollectionUtils::isResource(collection) || CollectionUtils::isStructural(collection)); +} + +bool ActionStateManager::isSpecialCollection(const Collection &collection) const +{ + return collection.hasAttribute(); +} + +bool ActionStateManager::isFavoriteCollection(const Collection &collection) const +{ + if (!mReceiver) { + return false; + } + + bool result = false; + QMetaObject::invokeMethod(mReceiver, "isFavoriteCollection", Qt::DirectConnection, Q_RETURN_ARG(bool, result), Q_ARG(Akonadi::Collection, collection)); + + return result; +} + +bool ActionStateManager::hasResourceCapability(const Collection &collection, const QString &capability) const +{ + const Akonadi::AgentInstance instance = AgentManager::self()->instance(collection.resource()); + + return instance.type().capabilities().contains(capability); +} + +bool ActionStateManager::collectionCanHaveItems(const Collection &collection) const +{ + return !(collection.contentMimeTypes() == (QStringList() << QStringLiteral("inode/directory")) || CollectionUtils::isStructural(collection)); +} + +void ActionStateManager::enableAction(int action, bool state) +{ + if (!mReceiver) { + return; + } + + QMetaObject::invokeMethod(mReceiver, "enableAction", Qt::DirectConnection, Q_ARG(int, action), Q_ARG(bool, state)); +} + +void ActionStateManager::updatePluralLabel(int action, int count) +{ + if (!mReceiver) { + return; + } + + QMetaObject::invokeMethod(mReceiver, "updatePluralLabel", Qt::DirectConnection, Q_ARG(int, action), Q_ARG(int, count)); +} + +void ActionStateManager::updateAlternatingAction(int action) +{ + if (!mReceiver) { + return; + } + + QMetaObject::invokeMethod(mReceiver, "updateAlternatingAction", Qt::DirectConnection, Q_ARG(int, action)); +} diff --git a/src/widgets/actionstatemanager_p.h b/src/widgets/actionstatemanager_p.h new file mode 100644 index 0000000..ca7e106 --- /dev/null +++ b/src/widgets/actionstatemanager_p.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collection.h" +#include "item.h" + +class QObject; + +namespace Akonadi +{ +/** + * @short A helper class to manage action states. + * + * @author Tobias Koenig + */ +class ActionStateManager +{ +public: + /* + * Creates a new action state manager. + */ + explicit ActionStateManager() = default; + + virtual ~ActionStateManager() = default; + + /** + * Updates the states according to the selected collections and items. + * @param collections selected collections (from the folder tree) + * @param favoriteCollections selected collections (among the ones marked as favorites) + * @param items selected items + */ + void updateState(const Collection::List &collections, const Collection::List &favoriteCollections, const Item::List &items); + + /** + * Sets the @p receiver object that will actually update the states. + * + * The object must provide the following three slots: + * - void enableAction( int, bool ) + * - void updatePluralLabel( int, int ) + * - bool isFavoriteCollection( const Akonadi::Collection& ) + * @param receiver object that will actually update the states. + */ + void setReceiver(QObject *receiver); + +protected: + virtual bool isRootCollection(const Collection &collection) const; + virtual bool isResourceCollection(const Collection &collection) const; + virtual bool isFolderCollection(const Collection &collection) const; + virtual bool isSpecialCollection(const Collection &collection) const; + virtual bool isFavoriteCollection(const Collection &collection) const; + virtual bool hasResourceCapability(const Collection &collection, const QString &capability) const; + virtual bool collectionCanHaveItems(const Collection &collection) const; + + virtual void enableAction(int action, bool state); + virtual void updatePluralLabel(int action, int count); + virtual void updateAlternatingAction(int action); + +private: + Q_DISABLE_COPY_MOVE(ActionStateManager) + + QObject *mReceiver = nullptr; +}; + +} + diff --git a/src/widgets/agentactionmanager.cpp b/src/widgets/agentactionmanager.cpp new file mode 100644 index 0000000..9db6cf0 --- /dev/null +++ b/src/widgets/agentactionmanager.cpp @@ -0,0 +1,319 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentactionmanager.h" + +#include "agentfilterproxymodel.h" +#include "agentinstancecreatejob.h" +#include "agentinstancemodel.h" +#include "agentmanager.h" +#include "agenttypedialog.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +/// @cond PRIVATE + +static const struct { + const char *name; + const char *label; + const char *icon; + int shortcut; + const char *slot; +} agentActionData[] = {{"akonadi_agentinstance_create", I18N_NOOP("&New Agent Instance..."), "folder-new", 0, SLOT(slotCreateAgentInstance())}, + {"akonadi_agentinstance_delete", I18N_NOOP("&Delete Agent Instance"), "edit-delete", 0, SLOT(slotDeleteAgentInstance())}, + {"akonadi_agentinstance_configure", I18N_NOOP("&Configure Agent Instance"), "configure", 0, SLOT(slotConfigureAgentInstance())}}; +static const int numAgentActionData = sizeof agentActionData / sizeof *agentActionData; + +static_assert(numAgentActionData == AgentActionManager::LastType, "agentActionData table does not match AgentActionManager types"); + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentActionManager::Private +{ +public: + explicit Private(AgentActionManager *parent) + : q(parent) + { + mActions.fill(nullptr, AgentActionManager::LastType); + + setContextText(AgentActionManager::CreateAgentInstance, AgentActionManager::DialogTitle, i18nc("@title:window", "New Agent Instance")); + + setContextText(AgentActionManager::CreateAgentInstance, AgentActionManager::ErrorMessageText, ki18n("Could not create agent instance: %1")); + + setContextText(AgentActionManager::CreateAgentInstance, AgentActionManager::ErrorMessageTitle, i18n("Agent instance creation failed")); + + setContextText(AgentActionManager::DeleteAgentInstance, AgentActionManager::MessageBoxTitle, i18nc("@title:window", "Delete Agent Instance?")); + + setContextText(AgentActionManager::DeleteAgentInstance, + AgentActionManager::MessageBoxText, + i18n("Do you really want to delete the selected agent instance?")); + } + + void enableAction(AgentActionManager::Type type, bool enable) + { + Q_ASSERT(type >= 0 && type < AgentActionManager::LastType); + if (QAction *act = mActions[type]) { + act->setEnabled(enable); + } + } + + void updateActions() + { + const AgentInstance::List instances = selectedAgentInstances(); + + const bool createActionEnabled = true; + bool deleteActionEnabled = true; + bool configureActionEnabled = true; + + if (instances.isEmpty()) { + deleteActionEnabled = false; + configureActionEnabled = false; + } + + if (instances.count() == 1) { + const AgentInstance &instance = instances.first(); + if (instance.type().capabilities().contains(QLatin1String("NoConfig"))) { + configureActionEnabled = false; + } + } + + enableAction(CreateAgentInstance, createActionEnabled); + enableAction(DeleteAgentInstance, deleteActionEnabled); + enableAction(ConfigureAgentInstance, configureActionEnabled); + + Q_EMIT q->actionStateUpdated(); + } + + AgentInstance::List selectedAgentInstances() const + { + AgentInstance::List instances; + + if (!mSelectionModel) { + return instances; + } + + const QModelIndexList lstModelIndex = mSelectionModel->selectedRows(); + for (const QModelIndex &index : lstModelIndex) { + const auto instance = index.data(AgentInstanceModel::InstanceRole).value(); + if (instance.isValid()) { + instances << instance; + } + } + + return instances; + } + + void slotCreateAgentInstance() + { + QPointer dlg(new Akonadi::AgentTypeDialog(mParentWidget)); + dlg->setWindowTitle(contextText(AgentActionManager::CreateAgentInstance, AgentActionManager::DialogTitle)); + + for (const QString &mimeType : std::as_const(mMimeTypeFilter)) { + dlg->agentFilterProxyModel()->addMimeTypeFilter(mimeType); + } + + for (const QString &capability : std::as_const(mCapabilityFilter)) { + dlg->agentFilterProxyModel()->addCapabilityFilter(capability); + } + + if (dlg->exec() == QDialog::Accepted) { + const AgentType agentType = dlg->agentType(); + + if (agentType.isValid()) { + auto job = new AgentInstanceCreateJob(agentType, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + slotAgentInstanceCreationResult(job); + }); + job->configure(mParentWidget); + job->start(); + } + } + delete dlg; + } + + void slotDeleteAgentInstance() + { + const AgentInstance::List instances = selectedAgentInstances(); + if (!instances.isEmpty()) { + if (KMessageBox::questionYesNo(mParentWidget, + contextText(AgentActionManager::DeleteAgentInstance, AgentActionManager::MessageBoxText), + contextText(AgentActionManager::DeleteAgentInstance, AgentActionManager::MessageBoxTitle), + KStandardGuiItem::del(), + KStandardGuiItem::cancel(), + QString(), + KMessageBox::Dangerous) + == KMessageBox::Yes) { + for (const AgentInstance &instance : instances) { + AgentManager::self()->removeInstance(instance); + } + } + } + } + + void slotConfigureAgentInstance() + { + AgentInstance::List instances = selectedAgentInstances(); + if (instances.isEmpty()) { + return; + } + + instances.first().configure(mParentWidget); + } + + void slotAgentInstanceCreationResult(KJob *job) + { + if (job->error()) { + KMessageBox::error(mParentWidget, + contextText(AgentActionManager::CreateAgentInstance, AgentActionManager::ErrorMessageText).arg(job->errorString()), + contextText(AgentActionManager::CreateAgentInstance, AgentActionManager::ErrorMessageTitle)); + } + } + + void setContextText(AgentActionManager::Type type, AgentActionManager::TextContext context, const QString &data) + { + mContextTexts[type].insert(context, data); + } + + void setContextText(AgentActionManager::Type type, AgentActionManager::TextContext context, const KLocalizedString &data) + { + mContextTexts[type].insert(context, data.toString()); + } + + QString contextText(AgentActionManager::Type type, AgentActionManager::TextContext context) const + { + return mContextTexts[type].value(context); + } + + AgentActionManager *const q; + KActionCollection *mActionCollection = nullptr; + QWidget *mParentWidget = nullptr; + QItemSelectionModel *mSelectionModel = nullptr; + QVector mActions; + QStringList mMimeTypeFilter; + QStringList mCapabilityFilter; + + using ContextTexts = QHash; + QHash mContextTexts; +}; + +/// @endcond + +AgentActionManager::AgentActionManager(KActionCollection *actionCollection, QWidget *parent) + : QObject(parent) + , d(new Private(this)) +{ + d->mParentWidget = parent; + d->mActionCollection = actionCollection; +} + +AgentActionManager::~AgentActionManager() +{ + delete d; +} + +void AgentActionManager::setSelectionModel(QItemSelectionModel *selectionModel) +{ + d->mSelectionModel = selectionModel; + connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() { + d->updateActions(); + }); +} + +void AgentActionManager::setMimeTypeFilter(const QStringList &mimeTypes) +{ + d->mMimeTypeFilter = mimeTypes; +} + +void AgentActionManager::setCapabilityFilter(const QStringList &capabilities) +{ + d->mCapabilityFilter = capabilities; +} + +QAction *AgentActionManager::createAction(Type type) +{ + Q_ASSERT(type >= 0 && type < LastType); + Q_ASSERT(agentActionData[type].name); + if (QAction *act = d->mActions[type]) { + return act; + } + + auto action = new QAction(d->mParentWidget); + action->setText(i18n(agentActionData[type].label)); + + if (agentActionData[type].icon) { + action->setIcon(QIcon::fromTheme(QString::fromLatin1(agentActionData[type].icon))); + } + + action->setShortcut(agentActionData[type].shortcut); + + if (agentActionData[type].slot) { + connect(action, SIGNAL(triggered()), agentActionData[type].slot); + } + + d->mActionCollection->addAction(QString::fromLatin1(agentActionData[type].name), action); + d->mActions[type] = action; + d->updateActions(); + + return action; +} + +void AgentActionManager::createAllActions() +{ + for (int type = 0; type < LastType; ++type) { + auto action = createAction(static_cast(type)); + Q_UNUSED(action) + } +} + +QAction *AgentActionManager::action(Type type) const +{ + Q_ASSERT(type >= 0 && type < LastType); + return d->mActions[type]; +} + +void AgentActionManager::interceptAction(Type type, bool intercept) +{ + Q_ASSERT(type >= 0 && type < LastType); + + const QAction *action = d->mActions[type]; + + if (!action) { + return; + } + + if (intercept) { + disconnect(action, SIGNAL(triggered()), this, agentActionData[type].slot); + } else { + connect(action, SIGNAL(triggered()), agentActionData[type].slot); + } +} + +AgentInstance::List AgentActionManager::selectedAgentInstances() const +{ + return d->selectedAgentInstances(); +} + +void AgentActionManager::setContextText(Type type, TextContext context, const QString &text) +{ + d->setContextText(type, context, text); +} + +void AgentActionManager::setContextText(Type type, TextContext context, const KLocalizedString &text) +{ + d->setContextText(type, context, text); +} + +#include "moc_agentactionmanager.cpp" diff --git a/src/widgets/agentactionmanager.h b/src/widgets/agentactionmanager.h new file mode 100644 index 0000000..196386e --- /dev/null +++ b/src/widgets/agentactionmanager.h @@ -0,0 +1,168 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentinstance.h" +#include "akonadiwidgets_export.h" + +#include + +class QAction; +class KActionCollection; +class KLocalizedString; +class QItemSelectionModel; +class QWidget; + +namespace Akonadi +{ +/** + * @short Manages generic actions for agent and agent instance views. + * + * @author Tobias Koenig + * @since 4.6 + */ +class AKONADIWIDGETS_EXPORT AgentActionManager : public QObject +{ + Q_OBJECT +public: + /** + * Describes the supported actions. + */ + enum Type { + CreateAgentInstance, ///< Creates an agent instance + DeleteAgentInstance, ///< Deletes the selected agent instance + ConfigureAgentInstance, ///< Configures the selected agent instance + LastType ///< Marks last action + }; + + /** + * Describes the text context that can be customized. + */ + enum TextContext { + DialogTitle, ///< The window title of a dialog + DialogText, ///< The text of a dialog + MessageBoxTitle, ///< The window title of a message box + MessageBoxText, ///< The text of a message box + MessageBoxAlternativeText, ///< An alternative text of a message box + ErrorMessageTitle, ///< The window title of an error message + ErrorMessageText ///< The text of an error message + }; + + /** + * Creates a new agent action manager. + * + * @param actionCollection The action collection to operate on. + * @param parent The parent widget. + */ + explicit AgentActionManager(KActionCollection *actionCollection, QWidget *parent = nullptr); + + /** + * Destroys the agent action manager. + */ + ~AgentActionManager(); + + /** + * Sets the agent selection @p model based on which the actions should operate. + * If none is set, all actions will be disabled. + * @param model model based on which actions should operate + */ + void setSelectionModel(QItemSelectionModel *model); + + /** + * Sets the mime type filter that will be used when creating new agent instances. + */ + void setMimeTypeFilter(const QStringList &mimeTypes); + + /** + * Sets the capability filter that will be used when creating new agent instances. + */ + void setCapabilityFilter(const QStringList &capabilities); + + /** + * Creates the action of the given type and adds it to the action collection + * specified in the constructor if it does not exist yet. The action is + * connected to its default implementation provided by this class. + * @param type action type + */ + Q_REQUIRED_RESULT QAction *createAction(Type type); + + /** + * Convenience method to create all standard actions. + * @see createAction() + */ + void createAllActions(); + + /** + * Returns the action of the given type, 0 if it has not been created (yet). + */ + Q_REQUIRED_RESULT QAction *action(Type type) const; + + /** + * Sets whether the default implementation for the given action @p type + * shall be executed when the action is triggered. + * + * @param intercept If @c false, the default implementation will be executed, + * if @c true no action is taken. + * + * @since 4.6 + */ + void interceptAction(Type type, bool intercept = true); + + /** + * Returns the list of agent instances that are currently selected. + * The list is empty if no agent instance is currently selected. + * + * @since 4.6 + */ + Q_REQUIRED_RESULT Akonadi::AgentInstance::List selectedAgentInstances() const; + + /** + * Sets the @p text of the action @p type for the given @p context. + * + * @param type action type + * @param context context of the given action + * @param text text for the given action type + * + * @since 4.6 + */ + void setContextText(Type type, TextContext context, const QString &text); + + /** + * Sets the @p text of the action @p type for the given @p context. + * + * @since 4.8 + * @param type action type + * @param context context of the given action type + * @param text localized text for the given action type + */ + void setContextText(Type type, TextContext context, const KLocalizedString &text); + +Q_SIGNALS: + /** + * This signal is emitted whenever the action state has been updated. + * In case you have special needs for changing the state of some actions, + * connect to this signal and adjust the action state. + */ + void actionStateUpdated(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void updateActions()) + + Q_PRIVATE_SLOT(d, void slotCreateAgentInstance()) + Q_PRIVATE_SLOT(d, void slotDeleteAgentInstance()) + Q_PRIVATE_SLOT(d, void slotConfigureAgentInstance()) + + Q_PRIVATE_SLOT(d, void slotAgentInstanceCreationResult(KJob *)) + /// @endcond +}; + +} + diff --git a/src/widgets/agentconfigurationdialog.cpp b/src/widgets/agentconfigurationdialog.cpp new file mode 100644 index 0000000..3561902 --- /dev/null +++ b/src/widgets/agentconfigurationdialog.cpp @@ -0,0 +1,107 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentconfigurationdialog.h" +#include "agentconfigurationbase.h" +#include "agentconfigurationwidget.h" +#include "agentconfigurationwidget_p.h" +#include "core/agentmanager.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Akonadi +{ +class Q_DECL_HIDDEN AgentConfigurationDialog::Private +{ +public: + explicit Private(AgentConfigurationDialog *qq) + : q(qq) + { + } + void restoreDialogSize(); + AgentConfigurationDialog *const q; + QPushButton *okButton = nullptr; + QScopedPointer widget; +}; + +void AgentConfigurationDialog::Private::restoreDialogSize() +{ + if (widget) { + const QSize size = widget->restoreDialogSize(); + if (size.isValid()) { + q->resize(size); + } + } +} + +} // namespace Akonadi + +using namespace Akonadi; + +AgentConfigurationDialog::AgentConfigurationDialog(const AgentInstance &instance, QWidget *parent) + : QDialog(parent) + , d(new Private(this)) +{ + setWindowTitle(i18nc("%1 = agent name", "%1 Configuration", instance.name())); + setWindowIcon(instance.type().icon()); + + auto l = new QVBoxLayout(this); + + d->widget.reset(new AgentConfigurationWidget(instance, this)); + l->addWidget(d->widget.data()); + + auto btnBox = new QDialogButtonBox(d->widget->standardButtons(), this); + l->addWidget(btnBox); + connect(btnBox, &QDialogButtonBox::accepted, this, &AgentConfigurationDialog::accept); + connect(btnBox, &QDialogButtonBox::rejected, this, &AgentConfigurationDialog::reject); + if (QPushButton *applyButton = btnBox->button(QDialogButtonBox::Apply)) { + connect(applyButton, &QPushButton::clicked, d->widget.data(), &AgentConfigurationWidget::save); + } + if ((d->okButton = btnBox->button(QDialogButtonBox::Ok))) { + connect(d->widget.data(), &AgentConfigurationWidget::enableOkButton, d->okButton, &QPushButton::setEnabled); + } + + if (auto plugin = d->widget->d->plugin) { + if (auto aboutData = plugin->aboutData()) { + auto helpMenu = new KHelpMenu(this, *aboutData, true); + helpMenu->action(KHelpMenu::menuDonate); + // Initialize menu + QMenu *menu = helpMenu->menu(); + // HACK: the actions are populated from QGuiApplication so they would refer to the + // current application not to the agent, so we have to adjust the strings in some + // of the actions. + helpMenu->action(KHelpMenu::menuAboutApp)->setIcon(QIcon::fromTheme(aboutData->programIconName())); + helpMenu->action(KHelpMenu::menuHelpContents)->setText(i18n("%1 Handbook", aboutData->displayName())); + helpMenu->action(KHelpMenu::menuAboutApp)->setText(i18n("About %1", aboutData->displayName())); + btnBox->addButton(QDialogButtonBox::Help)->setMenu(menu); + } + } + d->restoreDialogSize(); +} + +AgentConfigurationDialog::~AgentConfigurationDialog() +{ + if (d->widget) { + d->widget->saveDialogSize(size()); + } +} + +void AgentConfigurationDialog::accept() +{ + if (d->widget) { + d->widget->save(); + } + + QDialog::accept(); +} diff --git a/src/widgets/agentconfigurationdialog.h b/src/widgets/agentconfigurationdialog.h new file mode 100644 index 0000000..ca2ee0d --- /dev/null +++ b/src/widgets/agentconfigurationdialog.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadiwidgets_export.h" + +namespace Akonadi +{ +class AgentInstance; +class AKONADIWIDGETS_EXPORT AgentConfigurationDialog : public QDialog +{ + Q_OBJECT +public: + explicit AgentConfigurationDialog(const AgentInstance &instance, QWidget *parent = nullptr); + ~AgentConfigurationDialog() override; + + void accept() override; + +private: + class Private; + const QScopedPointer d; +}; + +} + diff --git a/src/widgets/agentconfigurationwidget.cpp b/src/widgets/agentconfigurationwidget.cpp new file mode 100644 index 0000000..38df80b --- /dev/null +++ b/src/widgets/agentconfigurationwidget.cpp @@ -0,0 +1,166 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentconfigurationwidget.h" +#include "agentconfigurationdialog.h" +#include "agentconfigurationwidget_p.h" +#include "akonadiwidgets_debug.h" +#include "core/agentconfigurationbase.h" +#include "core/agentconfigurationfactorybase.h" +#include "core/agentconfigurationmanager_p.h" +#include "core/agentmanager.h" +#include "core/servermanager.h" + +#include +#include +#include +#include + +#include +#include + +#include + +using namespace Akonadi; + +AgentConfigurationWidget::Private::Private(const AgentInstance &instance) + : agentInstance(instance) +{ +} + +AgentConfigurationWidget::Private::~Private() +{ +} + +void AgentConfigurationWidget::Private::setupErrorWidget(QWidget *parent, const QString &text) +{ + auto layout = new QVBoxLayout(parent); + layout->addStretch(2); + auto label = new QLabel(text, parent); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + layout->addStretch(2); +} + +bool AgentConfigurationWidget::Private::loadPlugin(const QString &pluginPath) +{ + if (pluginPath.isEmpty()) { + qCDebug(AKONADIWIDGETS_LOG) << "Haven't found config plugin for" << agentInstance.type().identifier(); + return false; + } + loader = decltype(loader)(new QPluginLoader(pluginPath)); + if (!loader->load()) { + qCWarning(AKONADIWIDGETS_LOG) << "Failed to load config plugin" << pluginPath << ":" << loader->errorString(); + loader.reset(); + return false; + } + factory = qobject_cast(loader->instance()); + if (!factory) { + // will unload the QPluginLoader and thus delete the factory as well + qCWarning(AKONADIWIDGETS_LOG) << "Config plugin" << pluginPath << "does not contain AgentConfigurationFactory!"; + loader.reset(); + return false; + } + + qCDebug(AKONADIWIDGETS_LOG) << "Loaded agent configuration plugin" << pluginPath; + return true; +} + +AgentConfigurationWidget::AgentConfigurationWidget(const AgentInstance &instance, QWidget *parent) + : QWidget(parent) + , d(new Private(instance)) +{ + if (AgentConfigurationManager::self()->registerInstanceConfiguration(instance.identifier())) { + const auto pluginPath = AgentConfigurationManager::self()->findConfigPlugin(instance.type().identifier()); + if (d->loadPlugin(pluginPath)) { + QString configName = instance.identifier() + QStringLiteral("rc"); + configName = Akonadi::ServerManager::addNamespace(configName); + KSharedConfigPtr config = KSharedConfig::openConfig(configName); + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + d->plugin = d->factory->create(config, this, {instance.identifier()}); + connect(d->plugin.data(), &AgentConfigurationBase::enableOkButton, this, &AgentConfigurationWidget::enableOkButton); + } else { + // Hide this dialog and fallback to calling the out-of-process configuration + if (auto dlg = qobject_cast(parent)) { + const_cast(instance).configure(topLevelWidget()->parentWidget()); + // If we are inside the AgentConfigurationDialog, hide the dialog + QTimer::singleShot(0, this, [dlg]() { + dlg->reject(); + }); + } else { + const_cast(instance).configure(); + // Otherwise show a message that this is opened externally + d->setupErrorWidget(this, i18n("The configuration dialog has been opened in another window")); + } + + // TODO: Re-enable once we can kill the fallback code above ^^ + // d->setupErrorWidget(this, i18n("Failed to load configuration plugin")); + } + } else if (AgentConfigurationManager::self()->isInstanceRegistered(instance.identifier())) { + d->setupErrorWidget(this, i18n("Configuration for %1 is already opened elsewhere.", instance.name())); + } else { + d->setupErrorWidget(this, i18n("Failed to register %1 configuration dialog.", instance.name())); + } + + QTimer::singleShot(0, this, &AgentConfigurationWidget::load); +} + +AgentConfigurationWidget::~AgentConfigurationWidget() +{ + AgentConfigurationManager::self()->unregisterInstanceConfiguration(d->agentInstance.identifier()); +} + +void AgentConfigurationWidget::load() +{ + if (d->plugin) { + d->plugin->load(); + } +} + +void AgentConfigurationWidget::save() +{ + qCDebug(AKONADIWIDGETS_LOG) << "Saving configuration for" << d->agentInstance.identifier(); + if (d->plugin) { + if (d->plugin->save()) { + d->agentInstance.reconfigure(); + } + } +} + +QSize AgentConfigurationWidget::restoreDialogSize() const +{ + if (d->plugin) { + return d->plugin->restoreDialogSize(); + } + return {}; +} + +void AgentConfigurationWidget::saveDialogSize(QSize size) +{ + if (d->plugin) { + d->plugin->saveDialogSize(size); + } +} + +QDialogButtonBox::StandardButtons AgentConfigurationWidget::standardButtons() const +{ + if (d->plugin) { + return d->plugin->standardButtons(); + } + return QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel; +} + +void AgentConfigurationWidget::childEvent(QChildEvent *event) +{ + if (event->added()) { + if (event->child()->isWidgetType()) { + layout()->addWidget(static_cast(event->child())); + } + } + + QWidget::childEvent(event); +} diff --git a/src/widgets/agentconfigurationwidget.h b/src/widgets/agentconfigurationwidget.h new file mode 100644 index 0000000..7942fc6 --- /dev/null +++ b/src/widgets/agentconfigurationwidget.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include +#include + +namespace Akonadi +{ +class AgentInstance; +class AgentConfigurationDialog; +/** + * @brief A widget for displaying agent configuration in applications. + * + * To implement an agent configuration widget, see AgentConfigurationBase. + */ +class AKONADIWIDGETS_EXPORT AgentConfigurationWidget : public QWidget +{ + Q_OBJECT +public: + explicit AgentConfigurationWidget(const Akonadi::AgentInstance &instance, QWidget *parent = nullptr); + ~AgentConfigurationWidget() override; + + void load(); + void save(); + QSize restoreDialogSize() const; + void saveDialogSize(QSize size); + QDialogButtonBox::StandardButtons standardButtons() const; + +Q_SIGNALS: + void enableOkButton(bool enabled); + +protected: + void childEvent(QChildEvent *event) override; + +private: + class Private; + friend class Private; + friend class AgentConfigurationDialog; + QScopedPointer d; +}; + +} + diff --git a/src/widgets/agentconfigurationwidget_p.h b/src/widgets/agentconfigurationwidget_p.h new file mode 100644 index 0000000..eecfe4e --- /dev/null +++ b/src/widgets/agentconfigurationwidget_p.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2018 Daniel Vrátil + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "agentconfigurationfactorybase.h" +#include "agentconfigurationwidget.h" +#include "agentinstance.h" + +#include +#include + +#include + +namespace Akonadi +{ +class Q_DECL_HIDDEN AgentConfigurationWidget::Private +{ +private: + struct PluginLoaderDeleter { + inline void operator()(QPluginLoader *loader) + { + loader->unload(); + delete loader; + } + }; + +public: + Private(const AgentInstance &instance); + ~Private(); + + void setupErrorWidget(QWidget *parent, const QString &text); + bool loadPlugin(const QString &pluginPath); + + std::unique_ptr loader; + QPointer factory = nullptr; + QPointer plugin = nullptr; + QWidget *baseWidget = nullptr; + AgentInstance agentInstance; +}; + +} + diff --git a/src/widgets/agentinstancewidget.cpp b/src/widgets/agentinstancewidget.cpp new file mode 100644 index 0000000..b3cf83c --- /dev/null +++ b/src/widgets/agentinstancewidget.cpp @@ -0,0 +1,295 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstancewidget.h" + +#include "agentfilterproxymodel.h" +#include "agentinstance.h" +#include "agentinstancemodel.h" + +#include +#include +#include +#include +#include +#include + +namespace Akonadi +{ +namespace Internal +{ +static void iconsEarlyCleanup(); + +struct Icons { + Icons() + : readyPixmap(QIcon::fromTheme(QStringLiteral("user-online")).pixmap(QSize(16, 16))) + , syncPixmap(QIcon::fromTheme(QStringLiteral("network-connect")).pixmap(QSize(16, 16))) + , errorPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(QSize(16, 16))) + , offlinePixmap(QIcon::fromTheme(QStringLiteral("network-disconnect")).pixmap(QSize(16, 16))) + { + qAddPostRoutine(iconsEarlyCleanup); + } + QPixmap readyPixmap, syncPixmap, errorPixmap, offlinePixmap; +}; + +Q_GLOBAL_STATIC(Icons, s_icons) // NOLINT(readability-redundant-member-init) + +// called as a Qt post routine, to prevent pixmap leaking +void iconsEarlyCleanup() +{ + Icons *const ic = s_icons; + ic->readyPixmap = ic->syncPixmap = ic->errorPixmap = ic->offlinePixmap = QPixmap(); +} + +static const int s_delegatePaddingSize = 7; + +/** + * @internal + */ + +class AgentInstanceWidgetDelegate : public QAbstractItemDelegate +{ + Q_OBJECT +public: + explicit AgentInstanceWidgetDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +} // namespace Internal + +using Akonadi::Internal::AgentInstanceWidgetDelegate; + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentInstanceWidget::Private +{ +public: + explicit Private(AgentInstanceWidget *parent) + : mParent(parent) + { + } + + void currentAgentInstanceChanged(const QModelIndex ¤tIndex, const QModelIndex &previousIndex); + void currentAgentInstanceDoubleClicked(const QModelIndex ¤tIndex); + void currentAgentInstanceClicked(const QModelIndex ¤tIndex); + + AgentInstanceWidget *const mParent; + QListView *mView = nullptr; + AgentInstanceModel *mModel = nullptr; + AgentFilterProxyModel *proxy = nullptr; +}; + +void AgentInstanceWidget::Private::currentAgentInstanceChanged(const QModelIndex ¤tIndex, const QModelIndex &previousIndex) +{ + AgentInstance currentInstance; + if (currentIndex.isValid()) { + currentInstance = currentIndex.data(AgentInstanceModel::InstanceRole).value(); + } + + AgentInstance previousInstance; + if (previousIndex.isValid()) { + previousInstance = previousIndex.data(AgentInstanceModel::InstanceRole).value(); + } + + Q_EMIT mParent->currentChanged(currentInstance, previousInstance); +} + +void AgentInstanceWidget::Private::currentAgentInstanceDoubleClicked(const QModelIndex ¤tIndex) +{ + AgentInstance currentInstance; + if (currentIndex.isValid()) { + currentInstance = currentIndex.data(AgentInstanceModel::InstanceRole).value(); + } + + Q_EMIT mParent->doubleClicked(currentInstance); +} + +void AgentInstanceWidget::Private::currentAgentInstanceClicked(const QModelIndex ¤tIndex) +{ + AgentInstance currentInstance; + if (currentIndex.isValid()) { + currentInstance = currentIndex.data(AgentInstanceModel::InstanceRole).value(); + } + + Q_EMIT mParent->clicked(currentInstance); +} + +AgentInstanceWidget::AgentInstanceWidget(QWidget *parent) + : QWidget(parent) + , d(new Private(this)) +{ + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + d->mView = new QListView(this); + d->mView->setContextMenuPolicy(Qt::NoContextMenu); + d->mView->setItemDelegate(new Internal::AgentInstanceWidgetDelegate(d->mView)); + d->mView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + d->mView->setAlternatingRowColors(true); + d->mView->setSelectionMode(QAbstractItemView::ExtendedSelection); + layout->addWidget(d->mView); + + d->mModel = new AgentInstanceModel(this); + + d->proxy = new AgentFilterProxyModel(this); + d->proxy->setDynamicSortFilter(true); + d->proxy->sort(0); + d->proxy->setSortCaseSensitivity(Qt::CaseInsensitive); + d->proxy->setSourceModel(d->mModel); + d->mView->setModel(d->proxy); + + d->mView->selectionModel()->setCurrentIndex(d->mView->model()->index(0, 0), QItemSelectionModel::Select); + d->mView->scrollTo(d->mView->model()->index(0, 0)); + + connect(d->mView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const auto &tl, const auto &br) { + d->currentAgentInstanceChanged(tl, br); + }); + connect(d->mView, &QListView::doubleClicked, this, [this](const QModelIndex ¤tIndex) { + d->currentAgentInstanceDoubleClicked(currentIndex); + }); + connect(d->mView, &QListView::clicked, this, [this](const auto &mi) { + d->currentAgentInstanceClicked(mi); + }); +} + +AgentInstanceWidget::~AgentInstanceWidget() +{ + delete d; +} + +AgentInstance AgentInstanceWidget::currentAgentInstance() const +{ + QItemSelectionModel *selectionModel = d->mView->selectionModel(); + if (!selectionModel) { + return AgentInstance(); + } + + QModelIndex index = selectionModel->currentIndex(); + if (!index.isValid()) { + return AgentInstance(); + } + + return index.data(AgentInstanceModel::InstanceRole).value(); +} + +AgentInstance::List AgentInstanceWidget::selectedAgentInstances() const +{ + AgentInstance::List list; + QItemSelectionModel *selectionModel = d->mView->selectionModel(); + if (!selectionModel) { + return list; + } + + const QModelIndexList indexes = selectionModel->selection().indexes(); + list.reserve(indexes.count()); + for (const QModelIndex &index : indexes) { + list.append(index.data(AgentInstanceModel::InstanceRole).value()); + } + + return list; +} + +QAbstractItemView *AgentInstanceWidget::view() const +{ + return d->mView; +} + +AgentFilterProxyModel *AgentInstanceWidget::agentFilterProxyModel() const +{ + return d->proxy; +} + +AgentInstanceWidgetDelegate::AgentInstanceWidgetDelegate(QObject *parent) + : QAbstractItemDelegate(parent) +{ +} + +void AgentInstanceWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + + QStyle *style = QApplication::style(); + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr); + + auto icon = index.data(Qt::DecorationRole).value(); + const QString name = index.model()->data(index, Qt::DisplayRole).toString(); + int status = index.model()->data(index, AgentInstanceModel::StatusRole).toInt(); + uint progress = index.model()->data(index, AgentInstanceModel::ProgressRole).toUInt(); + QString statusMessage = index.model()->data(index, AgentInstanceModel::StatusMessageRole).toString(); + + QPixmap statusPixmap; + + if (!index.data(AgentInstanceModel::OnlineRole).toBool()) { + statusPixmap = s_icons->offlinePixmap; + } else if (status == AgentInstance::Idle) { + statusPixmap = s_icons->readyPixmap; + } else if (status == AgentInstance::Running) { + statusPixmap = s_icons->syncPixmap; + } else { + statusPixmap = s_icons->errorPixmap; + } + + if (status == 1) { + statusMessage.append(QStringLiteral(" (%1%)").arg(progress)); + } + + const QPixmap iconPixmap = icon.pixmap(style->pixelMetric(QStyle::PM_MessageBoxIconSize)); + QRect innerRect = option.rect.adjusted(s_delegatePaddingSize, + s_delegatePaddingSize, + -s_delegatePaddingSize, + -s_delegatePaddingSize); // add some padding round entire delegate + + const QSize decorationSize = iconPixmap.size(); + const QSize statusIconSize = statusPixmap.size(); //= KIconLoader::global()->currentSize(KIconLoader::Small); + + QFont nameFont = option.font; + nameFont.setBold(true); + + QFont statusTextFont = option.font; + const QRect decorationRect(innerRect.left(), innerRect.top(), decorationSize.width(), innerRect.height()); + const QRect nameTextRect(decorationRect.topRight() + QPoint(4, 0), innerRect.topRight() + QPoint(0, innerRect.height() / 2)); + const QRect statusTextRect(decorationRect.bottomRight() + QPoint(4, -innerRect.height() / 2), innerRect.bottomRight()); + + QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; + if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) { + cg = QPalette::Inactive; + } + + if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(cg, QPalette::HighlightedText)); + } else { + painter->setPen(option.palette.color(cg, QPalette::Text)); + } + + painter->drawPixmap(style->itemPixmapRect(decorationRect, Qt::AlignCenter, iconPixmap), iconPixmap); + + painter->setFont(nameFont); + painter->drawText(nameTextRect, Qt::AlignVCenter | Qt::AlignLeft, name); + + painter->setFont(statusTextFont); + painter->drawText(statusTextRect.adjusted(statusIconSize.width() + 4, 0, 0, 0), Qt::AlignVCenter | Qt::AlignLeft, statusMessage); + painter->drawPixmap(style->itemPixmapRect(statusTextRect, Qt::AlignVCenter | Qt::AlignLeft, statusPixmap), statusPixmap); +} + +QSize AgentInstanceWidgetDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_UNUSED(index) + + const int iconHeight = QApplication::style()->pixelMetric(QStyle::PM_MessageBoxIconSize) + (s_delegatePaddingSize * 2); // icon height + padding either side + const int textHeight = + option.fontMetrics.height() + qMax(option.fontMetrics.height(), 16) + (s_delegatePaddingSize * 2); // height of text + icon/text + padding either side + + return QSize(1, qMax(iconHeight, textHeight)); // any width,the view will give us the whole thing in list mode +} + +} // namespace Akonadi + +#include "agentinstancewidget.moc" diff --git a/src/widgets/agentinstancewidget.h b/src/widgets/agentinstancewidget.h new file mode 100644 index 0000000..a80ccb3 --- /dev/null +++ b/src/widgets/agentinstancewidget.h @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + SPDX-FileCopyrightText: 2012-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +class QAbstractItemView; +namespace Akonadi +{ +class AgentInstance; +class AgentFilterProxyModel; + +/** + * @short Provides a widget that lists all available agent instances. + * + * The widget is listening on the dbus for changes, so the + * widget is updated automatically as soon as new agent instances + * are added to or removed from the system. + * + * @code + * + * MyWidget::MyWidget( QWidget *parent ) + * : QWidget( parent ) + * { + * QVBoxLayout *layout = new QVBoxLayout( this ); + * + * mAgentInstanceWidget = new Akonadi::AgentInstanceWidget( this ); + * layout->addWidget( mAgentInstanceWidget ); + * + * connect( mAgentInstanceWidget, SIGNAL(doubleClicked(Akonadi::AgentInstance)), + * this, SLOT(slotInstanceSelected(Akonadi::AgentInstance)) ); + * } + * + * ... + * + * MyWidget::slotInstanceSelected( Akonadi::AgentInstance &instance ) + * { + * qCDebug(AKONADIWIDGETS_LOG) << "Selected instance" << instance.name(); + * } + * + * @endcode + * + * @author Tobias Koenig + */ +class AKONADIWIDGETS_EXPORT AgentInstanceWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * Creates a new agent instance widget. + * + * @param parent The parent widget. + */ + explicit AgentInstanceWidget(QWidget *parent = nullptr); + + /** + * Destroys the agent instance widget. + */ + ~AgentInstanceWidget(); + + /** + * Returns the current agent instance or an invalid agent instance + * if no agent instance is selected. + */ + Q_REQUIRED_RESULT AgentInstance currentAgentInstance() const; + + /** + * Returns the selected agent instances. + * @since 4.5 + */ + Q_REQUIRED_RESULT QVector selectedAgentInstances() const; + + /** + * Returns the agent filter proxy model, use this to filter by + * agent mimetype or capabilities. + */ + Q_REQUIRED_RESULT AgentFilterProxyModel *agentFilterProxyModel() const; + + /** + * Returns the view used in the widget. + * @since 4.5 + */ + Q_REQUIRED_RESULT QAbstractItemView *view() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever the current agent instance changes. + * + * @param current The current agent instance. + * @param previous The previous agent instance. + */ + void currentChanged(const Akonadi::AgentInstance ¤t, const Akonadi::AgentInstance &previous); + + /** + * This signal is emitted whenever there is a double click on an agent instance. + * + * @param current The current agent instance. + */ + void doubleClicked(const Akonadi::AgentInstance ¤t); + + /** + * This signal is emitted whenever there is a click on an agent instance. + * + * @param current The current agent instance. + * @since 4.9.1 + */ + void clicked(const Akonadi::AgentInstance ¤t); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/agenttypedialog.cpp b/src/widgets/agenttypedialog.cpp new file mode 100644 index 0000000..78cc91c --- /dev/null +++ b/src/widgets/agenttypedialog.cpp @@ -0,0 +1,114 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2008 Omat Holding B.V. + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "agenttypedialog.h" +#include "agentfilterproxymodel.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN AgentTypeDialog::Private +{ +public: + explicit Private(AgentTypeDialog *qq) + : q(qq) + { + } + void readConfig(); + void writeConfig() const; + void slotSearchAgentType(const QString &str); + AgentTypeWidget *Widget = nullptr; + AgentType agentType; + AgentTypeDialog *const q; +}; + +void AgentTypeDialog::Private::writeConfig() const +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "AgentTypeDialog"); + group.writeEntry("Size", q->size()); +} + +void AgentTypeDialog::Private::readConfig() +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "AgentTypeDialog"); + const QSize sizeDialog = group.readEntry("Size", QSize(460, 320)); + if (sizeDialog.isValid()) { + q->resize(sizeDialog); + } +} + +void AgentTypeDialog::Private::slotSearchAgentType(const QString &str) +{ + Widget->agentFilterProxyModel()->setFilterRegularExpression(str); +} + +AgentTypeDialog::AgentTypeDialog(QWidget *parent) + : QDialog(parent) + , d(new Private(this)) +{ + auto layout = new QVBoxLayout(this); + + d->Widget = new Akonadi::AgentTypeWidget(this); + connect(d->Widget, &AgentTypeWidget::activated, this, &AgentTypeDialog::accept); + + auto searchLine = new QLineEdit(this); + layout->addWidget(searchLine); + searchLine->setClearButtonEnabled(true); + connect(searchLine, &QLineEdit::textChanged, this, [this](const QString &str) { + d->slotSearchAgentType(str); + }); + + layout->addWidget(d->Widget); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &AgentTypeDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &AgentTypeDialog::reject); + QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); // NOLINT(bugprone-suspicious-enum-usage) + layout->addWidget(buttonBox); + d->readConfig(); + + searchLine->setFocus(); +} + +AgentTypeDialog::~AgentTypeDialog() +{ + d->writeConfig(); + delete d; +} + +void AgentTypeDialog::done(int result) +{ + if (result == Accepted) { + d->agentType = d->Widget->currentAgentType(); + } else { + d->agentType = AgentType(); + } + + QDialog::done(result); +} + +AgentType AgentTypeDialog::agentType() const +{ + return d->agentType; +} + +AgentFilterProxyModel *AgentTypeDialog::agentFilterProxyModel() const +{ + return d->Widget->agentFilterProxyModel(); +} diff --git a/src/widgets/agenttypedialog.h b/src/widgets/agenttypedialog.h new file mode 100644 index 0000000..beb8717 --- /dev/null +++ b/src/widgets/agenttypedialog.h @@ -0,0 +1,82 @@ +/* + SPDX-FileCopyrightText: 2006 Tobias Koenig + SPDX-FileCopyrightText: 2008 Omat Holding B.V. + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include "agenttype.h" +#include "agenttypewidget.h" + +#include + +namespace Akonadi +{ +/** + * @short A dialog to select an available agent type. + * + * This dialogs allows the user to select an agent type from the + * list of all available agent types. The list can be filtered + * by the proxy model returned by agentFilterProxyModel(). + * + * @code + * + * Akonadi::AgentTypeDialog dlg( this ); + * + * // only list agent types that provide contacts + * dlg.agentFilterProxyModel()->addMimeTypeFilter( "text/directory" ); + * + * if ( dlg.exec() ) { + * const AgentType agentType = dlg.agentType(); + * ... + * } + * + * @endcode + * + * @author Tom Albers + * @since 4.2 + */ +class AKONADIWIDGETS_EXPORT AgentTypeDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * Creates a new agent type dialog. + * + * @param parent The parent widget of the dialog. + */ + explicit AgentTypeDialog(QWidget *parent = nullptr); + + /** + * Destroys the agent type dialog. + */ + ~AgentTypeDialog() override; + + /** + * Returns the agent type that was selected by the user, + * or an empty agent type object if no agent type has been selected. + */ + Q_REQUIRED_RESULT AgentType agentType() const; + + /** + * Returns the agent filter proxy model that can be used + * to filter the agent types that shall be shown in the + * dialog. + */ + Q_REQUIRED_RESULT AgentFilterProxyModel *agentFilterProxyModel() const; + +public Q_SLOTS: + void done(int result) override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/agenttypewidget.cpp b/src/widgets/agenttypewidget.cpp new file mode 100644 index 0000000..5ab21dd --- /dev/null +++ b/src/widgets/agenttypewidget.cpp @@ -0,0 +1,261 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agenttypewidget.h" + +#include +#include +#include +#include + +#include "agentfilterproxymodel.h" +#include "agenttype.h" +#include "agenttypemodel.h" + +namespace Akonadi +{ +namespace Internal +{ +/** + * @internal + */ +class AgentTypeWidgetDelegate : public QAbstractItemDelegate +{ + Q_OBJECT +public: + explicit AgentTypeWidgetDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + void drawFocus(QPainter * /*painter*/, const QStyleOptionViewItem & /*option*/, QRect /*rect*/) const; +}; + +} // namespace Internal + +using Akonadi::Internal::AgentTypeWidgetDelegate; + +/** + * @internal + */ +class Q_DECL_HIDDEN AgentTypeWidget::Private +{ +public: + explicit Private(AgentTypeWidget *parent) + : mParent(parent) + { + } + + void currentAgentTypeChanged(const QModelIndex & /*currentIndex*/, const QModelIndex & /*previousIndex*/); + + void typeActivated(const QModelIndex &index) + { + if (index.flags() & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) { + Q_EMIT mParent->activated(); + } + } + + AgentTypeWidget *const mParent; + QListView *mView = nullptr; + AgentTypeModel *mModel = nullptr; + AgentFilterProxyModel *proxyModel = nullptr; +}; + +void AgentTypeWidget::Private::currentAgentTypeChanged(const QModelIndex ¤tIndex, const QModelIndex &previousIndex) +{ + AgentType currentType; + if (currentIndex.isValid()) { + currentType = currentIndex.data(AgentTypeModel::TypeRole).value(); + } + + AgentType previousType; + if (previousIndex.isValid()) { + previousType = previousIndex.data(AgentTypeModel::TypeRole).value(); + } + + Q_EMIT mParent->currentChanged(currentType, previousType); +} + +AgentTypeWidget::AgentTypeWidget(QWidget *parent) + : QWidget(parent) + , d(new Private(this)) +{ + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + d->mView = new QListView(this); + d->mView->setItemDelegate(new AgentTypeWidgetDelegate(d->mView)); + d->mView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + d->mView->setAlternatingRowColors(true); + layout->addWidget(d->mView); + + d->mModel = new AgentTypeModel(d->mView); + d->proxyModel = new AgentFilterProxyModel(this); + d->proxyModel->setSourceModel(d->mModel); + d->proxyModel->sort(0); + d->mView->setModel(d->proxyModel); + + d->mView->selectionModel()->setCurrentIndex(d->mView->model()->index(0, 0), QItemSelectionModel::Select); + d->mView->scrollTo(d->mView->model()->index(0, 0)); + connect(d->mView->selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex &start, const QModelIndex &end) { + d->currentAgentTypeChanged(start, end); + }); + connect(d->mView, QOverload::of(&QListView::activated), this, [this](const QModelIndex &index) { + d->typeActivated(index); + }); +} + +AgentTypeWidget::~AgentTypeWidget() +{ + delete d; +} + +AgentType AgentTypeWidget::currentAgentType() const +{ + QItemSelectionModel *selectionModel = d->mView->selectionModel(); + if (!selectionModel) { + return AgentType(); + } + + QModelIndex index = selectionModel->currentIndex(); + if (!index.isValid()) { + return AgentType(); + } + + return index.data(AgentTypeModel::TypeRole).value(); +} + +AgentFilterProxyModel *AgentTypeWidget::agentFilterProxyModel() const +{ + return d->proxyModel; +} + +/** + * AgentTypeWidgetDelegate + */ + +AgentTypeWidgetDelegate::AgentTypeWidgetDelegate(QObject *parent) + : QAbstractItemDelegate(parent) +{ +} + +void AgentTypeWidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + + painter->setRenderHint(QPainter::Antialiasing); + + const QString name = index.model()->data(index, Qt::DisplayRole).toString(); + const QString comment = index.model()->data(index, AgentTypeModel::DescriptionRole).toString(); + + const QVariant data = index.model()->data(index, Qt::DecorationRole); + + QPixmap pixmap; + if (data.isValid() && data.type() == QVariant::Icon) { + pixmap = qvariant_cast(data).pixmap(64, 64); + } + + const QFont oldFont = painter->font(); + QFont boldFont(oldFont); + boldFont.setBold(true); + painter->setFont(boldFont); + QFontMetrics fm = painter->fontMetrics(); + int hn = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, name).height(); + int wn = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, name).width(); + painter->setFont(oldFont); + + fm = painter->fontMetrics(); + int hc = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, comment).height(); + int wc = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, comment).width(); + int wp = pixmap.width(); + + QStyleOptionViewItem opt(option); + opt.showDecorationSelected = true; + QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter); + + QPen pen = painter->pen(); + QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; + if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) { + cg = QPalette::Inactive; + } + if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(cg, QPalette::HighlightedText)); + } else { + painter->setPen(option.palette.color(cg, QPalette::Text)); + } + + painter->setFont(option.font); + + painter->drawPixmap(option.rect.x() + 5, option.rect.y() + 5, pixmap); + + painter->setFont(boldFont); + if (!name.isEmpty()) { + painter->drawText(option.rect.x() + 5 + wp + 5, option.rect.y() + 7, wn, hn, Qt::AlignLeft, name); + } + painter->setFont(oldFont); + + if (!comment.isEmpty()) { + painter->drawText(option.rect.x() + 5 + wp + 5, option.rect.y() + 7 + hn, wc, hc, Qt::AlignLeft, comment); + } + + painter->setPen(pen); + + drawFocus(painter, option, option.rect); +} + +QSize AgentTypeWidgetDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + if (!index.isValid()) { + return QSize(0, 0); + } + + const QString name = index.model()->data(index, Qt::DisplayRole).toString(); + const QString comment = index.model()->data(index, AgentTypeModel::DescriptionRole).toString(); + + QFontMetrics fm = option.fontMetrics; + int hn = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, name).height(); + int wn = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, name).width(); + int hc = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, comment).height(); + int wc = fm.boundingRect(0, 0, 0, 0, Qt::AlignLeft, comment).width(); + + int width = 0; + int height = 0; + + if (!name.isEmpty()) { + height += hn; + width = qMax(width, wn); + } + + if (!comment.isEmpty()) { + height += hc; + width = qMax(width, wc); + } + + height = qMax(height, 64) + 10; + width += 64 + 15; + + return QSize(width, height); +} + +void AgentTypeWidgetDelegate::drawFocus(QPainter *painter, const QStyleOptionViewItem &option, QRect rect) const +{ + if (option.state & QStyle::State_HasFocus) { + QStyleOptionFocusRect o; + o.QStyleOption::operator=(option); + o.rect = rect; + o.state |= QStyle::State_KeyboardFocusChange; + QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; + o.backgroundColor = option.palette.color(cg, (option.state & QStyle::State_Selected) ? QPalette::Highlight : QPalette::Window); + QApplication::style()->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter); + } +} + +} // namespace Akonadi + +#include "agenttypewidget.moc" diff --git a/src/widgets/agenttypewidget.h b/src/widgets/agenttypewidget.h new file mode 100644 index 0000000..f710a74 --- /dev/null +++ b/src/widgets/agenttypewidget.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +namespace Akonadi +{ +class AgentFilterProxyModel; +class AgentType; + +/** + * @short Provides a widget that lists all available agent types. + * + * The widget is listening on the dbus for changes, so the + * widget is updated automatically as soon as new agent types + * are added to or removed from the system. + * + * @code + * + * Akonadi::AgentTypeWidget *widget = new Akonadi::AgentTypeWidget( this ); + * + * // only list agent types that provide contacts + * widget->agentFilterProxyModel()->addMimeTypeFilter( "text/directory" ); + * + * @endcode + * + * If you want a dialog, you can use the Akonadi::AgentTypeDialog. + * + * @author Tobias Koenig + */ +class AKONADIWIDGETS_EXPORT AgentTypeWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * Creates a new agent type widget. + * + * @param parent The parent widget. + */ + explicit AgentTypeWidget(QWidget *parent = nullptr); + + /** + * Destroys the agent type widget. + */ + ~AgentTypeWidget(); + + /** + * Returns the current agent type or an invalid agent type + * if no agent type is selected. + */ + Q_REQUIRED_RESULT AgentType currentAgentType() const; + + /** + * Returns the agent filter proxy model, use this to filter by + * agent mimetype or capabilities. + */ + Q_REQUIRED_RESULT AgentFilterProxyModel *agentFilterProxyModel() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever the current agent type changes. + * + * @param current The current agent type. + * @param previous The previous agent type. + */ + void currentChanged(const Akonadi::AgentType ¤t, const Akonadi::AgentType &previous); + + /** + * This signal is emitted whenever the user activates an agent. + * @since 4.2 + */ + void activated(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/akonadiwidgetstests_export.h.in b/src/widgets/akonadiwidgetstests_export.h.in new file mode 100644 index 0000000..1c173dd --- /dev/null +++ b/src/widgets/akonadiwidgetstests_export.h.in @@ -0,0 +1,2 @@ +#include "akonadiwidgets_export.h" +#define AKONADIWIDGET_TESTS_EXPORT @AKONADIWIDGET_TESTS_EXPORT@ diff --git a/src/widgets/cachepolicypage.cpp b/src/widgets/cachepolicypage.cpp new file mode 100644 index 0000000..67cad29 --- /dev/null +++ b/src/widgets/cachepolicypage.cpp @@ -0,0 +1,155 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "cachepolicypage.h" + +#include "ui_cachepolicypage.h" + +#include "cachepolicy.h" +#include "collection.h" +#include "collectionutils.h" + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CachePolicyPage::Private +{ +public: + Private() + : mUi(new Ui::CachePolicyPage) + { + } + + ~Private() + { + delete mUi; + } + + void slotIntervalValueChanged(int /*interval*/); + void slotCacheValueChanged(int /*interval*/); + void slotRetrievalOptionsGroupBoxDisabled(bool disable); + + Ui::CachePolicyPage *const mUi; +}; + +void CachePolicyPage::Private::slotIntervalValueChanged(int interval) +{ + mUi->checkInterval->setSuffix(QLatin1Char(' ') + i18np("minute", "minutes", interval)); +} + +void CachePolicyPage::Private::slotCacheValueChanged(int interval) +{ + mUi->localCacheTimeout->setSuffix(QLatin1Char(' ') + i18np("minute", "minutes", interval)); +} + +void CachePolicyPage::Private::slotRetrievalOptionsGroupBoxDisabled(bool disable) +{ + mUi->retrievalOptionsGroupBox->setDisabled(disable); + if (!disable) { + mUi->label->setEnabled(mUi->retrieveOnlyHeaders->isChecked()); + mUi->localCacheTimeout->setEnabled(mUi->retrieveOnlyHeaders->isChecked()); + } +} + +CachePolicyPage::CachePolicyPage(QWidget *parent, GuiMode mode) + : CollectionPropertiesPage(parent) + , d(new Private) +{ + setObjectName(QStringLiteral("Akonadi::CachePolicyPage")); + setPageTitle(i18n("Retrieval")); + + d->mUi->setupUi(this); + connect(d->mUi->checkInterval, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { + d->slotIntervalValueChanged(value); + }); + connect(d->mUi->localCacheTimeout, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { + d->slotCacheValueChanged(value); + }); + connect(d->mUi->inherit, &QCheckBox::toggled, this, [this](bool checked) { + d->slotRetrievalOptionsGroupBoxDisabled(checked); + }); + if (mode == AdvancedMode) { + d->mUi->stackedWidget->setCurrentWidget(d->mUi->rawPage); + } +} + +CachePolicyPage::~CachePolicyPage() +{ + delete d; +} + +bool Akonadi::CachePolicyPage::canHandle(const Collection &collection) const +{ + return !collection.isVirtual(); +} + +void CachePolicyPage::load(const Collection &collection) +{ + const CachePolicy policy = collection.cachePolicy(); + + int interval = policy.intervalCheckTime(); + if (interval == -1) { + interval = 0; + } + + int cache = policy.cacheTimeout(); + if (cache == -1) { + cache = 0; + } + + d->mUi->inherit->setChecked(policy.inheritFromParent()); + d->mUi->checkInterval->setValue(interval); + d->mUi->localCacheTimeout->setValue(cache); + d->mUi->syncOnDemand->setChecked(policy.syncOnDemand()); + d->mUi->localParts->setItems(policy.localParts()); + + const bool fetchBodies = policy.localParts().contains(QLatin1String("RFC822")); + d->mUi->retrieveFullMessages->setChecked(fetchBodies); + + // done explicitly to disable/enabled widgets + d->mUi->retrieveOnlyHeaders->setChecked(!fetchBodies); + d->mUi->label->setEnabled(!fetchBodies); + d->mUi->localCacheTimeout->setEnabled(!fetchBodies); +} + +void CachePolicyPage::save(Collection &collection) +{ + int interval = d->mUi->checkInterval->value(); + if (interval == 0) { + interval = -1; + } + + int cache = d->mUi->localCacheTimeout->value(); + if (cache == 0) { + cache = -1; + } + + CachePolicy policy = collection.cachePolicy(); + policy.setInheritFromParent(d->mUi->inherit->isChecked()); + policy.setIntervalCheckTime(interval); + policy.setCacheTimeout(cache); + policy.setSyncOnDemand(d->mUi->syncOnDemand->isChecked()); + + QStringList localParts = d->mUi->localParts->items(); + + // Unless we are in "raw" mode, add "bodies" to the list of message + // parts to keep around locally, if the user selected that, or remove + // it otherwise. In "raw" mode we simple use the values from the list + // view. + if (d->mUi->stackedWidget->currentWidget() != d->mUi->rawPage) { + if (d->mUi->retrieveFullMessages->isChecked() && !localParts.contains(QLatin1String("RFC822"))) { + localParts.append(QStringLiteral("RFC822")); + } else if (!d->mUi->retrieveFullMessages->isChecked() && localParts.contains(QLatin1String("RFC822"))) { + localParts.removeAll(QStringLiteral("RFC822")); + } + } + + policy.setLocalParts(localParts); + collection.setCachePolicy(policy); +} + +#include "moc_cachepolicypage.cpp" diff --git a/src/widgets/cachepolicypage.h b/src/widgets/cachepolicypage.h new file mode 100644 index 0000000..466d016 --- /dev/null +++ b/src/widgets/cachepolicypage.h @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2010 Till Adam + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "collectionpropertiespage.h" + +namespace Akonadi +{ +/** + * @short A page in a collection properties dialog to configure the cache policy. + * + * This page allows the user to fine tune the cache policy of a collection + * in the Akonadi storage. It provides two modes, a UserMode and an AdvancedMode. + * While the former should be used in end-user applications, the latter can be + * used in debugging tools. + * + * @see Akonadi::CollectionPropertiesDialog, Akonadi::CollectionPropertiesPageFactory + * + * @author Till Adam + * @since 4.6 + */ +class AKONADIWIDGETS_EXPORT CachePolicyPage : public CollectionPropertiesPage +{ + Q_OBJECT + +public: + /** + * Describes the mode of the cache policy page. + */ + enum GuiMode { + UserMode, ///< A simplified UI for end-users will be provided. + AdvancedMode ///< An advanced UI for debugging will be provided. + }; + + /** + * Creates a new cache policy page. + * + * @param parent The parent widget. + * @param mode The UI mode that will be used for the page. + */ + explicit CachePolicyPage(QWidget *parent, GuiMode mode = UserMode); + + /** + * Destroys the cache policy page. + */ + ~CachePolicyPage() override; + + /** + * Checks if the cache policy page can actually handle the given @p collection. + */ + Q_REQUIRED_RESULT bool canHandle(const Collection &collection) const override; + + /** + * Loads the page content from the given @p collection. + */ + void load(const Collection &collection) override; + + /** + * Saves page content to the given @p collection. + */ + void save(Collection &collection) override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CachePolicyPageFactory, CachePolicyPage) + +} + diff --git a/src/widgets/cachepolicypage.ui b/src/widgets/cachepolicypage.ui new file mode 100644 index 0000000..1af16d5 --- /dev/null +++ b/src/widgets/cachepolicypage.ui @@ -0,0 +1,302 @@ + + + CachePolicyPage + + + true + + + + 0 + 0 + 602 + 461 + + + + + + + Use options from parent folder or account + + + false + + + false + + + + + + + Synchronize when selecting this folder + + + + + + + + + Automatically synchronize after: + + + + + + + Never + + + minutes + + + 0 + + + 9999 + + + 0 + + + + + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Locally Cached Parts + + + + + + false + + + KEditListWidget::Add|KEditListWidget::Remove + + + Locally Cached Parts + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Retrieval Options + + + + + + Always retrieve full &messages + + + true + + + + + + + &Retrieve message bodies on demand + + + + + + + + + Keep message bodies locally for: + + + + + + + Forever + + + minutes + + + 0 + + + 0 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + KEditListWidget + QWidget +
keditlistwidget.h
+
+
+ + + + inherit + toggled(bool) + localParts + setDisabled(bool) + + + 199 + 14 + + + 321 + 192 + + + + + retrieveOnlyHeaders + toggled(bool) + label + setEnabled(bool) + + + 299 + 143 + + + 162 + 168 + + + + + retrieveOnlyHeaders + toggled(bool) + localCacheTimeout + setEnabled(bool) + + + 299 + 143 + + + 439 + 168 + + + + + inherit + toggled(bool) + syncOnDemand + setDisabled(bool) + + + 304 + 18 + + + 304 + 39 + + + + + inherit + toggled(bool) + label_2 + setDisabled(bool) + + + 304 + 18 + + + 154 + 64 + + + + + inherit + toggled(bool) + checkInterval + setDisabled(bool) + + + 304 + 18 + + + 446 + 64 + + + + +
diff --git a/src/widgets/collectioncombobox.cpp b/src/widgets/collectioncombobox.cpp new file mode 100644 index 0000000..88bf9e6 --- /dev/null +++ b/src/widgets/collectioncombobox.cpp @@ -0,0 +1,173 @@ +/* + This file is part of Akonadi Contact. + + SPDX-FileCopyrightText: 2007-2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectioncombobox.h" + +#include "asyncselectionhandler_p.h" +#include "collectiondialog.h" + +#include "collectionfetchscope.h" +#include "collectionfilterproxymodel.h" +#include "collectionutils.h" +#include "entityrightsfiltermodel.h" +#include "entitytreemodel.h" +#include "monitor.h" +#include "session.h" + +#include + +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionComboBox::Private +{ +public: + Private(QAbstractItemModel *customModel, CollectionComboBox *parent) + : mParent(parent) + { + if (customModel) { + mBaseModel = customModel; + } else { + mMonitor = new Akonadi::Monitor(mParent); + mMonitor->setObjectName(QStringLiteral("CollectionComboBoxMonitor")); + mMonitor->fetchCollection(true); + mMonitor->setCollectionMonitored(Akonadi::Collection::root()); + + // This ETM will be set to only show collections with the wanted mimetype in setMimeTypeFilter + mModel = new EntityTreeModel(mMonitor, mParent); + mModel->setItemPopulationStrategy(EntityTreeModel::NoItemPopulation); + mModel->setListFilter(CollectionFetchScope::Display); + + mBaseModel = mModel; + } + + // Flatten the tree, e.g. + // Kolab + // Kolab / Inbox + // Kolab / Inbox / Calendar + auto proxyModel = new KDescendantsProxyModel(parent); + proxyModel->setDisplayAncestorData(true); + proxyModel->setSourceModel(mBaseModel); + + // Filter it by mimetype again, to only keep + // Kolab / Inbox / Calendar + mMimeTypeFilterModel = new CollectionFilterProxyModel(parent); + mMimeTypeFilterModel->setSourceModel(proxyModel); + + // Filter by access rights. TODO: maybe this functionality could be provided by CollectionFilterProxyModel, to save one proxy? + mRightsFilterModel = new EntityRightsFilterModel(parent); + mRightsFilterModel->setSourceModel(mMimeTypeFilterModel); + + mParent->setModel(mRightsFilterModel); + mParent->model()->sort(mParent->modelColumn()); + + mSelectionHandler = new AsyncSelectionHandler(mRightsFilterModel, mParent); + mParent->connect(mSelectionHandler, &AsyncSelectionHandler::collectionAvailable, mParent, [this](const auto &mi) { + activated(mi); + }); + } + + ~Private() = default; + + void activated(int index); + void activated(const QModelIndex &index); + + CollectionComboBox *const mParent; + + Monitor *mMonitor = nullptr; + EntityTreeModel *mModel = nullptr; + QAbstractItemModel *mBaseModel = nullptr; + CollectionFilterProxyModel *mMimeTypeFilterModel = nullptr; + EntityRightsFilterModel *mRightsFilterModel = nullptr; + AsyncSelectionHandler *mSelectionHandler = nullptr; +}; + +void CollectionComboBox::Private::activated(int index) +{ + const QModelIndex modelIndex = mParent->model()->index(index, 0); + if (modelIndex.isValid()) { + Q_EMIT mParent->currentChanged(modelIndex.data(EntityTreeModel::CollectionRole).value()); + } +} + +void CollectionComboBox::Private::activated(const QModelIndex &index) +{ + mParent->setCurrentIndex(index.row()); +} + +CollectionComboBox::CollectionComboBox(QWidget *parent) + : QComboBox(parent) + , d(new Private(nullptr, this)) +{ +} + +CollectionComboBox::CollectionComboBox(QAbstractItemModel *model, QWidget *parent) + : QComboBox(parent) + , d(new Private(model, this)) +{ +} + +CollectionComboBox::~CollectionComboBox() +{ + delete d; +} + +void CollectionComboBox::setMimeTypeFilter(const QStringList &contentMimeTypes) +{ + d->mMimeTypeFilterModel->clearFilters(); + d->mMimeTypeFilterModel->addMimeTypeFilters(contentMimeTypes); + + if (d->mMonitor) { + for (const QString &mimeType : contentMimeTypes) { + d->mMonitor->setMimeTypeMonitored(mimeType, true); + } + } +} + +QStringList CollectionComboBox::mimeTypeFilter() const +{ + return d->mMimeTypeFilterModel->mimeTypeFilters(); +} + +void CollectionComboBox::setAccessRightsFilter(Collection::Rights rights) +{ + d->mRightsFilterModel->setAccessRights(rights); +} + +Akonadi::Collection::Rights CollectionComboBox::accessRightsFilter() const +{ + return d->mRightsFilterModel->accessRights(); +} + +void CollectionComboBox::setDefaultCollection(const Collection &collection) +{ + d->mSelectionHandler->waitForCollection(collection); +} + +Akonadi::Collection CollectionComboBox::currentCollection() const +{ + const QModelIndex modelIndex = model()->index(currentIndex(), 0); + if (modelIndex.isValid()) { + return modelIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); + } else { + return Akonadi::Collection(); + } +} + +void CollectionComboBox::setExcludeVirtualCollections(bool b) +{ + d->mMimeTypeFilterModel->setExcludeVirtualCollections(b); +} + +bool CollectionComboBox::excludeVirtualCollections() const +{ + return d->mMimeTypeFilterModel->excludeVirtualCollections(); +} + +#include "moc_collectioncombobox.cpp" diff --git a/src/widgets/collectioncombobox.h b/src/widgets/collectioncombobox.h new file mode 100644 index 0000000..4583f18 --- /dev/null +++ b/src/widgets/collectioncombobox.h @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2009 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "collection.h" + +#include + +class QAbstractItemModel; + +namespace Akonadi +{ +/** + * @short A combobox for selecting an Akonadi collection. + * + * This widget provides a combobox to select a collection + * from the Akonadi storage. + * The available collections can be filtered by mime type and + * access rights. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * QStringList contentMimeTypes; + * contentMimeTypes << KContacts::Addressee::mimeType(); + * contentMimeTypes << KContacts::ContactGroup::mimeType(); + * + * CollectionComboBox *box = new CollectionComboBox( this ); + * box->setMimeTypeFilter( contentMimeTypes ); + * box->setAccessRightsFilter( Collection::CanCreateItem ); + * ... + * + * const Collection collection = box->currentCollection(); + * + * @endcode + * + * @author Tobias Koenig + * @since 4.4 + */ +class AKONADIWIDGETS_EXPORT CollectionComboBox : public QComboBox +{ + Q_OBJECT + +public: + /** + * Creates a new collection combobox. + * + * @param parent The parent widget. + */ + explicit CollectionComboBox(QWidget *parent = nullptr); + + /** + * Creates a new collection combobox with a custom @p model. + * + * The filtering by content mime type and access rights is done + * on top of the custom model. + * + * @param model The custom model to use. + * @param parent The parent widget. + */ + explicit CollectionComboBox(QAbstractItemModel *model, QWidget *parent = nullptr); + + /** + * Destroys the collection combobox. + */ + ~CollectionComboBox(); + + /** + * Sets the content @p mimetypes the collections shall be filtered by. + */ + void setMimeTypeFilter(const QStringList &mimetypes); + + /** + * Returns the content mimetype the collections are filtered by. + * Don't assume this list has the original order. + */ + Q_REQUIRED_RESULT QStringList mimeTypeFilter() const; + + /** + * Sets the access @p rights the collections shall be filtered by. + */ + void setAccessRightsFilter(Collection::Rights rights); + + /** + * Returns the access rights the collections are filtered by. + */ + Q_REQUIRED_RESULT Collection::Rights accessRightsFilter() const; + + /** + * Sets the @p collection that shall be selected by default. + */ + void setDefaultCollection(const Collection &collection); + + /** + * Returns the current selection. + */ + Q_REQUIRED_RESULT Akonadi::Collection currentCollection() const; + + /** + * @since 4.12 + */ + void setExcludeVirtualCollections(bool b); + /** + * @since 4.12 + */ + Q_REQUIRED_RESULT bool excludeVirtualCollections() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever the current selection + * has been changed. + * + * @param collection The current selection. + */ + void currentChanged(const Akonadi::Collection &collection); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void activated(int)) + Q_PRIVATE_SLOT(d, void activated(const QModelIndex &)) + /// @endcond +}; + +} + diff --git a/src/widgets/collectiondialog.cpp b/src/widgets/collectiondialog.cpp new file mode 100644 index 0000000..6e70f8b --- /dev/null +++ b/src/widgets/collectiondialog.cpp @@ -0,0 +1,417 @@ +/* + SPDX-FileCopyrightText: 2008 Ingo Klöcker + SPDX-FileCopyrightText: 2010-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiondialog.h" + +#include "asyncselectionhandler_p.h" + +#include "collectioncreatejob.h" +#include "collectionfetchscope.h" +#include "collectionfilterproxymodel.h" +#include "collectionutils.h" +#include "entityrightsfiltermodel.h" +#include "entitytreemodel.h" +#include "entitytreeview.h" +#include "monitor.h" +#include "session.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionDialog::Private +{ +public: + Private(QAbstractItemModel *customModel, CollectionDialog *parent, CollectionDialogOptions options) + : mParent(parent) + { + // setup GUI + auto layout = new QVBoxLayout(mParent); + + mTextLabel = new QLabel(mParent); + layout->addWidget(mTextLabel); + mTextLabel->hide(); + + auto filterCollectionLineEdit = new QLineEdit(mParent); + filterCollectionLineEdit->setClearButtonEnabled(true); + filterCollectionLineEdit->setPlaceholderText( + i18nc("@info Displayed grayed-out inside the " + "textbox, verb to search", + "Search")); + layout->addWidget(filterCollectionLineEdit); + + mView = new EntityTreeView(mParent); + mView->setDragDropMode(QAbstractItemView::NoDragDrop); + mView->header()->hide(); + layout->addWidget(mView); + + mUseByDefault = new QCheckBox(i18n("Use folder by default"), mParent); + mUseByDefault->hide(); + layout->addWidget(mUseByDefault); + + mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, mParent); + mParent->connect(mButtonBox, &QDialogButtonBox::accepted, mParent, &QDialog::accept); + mParent->connect(mButtonBox, &QDialogButtonBox::rejected, mParent, &QDialog::reject); + layout->addWidget(mButtonBox); + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + // setup models + QAbstractItemModel *baseModel = nullptr; + + if (customModel) { + baseModel = customModel; + } else { + mMonitor = new Akonadi::Monitor(mParent); + mMonitor->setObjectName(QStringLiteral("CollectionDialogMonitor")); + mMonitor->fetchCollection(true); + mMonitor->setCollectionMonitored(Akonadi::Collection::root()); + + auto model = new EntityTreeModel(mMonitor, mParent); + model->setItemPopulationStrategy(EntityTreeModel::NoItemPopulation); + model->setListFilter(CollectionFetchScope::Display); + baseModel = model; + } + + mMimeTypeFilterModel = new CollectionFilterProxyModel(mParent); + mMimeTypeFilterModel->setSourceModel(baseModel); + mMimeTypeFilterModel->setExcludeVirtualCollections(true); + + mRightsFilterModel = new EntityRightsFilterModel(mParent); + mRightsFilterModel->setSourceModel(mMimeTypeFilterModel); + + mFilterCollection = new QSortFilterProxyModel(mParent); + mFilterCollection->setRecursiveFilteringEnabled(true); + mFilterCollection->setSourceModel(mRightsFilterModel); + mFilterCollection->setFilterCaseSensitivity(Qt::CaseInsensitive); + mView->setModel(mFilterCollection); + + changeCollectionDialogOptions(options); + mParent->connect(filterCollectionLineEdit, &QLineEdit::textChanged, mParent, [this](const QString &str) { + slotFilterFixedString(str); + }); + + mParent->connect(mView->selectionModel(), &QItemSelectionModel::selectionChanged, mParent, [this]() { + slotSelectionChanged(); + }); + mParent->connect(mView, qOverload(&QAbstractItemView::doubleClicked), mParent, [this]() { + slotDoubleClicked(); + }); + + mSelectionHandler = new AsyncSelectionHandler(mFilterCollection, mParent); + mParent->connect(mSelectionHandler, &AsyncSelectionHandler::collectionAvailable, mParent, [this](const QModelIndex &index) { + slotCollectionAvailable(index); + }); + readConfig(); + } + + ~Private() + { + writeConfig(); + } + + void slotCollectionAvailable(const QModelIndex &index) + { + mView->expandAll(); + mView->setCurrentIndex(index); + } + + void slotFilterFixedString(const QString &filter) + { + mFilterCollection->setFilterFixedString(filter); + if (mKeepTreeExpanded) { + mView->expandAll(); + } + } + + void readConfig() + { + KConfig config(QStringLiteral("akonadi_contactrc")); + KConfigGroup group(&config, QStringLiteral("CollectionDialog")); + const QSize size = group.readEntry("Size", QSize(800, 500)); + if (size.isValid()) { + mParent->resize(size); + } + } + + void writeConfig() const + { + KConfig config(QStringLiteral("akonadi_contactrc")); + KConfigGroup group(&config, QStringLiteral("CollectionDialog")); + group.writeEntry("Size", mParent->size()); + group.sync(); + } + + CollectionDialog *const mParent; + + Monitor *mMonitor = nullptr; + CollectionFilterProxyModel *mMimeTypeFilterModel = nullptr; + EntityRightsFilterModel *mRightsFilterModel = nullptr; + EntityTreeView *mView = nullptr; + AsyncSelectionHandler *mSelectionHandler = nullptr; + QLabel *mTextLabel = nullptr; + QSortFilterProxyModel *mFilterCollection = nullptr; + QCheckBox *mUseByDefault = nullptr; + QStringList mContentMimeTypes; + QDialogButtonBox *mButtonBox = nullptr; + QPushButton *mNewSubfolderButton = nullptr; + bool mAllowToCreateNewChildCollection = false; + bool mKeepTreeExpanded = false; + + void slotDoubleClicked(); + void slotSelectionChanged(); + void slotAddChildCollection(); + void slotCollectionCreationResult(KJob *job); + bool canCreateCollection(const Akonadi::Collection &parentCollection) const; + void changeCollectionDialogOptions(CollectionDialogOptions options); + bool canSelectCollection() const; +}; + +void CollectionDialog::Private::slotDoubleClicked() +{ + if (canSelectCollection()) { + mParent->accept(); + } +} + +bool CollectionDialog::Private::canSelectCollection() const +{ + bool result = (!mView->selectionModel()->selectedIndexes().isEmpty()); + if (mAllowToCreateNewChildCollection) { + const Akonadi::Collection parentCollection = mParent->selectedCollection(); + + if (parentCollection.isValid()) { + result = (parentCollection.rights() & Akonadi::Collection::CanCreateItem); + } + } + return result; +} + +void CollectionDialog::Private::slotSelectionChanged() +{ + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(!mView->selectionModel()->selectedIndexes().isEmpty()); + if (mAllowToCreateNewChildCollection) { + const Akonadi::Collection parentCollection = mParent->selectedCollection(); + const bool canCreateChildCollections = canCreateCollection(parentCollection); + + mNewSubfolderButton->setEnabled(canCreateChildCollections && !parentCollection.isVirtual()); + if (parentCollection.isValid()) { + const bool canCreateItems = (parentCollection.rights() & Akonadi::Collection::CanCreateItem); + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(canCreateItems); + } + } +} + +void CollectionDialog::Private::changeCollectionDialogOptions(CollectionDialogOptions options) +{ + mAllowToCreateNewChildCollection = (options & AllowToCreateNewChildCollection); + if (mAllowToCreateNewChildCollection) { + mNewSubfolderButton = mButtonBox->addButton(i18n("&New Subfolder..."), QDialogButtonBox::NoRole); + mNewSubfolderButton->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); + mNewSubfolderButton->setToolTip(i18n("Create a new subfolder under the currently selected folder")); + mNewSubfolderButton->setEnabled(false); + connect(mNewSubfolderButton, &QPushButton::clicked, mParent, [this]() { + slotAddChildCollection(); + }); + } + mKeepTreeExpanded = (options & KeepTreeExpanded); + if (mKeepTreeExpanded) { + mParent->connect(mRightsFilterModel, &EntityRightsFilterModel::rowsInserted, mView, &EntityTreeView::expandAll, Qt::UniqueConnection); + mView->expandAll(); + } +} + +bool CollectionDialog::Private::canCreateCollection(const Akonadi::Collection &parentCollection) const +{ + if (!parentCollection.isValid()) { + return false; + } + + if ((parentCollection.rights() & Akonadi::Collection::CanCreateCollection)) { + const QStringList dialogMimeTypeFilter = mParent->mimeTypeFilter(); + const QStringList parentCollectionMimeTypes = parentCollection.contentMimeTypes(); + for (const QString &mimetype : dialogMimeTypeFilter) { + if (parentCollectionMimeTypes.contains(mimetype)) { + return true; + } + } + return true; + } + return false; +} + +void CollectionDialog::Private::slotAddChildCollection() +{ + const Akonadi::Collection parentCollection = mParent->selectedCollection(); + if (canCreateCollection(parentCollection)) { + bool ok = false; + const QString name = + QInputDialog::getText(mParent, i18nc("@title:window", "New Folder"), i18nc("@label:textbox, name of a thing", "Name"), {}, {}, &ok); + if (name.trimmed().isEmpty() || !ok) { + return; + } + + Akonadi::Collection collection; + collection.setName(name); + collection.setParentCollection(parentCollection); + if (!mContentMimeTypes.isEmpty()) { + collection.setContentMimeTypes(mContentMimeTypes); + } + auto job = new Akonadi::CollectionCreateJob(collection); + connect(job, &Akonadi::CollectionCreateJob::result, mParent, [this](KJob *job) { + slotCollectionCreationResult(job); + }); + } +} + +void CollectionDialog::Private::slotCollectionCreationResult(KJob *job) +{ + if (job->error()) { + QMessageBox::critical(mParent, i18n("Folder creation failed"), i18n("Could not create folder: %1", job->errorString())); + } +} + +CollectionDialog::CollectionDialog(QWidget *parent) + : QDialog(parent) + , d(new Private(nullptr, this, CollectionDialog::None)) +{ +} + +CollectionDialog::CollectionDialog(QAbstractItemModel *model, QWidget *parent) + : QDialog(parent) + , d(new Private(model, this, CollectionDialog::None)) +{ +} + +CollectionDialog::CollectionDialog(CollectionDialogOptions options, QAbstractItemModel *model, QWidget *parent) + : QDialog(parent) + , d(new Private(model, this, options)) +{ +} + +CollectionDialog::~CollectionDialog() +{ + delete d; +} + +Akonadi::Collection CollectionDialog::selectedCollection() const +{ + if (selectionMode() == QAbstractItemView::SingleSelection) { + const QModelIndex index = d->mView->currentIndex(); + if (index.isValid()) { + return index.model()->data(index, EntityTreeModel::CollectionRole).value(); + } + } + + return Collection(); +} + +Akonadi::Collection::List CollectionDialog::selectedCollections() const +{ + Collection::List collections; + const QItemSelectionModel *selectionModel = d->mView->selectionModel(); + const QModelIndexList selectedIndexes = selectionModel->selectedIndexes(); + for (const QModelIndex &index : selectedIndexes) { + if (index.isValid()) { + const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + collections.append(collection); + } + } + } + + return collections; +} + +void CollectionDialog::setMimeTypeFilter(const QStringList &mimeTypes) +{ + if (mimeTypeFilter() == mimeTypes) { + return; + } + + d->mMimeTypeFilterModel->clearFilters(); + d->mMimeTypeFilterModel->addMimeTypeFilters(mimeTypes); + + if (d->mMonitor) { + for (const QString &mimetype : mimeTypes) { + d->mMonitor->setMimeTypeMonitored(mimetype); + } + } +} + +QStringList CollectionDialog::mimeTypeFilter() const +{ + return d->mMimeTypeFilterModel->mimeTypeFilters(); +} + +void CollectionDialog::setAccessRightsFilter(Collection::Rights rights) +{ + if (accessRightsFilter() == rights) { + return; + } + d->mRightsFilterModel->setAccessRights(rights); +} + +Akonadi::Collection::Rights CollectionDialog::accessRightsFilter() const +{ + return d->mRightsFilterModel->accessRights(); +} + +void CollectionDialog::setDescription(const QString &text) +{ + d->mTextLabel->setText(text); + d->mTextLabel->show(); +} + +void CollectionDialog::setDefaultCollection(const Collection &collection) +{ + d->mSelectionHandler->waitForCollection(collection); +} + +void CollectionDialog::setSelectionMode(QAbstractItemView::SelectionMode mode) +{ + d->mView->setSelectionMode(mode); +} + +QAbstractItemView::SelectionMode CollectionDialog::selectionMode() const +{ + return d->mView->selectionMode(); +} + +void CollectionDialog::changeCollectionDialogOptions(CollectionDialogOptions options) +{ + d->changeCollectionDialogOptions(options); +} + +void CollectionDialog::setUseFolderByDefault(bool b) +{ + d->mUseByDefault->setChecked(b); + d->mUseByDefault->show(); +} + +bool CollectionDialog::useFolderByDefault() const +{ + return d->mUseByDefault->isChecked(); +} + +void CollectionDialog::setContentMimeTypes(const QStringList &mimetypes) +{ + d->mContentMimeTypes = mimetypes; +} + +#include "moc_collectiondialog.cpp" diff --git a/src/widgets/collectiondialog.h b/src/widgets/collectiondialog.h new file mode 100644 index 0000000..93f4574 --- /dev/null +++ b/src/widgets/collectiondialog.h @@ -0,0 +1,202 @@ +/* + SPDX-FileCopyrightText: 2008 Ingo Klöcker + SPDX-FileCopyrightText: 2010-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "collection.h" + +#include +#include + +namespace Akonadi +{ +/** + * @short A collection selection dialog. + * + * Provides a dialog that lists collections that are available + * on the Akonadi storage and allows the selection of one or multiple + * collections. + * + * The list of shown collections can be filtered by mime type and access + * rights. Note that mime types are not enabled by default, so + * setMimeTypeFilter() must be called to enable the desired mime types. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * // Show the user a dialog to select a writable collection of contacts + * CollectionDialog dlg( this ); + * dlg.setMimeTypeFilter( QStringList() << KContacts::Addressee::mimeType() ); + * dlg.setAccessRightsFilter( Collection::CanCreateItem ); + * dlg.setDescription( i18n( "Select an address book for saving:" ) ); + * + * if ( dlg.exec() ) { + * const Collection collection = dlg.selectedCollection(); + * ... + * } + * + * @endcode + * + * @author Ingo Klöcker + * @since 4.3 + */ +class AKONADIWIDGETS_EXPORT CollectionDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY(CollectionDialog) + +public: + /* @since 4.6 + */ + enum CollectionDialogOption { + None = 0, + AllowToCreateNewChildCollection = 1, + KeepTreeExpanded = 2, + }; + + Q_DECLARE_FLAGS(CollectionDialogOptions, CollectionDialogOption) + + /** + * Creates a new collection dialog. + * + * @param parent The parent widget. + */ + explicit CollectionDialog(QWidget *parent = nullptr); + + /** + * Creates a new collection dialog with a custom @p model. + * + * The filtering by content mime type and access rights is done + * on top of the custom model. + * + * @param model The custom model to use. + * @param parent The parent widget. + * + * @since 4.4 + */ + explicit CollectionDialog(QAbstractItemModel *model, QWidget *parent = nullptr); + + /** + * Creates a new collection dialog with a custom @p model. + * + * The filtering by content mime type and access rights is done + * on top of the custom model. + * + * @param options The collection dialog options. + * @param model The custom model to use. + * @param parent The parent widget. + * + * @since 4.6 + */ + + explicit CollectionDialog(CollectionDialogOptions options, QAbstractItemModel *model = nullptr, QWidget *parent = nullptr); + + /** + * Destroys the collection dialog. + */ + ~CollectionDialog(); + + /** + * Sets the mime types any of which the selected collection(s) shall support. + * Note that mime types are not enabled by default. + * @param mimeTypes MIME type filter values + */ + void setMimeTypeFilter(const QStringList &mimeTypes); + + /** + * Returns the mime types any of which the selected collection(s) shall support. + */ + Q_REQUIRED_RESULT QStringList mimeTypeFilter() const; + + /** + * Sets the access @p rights that the listed collections shall match with. + * @param rights access rights filter values + * @since 4.4 + */ + void setAccessRightsFilter(Collection::Rights rights); + + /** + * Sets the access @p rights that the listed collections shall match with. + * + * @since 4.4 + */ + Q_REQUIRED_RESULT Collection::Rights accessRightsFilter() const; + + /** + * Sets the @p text that will be shown in the dialog. + * @param text the dialog's description text + * @since 4.4 + */ + void setDescription(const QString &text); + + /** + * Sets the @p collection that shall be selected by default. + * @param collection the dialog's pre-selected collection + * @since 4.4 + */ + void setDefaultCollection(const Collection &collection); + + /** + * Sets the selection mode. The initial default mode is + * QAbstractItemView::SingleSelection. + * @param mode the selection mode to use + * @see QAbstractItemView::setSelectionMode() + */ + void setSelectionMode(QAbstractItemView::SelectionMode mode); + + /** + * Returns the selection mode. + * @see QAbstractItemView::selectionMode() + */ + Q_REQUIRED_RESULT QAbstractItemView::SelectionMode selectionMode() const; + + /** + * Returns the selected collection if the selection mode is + * QAbstractItemView::SingleSelection. If another selection mode was set, + * or nothing is selected, an invalid collection is returned. + */ + Q_REQUIRED_RESULT Akonadi::Collection selectedCollection() const; + + /** + * Returns the list of selected collections. + */ + Q_REQUIRED_RESULT Akonadi::Collection::List selectedCollections() const; + + /** + * Change collection dialog options. + * @param options the collection dialog options to change + * @since 4.6 + */ + void changeCollectionDialogOptions(CollectionDialogOptions options); + + /** + * @since 4.13 + */ + void setUseFolderByDefault(bool b); + /** + * @since 4.13 + */ + Q_REQUIRED_RESULT bool useFolderByDefault() const; + /** + * Allow to specify collection content mimetype when we create new one. + * @since 4.14.6 + */ + void setContentMimeTypes(const QStringList &mimetypes); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} // namespace Akonadi + diff --git a/src/widgets/collectiongeneralpropertiespage.cpp b/src/widgets/collectiongeneralpropertiespage.cpp new file mode 100644 index 0000000..30218ed --- /dev/null +++ b/src/widgets/collectiongeneralpropertiespage.cpp @@ -0,0 +1,78 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiongeneralpropertiespage_p.h" + +#include "collection.h" +#include "collectionstatistics.h" +#include "collectionutils.h" +#include "entitydisplayattribute.h" + +#include + +#include + +using namespace Akonadi; + +/// @cond PRIVATE + +CollectionGeneralPropertiesPage::CollectionGeneralPropertiesPage(QWidget *parent) + : CollectionPropertiesPage(parent) +{ + setObjectName(QStringLiteral("Akonadi::CollectionGeneralPropertiesPage")); + + setPageTitle(i18nc("@title:tab general properties page", "General")); + ui.setupUi(this); +} + +void CollectionGeneralPropertiesPage::load(const Collection &collection) +{ + QString displayName; + QString iconName; + if (collection.hasAttribute()) { + displayName = collection.attribute()->displayName(); + iconName = collection.attribute()->iconName(); + } + + if (displayName.isEmpty()) { + ui.nameEdit->setText(collection.name()); + } else { + ui.nameEdit->setText(displayName); + } + + if (iconName.isEmpty()) { + ui.customIcon->setIcon(CollectionUtils::defaultIconName(collection)); + } else { + ui.customIcon->setIcon(iconName); + } + ui.customIconCheckbox->setChecked(!iconName.isEmpty()); + + if (collection.statistics().count() >= 0) { + ui.countLabel->setText(i18ncp("@label", "One object", "%1 objects", collection.statistics().count())); + ui.sizeLabel->setText(KFormat().formatByteSize(collection.statistics().size())); + } else { + ui.statsBox->hide(); + } +} + +void CollectionGeneralPropertiesPage::save(Collection &collection) +{ + if (collection.hasAttribute() && !collection.attribute()->displayName().isEmpty()) { + collection.attribute()->setDisplayName(ui.nameEdit->text()); + } else { + collection.setName(ui.nameEdit->text()); + } + + if (ui.customIconCheckbox->isChecked()) { + collection.attribute(Collection::AddIfMissing)->setIconName(ui.customIcon->icon()); + } else if (collection.hasAttribute()) { + collection.attribute()->setIconName(QString()); + } +} + +/// @endcond + +#include "moc_collectiongeneralpropertiespage_p.cpp" diff --git a/src/widgets/collectiongeneralpropertiespage.ui b/src/widgets/collectiongeneralpropertiespage.ui new file mode 100644 index 0000000..4a70ec3 --- /dev/null +++ b/src/widgets/collectiongeneralpropertiespage.ui @@ -0,0 +1,137 @@ + + CollectionGeneralPropertiesPage + + + + 0 + 0 + 400 + 300 + + + + + + + &Name: + + + nameEdit + + + + + + + + + + &Use custom icon: + + + + + + + false + + + folder + + + 16 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Statistics + + + + + + Content: + + + + + + + 0 objects + + + + + + + Size: + + + + + + + 0 Byte + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + KIconButton + QPushButton +
kiconbutton.h
+
+
+ + + + customIconCheckbox + toggled(bool) + customIcon + setEnabled(bool) + + + 115 + 55 + + + 245 + 55 + + + + +
diff --git a/src/widgets/collectiongeneralpropertiespage_p.h b/src/widgets/collectiongeneralpropertiespage_p.h new file mode 100644 index 0000000..0649997 --- /dev/null +++ b/src/widgets/collectiongeneralpropertiespage_p.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collectionpropertiespage.h" +#include "ui_collectiongeneralpropertiespage.h" + +namespace Akonadi +{ +/// @cond PRIVATE + +/** + * @internal + */ +class CollectionGeneralPropertiesPage : public CollectionPropertiesPage +{ + Q_OBJECT +public: + explicit CollectionGeneralPropertiesPage(QWidget *parent = nullptr); + + void load(const Collection &collection) override; + void save(Collection &collection) override; + +private: + Ui::CollectionGeneralPropertiesPage ui; +}; + +AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CollectionGeneralPropertiesPageFactory, CollectionGeneralPropertiesPage) +/// @endcond + +} + diff --git a/src/widgets/collectionmaintenancepage.cpp b/src/widgets/collectionmaintenancepage.cpp new file mode 100644 index 0000000..e1b6be1 --- /dev/null +++ b/src/widgets/collectionmaintenancepage.cpp @@ -0,0 +1,155 @@ +/* + SPDX-FileCopyrightText: 2009-2021 Laurent Montel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "collectionmaintenancepage.h" +#include "agentmanager.h" +#include "akonadiwidgets_debug.h" +#include "cachepolicy.h" +#include "core/collectionstatistics.h" +#include "indexpolicyattribute.h" +#include "monitor.h" +#include "servermanager.h" +#include "ui_collectionmaintenancepage.h" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +class CollectionMaintenancePage::Private +{ +public: + Private() + { + } + + void slotReindexCollection() + { + if (currentCollection.isValid()) { + // Don't allow to reindex twice. + ui.reindexButton->setEnabled(false); + + const auto service = ServerManager::agentServiceName(ServerManager::Agent, QStringLiteral("akonadi_indexing_agent")); + QDBusInterface indexingAgentIface(service, QStringLiteral("/"), QStringLiteral("org.freedesktop.Akonadi.Indexer")); + if (indexingAgentIface.isValid()) { + indexingAgentIface.call(QStringLiteral("reindexCollection"), static_cast(currentCollection.id())); + ui.indexedCountLbl->setText(i18n("Remember that indexing can take some minutes.")); + } else { + qCWarning(AKONADIWIDGETS_LOG) << "indexer interface not valid"; + } + } + } + + void updateLabel(qint64 nbMail, qint64 nbUnreadMail, qint64 size) + { + ui.itemsCountLbl->setText(QString::number(qMax(0LL, nbMail))); + ui.unreadItemsCountLbl->setText(QString::number(qMax(0LL, nbUnreadMail))); + ui.folderSizeLbl->setText(KFormat().formatByteSize(qMax(0LL, size))); + } + + Akonadi::Collection currentCollection; + Akonadi::Monitor *monitor = nullptr; + + Ui::CollectionMaintenancePage ui; +}; + +CollectionMaintenancePage::CollectionMaintenancePage(QWidget *parent) + : CollectionPropertiesPage(parent) + , d(new Private) +{ + setObjectName(QStringLiteral("Akonadi::CollectionMaintenancePage")); + setPageTitle(i18n("Maintenance")); +} + +CollectionMaintenancePage::~CollectionMaintenancePage() +{ + delete d; +} + +void CollectionMaintenancePage::init(const Collection &col) +{ + d->ui.setupUi(this); + + d->currentCollection = col; + d->monitor = new Monitor(this); + d->monitor->setObjectName(QStringLiteral("CollectionMaintenancePageMonitor")); + d->monitor->setCollectionMonitored(col, true); + d->monitor->fetchCollectionStatistics(true); + connect(d->monitor, &Monitor::collectionStatisticsChanged, this, [this](Collection::Id /*unused*/, const CollectionStatistics &stats) { + d->updateLabel(stats.count(), stats.unreadCount(), stats.size()); + }); + + if (!col.isVirtual()) { + const AgentInstance instance = Akonadi::AgentManager::self()->instance(col.resource()); + d->ui.folderTypeLbl->setText(instance.type().name()); + } else { + d->ui.folderTypeLbl->hide(); + d->ui.filesLayout->labelForField(d->ui.folderTypeLbl)->hide(); + } + + connect(d->ui.reindexButton, &QPushButton::clicked, this, [this]() { + d->slotReindexCollection(); + }); + + // Check if the resource caches full payloads or at least has local storage + // (so that the indexer can retrieve the payloads on demand) + const auto resource = Akonadi::AgentManager::self()->instance(col.resource()).type(); + if (!col.cachePolicy().localParts().contains(QLatin1String("RFC822")) + && resource.customProperties().value(QStringLiteral("HasLocalStorage"), QString()) != QLatin1String("true")) { + d->ui.indexingGroup->hide(); + } +} + +void CollectionMaintenancePage::load(const Collection &col) +{ + init(col); + if (col.isValid()) { + d->updateLabel(col.statistics().count(), col.statistics().unreadCount(), col.statistics().size()); + const auto attr = col.attribute(); + const bool indexingWasEnabled(!attr || attr->indexingEnabled()); + d->ui.enableIndexingChkBox->setChecked(indexingWasEnabled); + if (indexingWasEnabled) { + const auto service = ServerManager::agentServiceName(ServerManager::Agent, QStringLiteral("akonadi_indexing_agent")); + QDBusInterface indexingAgentIface(service, QStringLiteral("/"), QStringLiteral("org.freedesktop.Akonadi.Indexer")); + if (indexingAgentIface.isValid()) { + auto reply = indexingAgentIface.asyncCall(QStringLiteral("indexedItems"), static_cast(col.id())); + auto w = new QDBusPendingCallWatcher(reply, this); + connect(w, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) { + QDBusPendingReply reply = *w; + if (reply.isError()) { + d->ui.indexedCountLbl->setText(i18n("Error while retrieving indexed items count")); + qCWarning(AKONADIWIDGETS_LOG) << "Failed to retrieve indexed items count:" << reply.error().message(); + } else { + d->ui.indexedCountLbl->setText(i18np("Indexed %1 item in this folder", "Indexed %1 items in this folder", reply.argumentAt<0>())); + } + w->deleteLater(); + }); + d->ui.indexedCountLbl->setText(i18n("Calculating indexed items...")); + } else { + qCDebug(AKONADIWIDGETS_LOG) << "Failed to obtain Indexer interface"; + d->ui.indexedCountLbl->hide(); + } + } else { + d->ui.indexedCountLbl->hide(); + } + } +} + +void CollectionMaintenancePage::save(Collection &collection) +{ + if (!collection.hasAttribute() && d->ui.enableIndexingChkBox->isChecked()) { + return; + } + + auto attr = collection.attribute(Akonadi::Collection::AddIfMissing); + attr->setIndexingEnabled(d->ui.enableIndexingChkBox->isChecked()); +} diff --git a/src/widgets/collectionmaintenancepage.h b/src/widgets/collectionmaintenancepage.h new file mode 100644 index 0000000..2968030 --- /dev/null +++ b/src/widgets/collectionmaintenancepage.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2009-2021 Laurent Montel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "collectionpropertiespage.h" + +namespace Akonadi +{ +class AKONADIWIDGETS_EXPORT CollectionMaintenancePage : public Akonadi::CollectionPropertiesPage +{ + Q_OBJECT +public: + explicit CollectionMaintenancePage(QWidget *parent = nullptr); + ~CollectionMaintenancePage() override; + + void load(const Akonadi::Collection &col) override; + void save(Akonadi::Collection &col) override; + +protected: + void init(const Akonadi::Collection &); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(CollectionMaintenancePageFactory, CollectionMaintenancePage) + +} + diff --git a/src/widgets/collectionmaintenancepage.ui b/src/widgets/collectionmaintenancepage.ui new file mode 100644 index 0000000..fbd0297 --- /dev/null +++ b/src/widgets/collectionmaintenancepage.ui @@ -0,0 +1,153 @@ + + + CollectionMaintenancePage + + + + 0 + 0 + 468 + 371 + + + + + + + + 0 + 0 + + + + Files + + + + + + Folder type: + + + + + + + unknown + + + + + + + unknown + + + + + + + Size: + + + + + + + + + + + 0 + 0 + + + + Items + + + + + + Total items: + + + + + + + unknown + + + + + + + Unread items: + + + + + + + unknown + + + + + + + + + + + 0 + 0 + + + + Indexing + + + + + + Enable fulltext indexing + + + + + + + Retrieving indexed items count ... + + + + + + + Reindex folder + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/widgets/collectionpropertiesdialog.cpp b/src/widgets/collectionpropertiesdialog.cpp new file mode 100644 index 0000000..7d63f82 --- /dev/null +++ b/src/widgets/collectionpropertiesdialog.cpp @@ -0,0 +1,233 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionpropertiesdialog.h" + +#include "cachepolicy.h" +#include "cachepolicypage.h" +#include "collection.h" +#include "collectiongeneralpropertiespage_p.h" +#include "collectionmodifyjob.h" + +#include "akonadiwidgets_debug.h" + +#include +#include +#include + +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionPropertiesDialog::Private +{ +public: + Private(CollectionPropertiesDialog *parent, const Akonadi::Collection &collection, const QStringList &pageNames); + + void init(); + + static void registerBuiltinPages(); + + void save() + { + const int numberOfTab(mTabWidget->count()); + for (int i = 0; i < numberOfTab; ++i) { + auto page = static_cast(mTabWidget->widget(i)); + page->save(mCollection); + } + + // We use WA_DeleteOnClose => Don't use dialog as parent otherwise we can't save modified collection. + auto job = new CollectionModifyJob(mCollection); + connect(job, &CollectionModifyJob::result, q, [this](KJob *job) { + saveResult(job); + }); + Q_EMIT q->settingsSaved(); + } + + void saveResult(KJob *job) + { + if (job->error()) { + // TODO + qCWarning(AKONADIWIDGETS_LOG) << job->errorString(); + } + } + + void setCurrentPage(const QString &name) + { + const int numberOfTab(mTabWidget->count()); + for (int i = 0; i < numberOfTab; ++i) { + QWidget *w = mTabWidget->widget(i); + if (w->objectName() == name) { + mTabWidget->setCurrentIndex(i); + break; + } + } + } + + CollectionPropertiesDialog *const q; + Collection mCollection; + QStringList mPageNames; + QTabWidget *mTabWidget = nullptr; +}; + +class CollectionPropertiesPageFactoryList : public QList +{ +public: + explicit CollectionPropertiesPageFactoryList() = default; + CollectionPropertiesPageFactoryList(const CollectionPropertiesPageFactoryList &) = delete; + CollectionPropertiesPageFactoryList &operator=(const CollectionPropertiesPageFactoryList &) = delete; + ~CollectionPropertiesPageFactoryList() + { + qDeleteAll(*this); + } +}; + +Q_GLOBAL_STATIC(CollectionPropertiesPageFactoryList, s_pages) // NOLINT(readability-redundant-member-init) + +static bool s_defaultPage = true; + +CollectionPropertiesDialog::Private::Private(CollectionPropertiesDialog *qq, const Akonadi::Collection &collection, const QStringList &pageNames) + : q(qq) + , mCollection(collection) + , mPageNames(pageNames) + , mTabWidget(nullptr) +{ + if (s_defaultPage) { + registerBuiltinPages(); + } +} + +void CollectionPropertiesDialog::Private::registerBuiltinPages() +{ + static bool registered = false; + + if (registered) { + return; + } + + s_pages->append(new CollectionGeneralPropertiesPageFactory()); + s_pages->append(new CachePolicyPageFactory()); + + registered = true; +} + +void CollectionPropertiesDialog::Private::init() +{ + auto mainLayout = new QVBoxLayout(q); + q->setAttribute(Qt::WA_DeleteOnClose); + mTabWidget = new QTabWidget(q); + mainLayout->addWidget(mTabWidget); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, q); + QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); // NOLINT(bugprone-suspicious-enum-usage) + q->connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); + q->connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); + mainLayout->addWidget(buttonBox); + + if (mPageNames.isEmpty()) { // default loading + for (CollectionPropertiesPageFactory *factory : std::as_const(*s_pages)) { + CollectionPropertiesPage *page = factory->createWidget(mTabWidget); + if (page->canHandle(mCollection)) { + mTabWidget->addTab(page, page->pageTitle()); + page->load(mCollection); + } else { + delete page; + } + } + } else { // custom loading + QHash pages; + + for (CollectionPropertiesPageFactory *factory : std::as_const(*s_pages)) { + CollectionPropertiesPage *page = factory->createWidget(mTabWidget); + const QString pageName = page->objectName(); + + if (page->canHandle(mCollection) && mPageNames.contains(pageName) && !pages.contains(pageName)) { + pages.insert(page->objectName(), page); + } else { + delete page; + } + } + + for (const QString &pageName : std::as_const(mPageNames)) { + CollectionPropertiesPage *page = pages.value(pageName); + if (page) { + mTabWidget->addTab(page, page->pageTitle()); + page->load(mCollection); + } + } + } + + q->connect(buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, q, [this]() { + save(); + }); + q->connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, q, &QObject::deleteLater); + + KConfigGroup group(KSharedConfig::openStateConfig(), "CollectionPropertiesDialog"); + const QSize size = group.readEntry("Size", QSize()); + if (size.isValid()) { + q->resize(size); + } else { + q->resize(q->sizeHint().width(), q->sizeHint().height()); + } +} + +CollectionPropertiesDialog::CollectionPropertiesDialog(const Collection &collection, QWidget *parent) + : QDialog(parent) + , d(new Private(this, collection, QStringList())) +{ + d->init(); +} + +CollectionPropertiesDialog::CollectionPropertiesDialog(const Collection &collection, const QStringList &pages, QWidget *parent) + : QDialog(parent) + , d(new Private(this, collection, pages)) +{ + d->init(); +} + +CollectionPropertiesDialog::~CollectionPropertiesDialog() +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "CollectionPropertiesDialog"); + group.writeEntry("Size", size()); + delete d; +} + +void CollectionPropertiesDialog::registerPage(CollectionPropertiesPageFactory *factory) +{ + if (s_pages->isEmpty() && s_defaultPage) { + Private::registerBuiltinPages(); + } + s_pages->append(factory); +} + +void CollectionPropertiesDialog::useDefaultPage(bool defaultPage) +{ + s_defaultPage = defaultPage; +} + +QString CollectionPropertiesDialog::defaultPageObjectName(DefaultPage page) +{ + switch (page) { + case GeneralPage: + return QStringLiteral("Akonadi::CollectionGeneralPropertiesPage"); + case CachePage: + return QStringLiteral("Akonadi::CachePolicyPage"); + } + + return QString(); +} + +void CollectionPropertiesDialog::setCurrentPage(const QString &name) +{ + d->setCurrentPage(name); +} + +#include "moc_collectionpropertiesdialog.cpp" diff --git a/src/widgets/collectionpropertiesdialog.h b/src/widgets/collectionpropertiesdialog.h new file mode 100644 index 0000000..48cbae4 --- /dev/null +++ b/src/widgets/collectionpropertiesdialog.h @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "collectionpropertiespage.h" + +#include + +namespace Akonadi +{ +class Collection; + +/** + * @short A generic and extensible dialog for collection properties. + * + * This dialog allows you to show or modify the properties of a collection. + * + * @code + * + * Akonadi::Collection collection = ... + * + * CollectionPropertiesDialog dlg( collection, this ); + * dlg.exec(); + * + * @endcode + * + * It can be extended by custom pages, which contains gui elements for custom + * properties. + * + * @see Akonadi::CollectionPropertiesPage + * + * @author Volker Krause + */ +class AKONADIWIDGETS_EXPORT CollectionPropertiesDialog : public QDialog +{ + Q_OBJECT +public: + /** + * Enumerates the registered default pages which can be displayed. + * + * @since 4.7 + */ + enum DefaultPage { + GeneralPage, //!< General properties page + CachePage //!< Cache properties page + }; + + /** + * Creates a new collection properties dialog. + * + * @param collection The collection which properties should be shown. + * @param parent The parent widget. + */ + explicit CollectionPropertiesDialog(const Collection &collection, QWidget *parent = nullptr); + + /** + * Creates a new collection properties dialog. + * + * This constructor allows to specify the subset of registered pages that will + * be shown as well as their order. The pages have to set an objectName in their + * constructor to make it work. If an empty list is passed, all registered pages + * will be loaded. Use defaultPageObjectName() to fetch the object name for a + * registered default page. + * + * @param collection The collection which properties should be shown. + * @param pages The object names of the pages that shall be loaded. + * @param parent The parent widget. + * + * @since 4.6 + */ + CollectionPropertiesDialog(const Collection &collection, const QStringList &pages, QWidget *parent = nullptr); + + /** + * Destroys the collection properties dialog. + * + * @note Never call manually, the dialog is deleted automatically once all changes + * are written back to the Akonadi storage. + */ + ~CollectionPropertiesDialog(); + + /** + * Register custom pages for the collection properties dialog. + * + * @param factory The properties page factory that provides the custom page. + * + * @see Akonadi::CollectionPropertiesPageFactory + */ + static void registerPage(CollectionPropertiesPageFactory *factory); + + /** + * Sets whether to @p use default page or not. + * + * @since 4.4 + * @param use mode of default page's usage + */ + static void useDefaultPage(bool use); + + /** + * Returns the object name of one of the dialog's registered default pages. + * The object name may be used in the QStringList constructor parameter to + * specify which default pages should be shown. + * + * @param page the desired page + * @return the page's object name + * + * @since 4.7 + */ + static QString defaultPageObjectName(DefaultPage page); + + /** + * Sets the page to be shown in the tab widget. + * + * @param name The object name of the page that is to be shown. + * + * @since 4.10 + */ + void setCurrentPage(const QString &name); + +Q_SIGNALS: + void settingsSaved(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/collectionpropertiespage.cpp b/src/widgets/collectionpropertiespage.cpp new file mode 100644 index 0000000..05e2950 --- /dev/null +++ b/src/widgets/collectionpropertiespage.cpp @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionpropertiespage.h" + +using namespace Akonadi; + +/// @cond PRIVATE + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionPropertiesPage::Private +{ +public: + QString title; +}; + +/// @endcond + +CollectionPropertiesPage::CollectionPropertiesPage(QWidget *parent) + : QWidget(parent) + , d(new Private) +{ +} + +CollectionPropertiesPage::~CollectionPropertiesPage() +{ + delete d; +} + +bool CollectionPropertiesPage::canHandle(const Collection &collection) const +{ + Q_UNUSED(collection) + return true; +} + +QString Akonadi::CollectionPropertiesPage::pageTitle() const +{ + return d->title; +} + +void CollectionPropertiesPage::setPageTitle(const QString &title) +{ + d->title = title; +} + +CollectionPropertiesPageFactory::~CollectionPropertiesPageFactory() +{ +} diff --git a/src/widgets/collectionpropertiespage.h b/src/widgets/collectionpropertiespage.h new file mode 100644 index 0000000..a7381a6 --- /dev/null +++ b/src/widgets/collectionpropertiespage.h @@ -0,0 +1,210 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +namespace Akonadi +{ +class Collection; + +/** + * @short A single page in a collection properties dialog. + * + * The collection properties dialog can be extended by custom + * collection properties pages, which provide gui elements for + * viewing and changing collection attributes. + * + * The following example shows how to create a simple collection + * properties page for the secrecy attribute from the Akonadi::Attribute + * example. + * + * @code + * + * class SecrecyPage : public CollectionPropertiesPage + * { + * public: + * SecrecyPage( QWidget *parent = nullptr ) + * : CollectionPropertiesPage( parent ) + * { + * QVBoxLayout *layout = new QVBoxLayout( this ); + * + * mSecrecy = new QComboBox( this ); + * mSecrecy->addItem( "Public" ); + * mSecrecy->addItem( "Private" ); + * mSecrecy->addItem( "Confidential" ); + * + * layout->addWidget( new QLabel( "Secrecy:" ) ); + * layout->addWidget( mSecrecy ); + * + * setPageTitle( i18n( "Secrecy" ) ); + * } + * + * void load( const Collection &collection ) + * { + * SecrecyAttribute *attr = collection.attribute( "secrecy" ); + * + * switch ( attr->secrecy() ) { + * case SecrecyAttribute::Public: mSecrecy->setCurrentIndex( 0 ); break; + * case SecrecyAttribute::Private: mSecrecy->setCurrentIndex( 1 ); break; + * case SecrecyAttribute::Confidential: mSecrecy->setCurrentIndex( 2 ); break; + * } + * } + * + * void save( Collection &collection ) + * { + * SecrecyAttribute *attr = collection.attribute( "secrecy" ); + * + * switch ( mSecrecy->currentIndex() ) { + * case 0: attr->setSecrecy( SecrecyAttribute::Public ); break; + * case 1: attr->setSecrecy( SecrecyAttribute::Private ); break; + * case 2: attr->setSecrecy( SecrecyAttribute::Confidential ); break; + * } + * } + * + * bool canHandle( const Collection &collection ) const + * { + * return collection.hasAttribute( "secrecy" ); + * } + * }; + * + * AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY( SecrecyPageFactory, SecrecyPage ) + * + * @endcode + * + * @see Akonadi::CollectionPropertiesDialog, Akonadi::CollectionPropertiesPageFactory + * + * @author Volker Krause + */ +class AKONADIWIDGETS_EXPORT CollectionPropertiesPage : public QWidget +{ + Q_OBJECT +public: + /** + * Creates a new collection properties page. + * + * @param parent The parent widget. + */ + explicit CollectionPropertiesPage(QWidget *parent = nullptr); + + /** + * Destroys the collection properties page. + */ + ~CollectionPropertiesPage(); + + /** + * Loads the page content from the given collection. + * + * @param collection The collection to load. + */ + virtual void load(const Collection &collection) = 0; + + /** + * Saves page content to the given collection. + * + * @param collection Reference to the collection to save to. + */ + virtual void save(Collection &collection) = 0; + + /** + * Checks if this page can actually handle the given collection. + * + * Returns @c true if the collection can be handled, @c false otherwise + * The default implementation returns always @c true. When @c false is returned + * this page is not shown in the properties dialog. + * @param collection The collection to check. + */ + virtual bool canHandle(const Collection &collection) const; + + /** + * Sets the page title. + * + * @param title Translated, preferably short tab title. + */ + void setPageTitle(const QString &title); + + /** + * Returns the page title. + */ + QString pageTitle() const; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +/** + * @short A factory class for collection properties dialog pages. + * + * The factory encapsulates the creation of the collection properties + * dialog page. + * You can use the AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY macro + * to create a factory class automatically. + * + * @author Volker Krause + */ +class AKONADIWIDGETS_EXPORT CollectionPropertiesPageFactory +{ +public: + /** + * Destroys the collection properties page factory. + */ + virtual ~CollectionPropertiesPageFactory(); + + /** + * Returns the actual page widget. + * + * @param parent The parent widget. + */ + virtual CollectionPropertiesPage *createWidget(QWidget *parent = nullptr) const = 0; + +protected: + explicit CollectionPropertiesPageFactory() = default; + +private: + Q_DISABLE_COPY_MOVE(CollectionPropertiesPageFactory) +}; + +/** + * @def AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY + * + * The AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY macro can be used to + * create a factory for a custom collection properties page. + * + * @code + * + * class MyPage : public Akonadi::CollectionPropertiesPage + * { + * ... + * } + * + * AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY( MyPageFactory, MyPage ) + * + * @endcode + * + * The macro takes two arguments, where the first one is the name of the + * factory class that shall be created and the second arguments is the name + * of the custom collection properties page class. + * + * @ingroup AkonadiMacros + */ +#define AKONADI_COLLECTION_PROPERTIES_PAGE_FACTORY(factoryName, className) \ + class factoryName : public Akonadi::CollectionPropertiesPageFactory \ + { \ + public: \ + inline Akonadi::CollectionPropertiesPage *createWidget(QWidget *parent = nullptr) const override \ + { \ + return new className(parent); \ + } \ + }; + +} + diff --git a/src/widgets/collectionrequester.cpp b/src/widgets/collectionrequester.cpp new file mode 100644 index 0000000..ebf85f8 --- /dev/null +++ b/src/widgets/collectionrequester.cpp @@ -0,0 +1,259 @@ +/* + SPDX-FileCopyrightText: 2008 Ingo Klöcker + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionrequester.h" +#include "collectionfetchjob.h" +#include "collectionfetchscope.h" +#include "entitydisplayattribute.h" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN CollectionRequester::Private +{ +public: + explicit Private(CollectionRequester *parent) + : q(parent) + { + } + + ~Private() + { + } + + void fetchCollection(const Collection &collection); + + void init(); + + // slots + void _k_slotOpenDialog(); + void _k_collectionReceived(KJob *job); + void _k_collectionsNamesReceived(KJob *job); + + CollectionRequester *const q; + Collection collection; + QLineEdit *edit = nullptr; + QPushButton *button = nullptr; + CollectionDialog *collectionDialog = nullptr; +}; + +void CollectionRequester::Private::fetchCollection(const Collection &collection) +{ + auto job = new CollectionFetchJob(collection, Akonadi::CollectionFetchJob::Base, q); + job->setProperty("OriginalCollectionId", collection.id()); + job->fetchScope().setAncestorRetrieval(CollectionFetchScope::All); + connect(job, &CollectionFetchJob::finished, q, [this](KJob *job) { + _k_collectionReceived(job); + }); +} + +void CollectionRequester::Private::_k_collectionReceived(KJob *job) +{ + auto fetch = qobject_cast(job); + if (!fetch) { + return; + } + if (fetch->collections().size() == 1) { + Collection::List chain; + Collection currentCollection = fetch->collections().at(0); + while (currentCollection.isValid()) { + chain << currentCollection; + currentCollection = Collection(currentCollection.parentCollection()); + } + + auto namesFetch = new CollectionFetchJob(chain, CollectionFetchJob::Base, q); + namesFetch->setProperty("OriginalCollectionId", job->property("OriginalCollectionId")); + namesFetch->fetchScope().setAncestorRetrieval(CollectionFetchScope::Parent); + connect(namesFetch, &CollectionFetchJob::finished, q, [this](KJob *job) { + _k_collectionsNamesReceived(job); + }); + } else { + _k_collectionsNamesReceived(job); + } +} + +void CollectionRequester::Private::_k_collectionsNamesReceived(KJob *job) +{ + auto fetch = qobject_cast(job); + const qint64 originalId = fetch->property("OriginalCollectionId").toLongLong(); + + QMap names; + const Akonadi::Collection::List lstCols = fetch->collections(); + for (const Collection &collection : lstCols) { + names.insert(collection.id(), collection); + } + + QStringList namesList; + Collection currentCollection = names.take(originalId); + while (currentCollection.isValid()) { + namesList.prepend(currentCollection.displayName()); + currentCollection = names.take(currentCollection.parentCollection().id()); + } + edit->setText(namesList.join(QLatin1Char('/'))); +} + +void CollectionRequester::Private::init() +{ + auto hbox = new QHBoxLayout(q); + hbox->setContentsMargins(0, 0, 0, 0); + + edit = new QLineEdit(q); + edit->setReadOnly(true); + edit->setPlaceholderText(i18n("No Folder")); + edit->setClearButtonEnabled(false); + edit->setFocusPolicy(Qt::NoFocus); + hbox->addWidget(edit); + + button = new QPushButton(q); + button->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); + const int buttonSize = edit->sizeHint().height(); + button->setFixedSize(buttonSize, buttonSize); + button->setToolTip(i18n("Open collection dialog")); + hbox->addWidget(button); + + hbox->setSpacing(-1); + + edit->installEventFilter(q); + q->setFocusProxy(button); + q->setFocusPolicy(Qt::StrongFocus); + + q->connect(button, &QPushButton::clicked, q, [this]() { + _k_slotOpenDialog(); + }); + + auto openAction = new QAction(q); + openAction->setShortcut(KStandardShortcut::Open); + q->connect(openAction, &QAction::triggered, q, [this]() { + _k_slotOpenDialog(); + }); + + collectionDialog = new CollectionDialog(q); + collectionDialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("akonadi"))); + collectionDialog->setWindowTitle(i18nc("@title:window", "Select a collection")); + collectionDialog->setSelectionMode(QAbstractItemView::SingleSelection); + collectionDialog->changeCollectionDialogOptions(CollectionDialog::KeepTreeExpanded); +} + +void CollectionRequester::Private::_k_slotOpenDialog() +{ + CollectionDialog *dlg = collectionDialog; + + if (dlg->exec() != QDialog::Accepted) { + return; + } + + const Akonadi::Collection collection = dlg->selectedCollection(); + q->setCollection(collection); + Q_EMIT q->collectionChanged(collection); +} + +CollectionRequester::CollectionRequester(QWidget *parent) + : QWidget(parent) + , d(new Private(this)) +{ + d->init(); +} + +CollectionRequester::CollectionRequester(const Akonadi::Collection &collection, QWidget *parent) + : QWidget(parent) + , d(new Private(this)) +{ + d->init(); + setCollection(collection); +} + +CollectionRequester::~CollectionRequester() +{ + delete d; +} + +Collection CollectionRequester::collection() const +{ + return d->collection; +} + +void CollectionRequester::setCollection(const Collection &collection) +{ + d->collection = collection; + QString name; + if (collection.isValid()) { + name = collection.displayName(); + } + + d->edit->setText(name); + Q_EMIT collectionChanged(collection); + d->fetchCollection(collection); +} + +void CollectionRequester::setMimeTypeFilter(const QStringList &mimeTypes) +{ + if (d->collectionDialog) { + d->collectionDialog->setMimeTypeFilter(mimeTypes); + } +} + +QStringList CollectionRequester::mimeTypeFilter() const +{ + if (d->collectionDialog) { + return d->collectionDialog->mimeTypeFilter(); + } else { + return QStringList(); + } +} + +void CollectionRequester::setAccessRightsFilter(Collection::Rights rights) +{ + if (d->collectionDialog) { + d->collectionDialog->setAccessRightsFilter(rights); + } +} + +Collection::Rights CollectionRequester::accessRightsFilter() const +{ + if (d->collectionDialog) { + return d->collectionDialog->accessRightsFilter(); + } else { + return Akonadi::Collection::ReadOnly; + } +} + +void CollectionRequester::changeCollectionDialogOptions(CollectionDialog::CollectionDialogOptions options) +{ + if (d->collectionDialog) { + d->collectionDialog->changeCollectionDialogOptions(options); + } +} + +void CollectionRequester::setContentMimeTypes(const QStringList &mimetypes) +{ + if (d->collectionDialog) { + d->collectionDialog->setContentMimeTypes(mimetypes); + } +} + +void CollectionRequester::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::WindowTitleChange) { + if (d->collectionDialog) { + d->collectionDialog->setWindowTitle(windowTitle()); + } + } else if (event->type() == QEvent::EnabledChange) { + if (d->collectionDialog) { + d->collectionDialog->setEnabled(true); + } + } + QWidget::changeEvent(event); +} + +#include "moc_collectionrequester.cpp" diff --git a/src/widgets/collectionrequester.h b/src/widgets/collectionrequester.h new file mode 100644 index 0000000..e3383fe --- /dev/null +++ b/src/widgets/collectionrequester.h @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2008 Ingo Klöcker + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "collection.h" +#include "collectiondialog.h" +#include + +namespace Akonadi +{ +/** + * @short A widget to request an Akonadi collection from the user. + * + * This class is a widget showing a read-only lineedit displaying + * the currently chosen collection and a button invoking a dialog + * for choosing a collection. + * + * Example: + * + * @code + * + * // create a collection requester to select a collection of contacts + * Akonadi::CollectionRequester requester( Akonadi::Collection::root(), this ); + * requester.setMimeTypeFilter( QStringList() << QString( "text/directory" ) ); + * + * ... + * + * const Akonadi::Collection collection = requester.collection(); + * if ( collection.isValid() ) { + * ... + * } + * + * @endcode + * + * @author Ingo Klöcker + * @since 4.3 + */ +class AKONADIWIDGETS_EXPORT CollectionRequester : public QWidget +{ + Q_OBJECT + Q_DISABLE_COPY(CollectionRequester) + +public: + /** + * Creates a collection requester. + * + * @param parent The parent widget. + */ + explicit CollectionRequester(QWidget *parent = nullptr); + + /** + * Creates a collection requester with an initial @p collection. + * + * @param collection The initial collection. + * @param parent The parent widget. + */ + explicit CollectionRequester(const Akonadi::Collection &collection, QWidget *parent = nullptr); + + /** + * Destroys the collection requester. + */ + ~CollectionRequester() override; + + /** + * Returns the currently chosen collection, or an empty collection if none + * none was chosen. + */ + Q_REQUIRED_RESULT Akonadi::Collection collection() const; + + /** + * Sets the mime types any of which the selected collection shall support. + */ + void setMimeTypeFilter(const QStringList &mimeTypes); + + /** + * Returns the mime types any of which the selected collection shall support. + */ + Q_REQUIRED_RESULT QStringList mimeTypeFilter() const; + + /** + * Sets the access @p rights that the listed collections shall match with. + * @param rights the access rights to set + * @since 4.4 + */ + void setAccessRightsFilter(Collection::Rights rights); + + /** + * Returns the access rights that the listed collections shall match with. + * @since 4.4 + */ + Q_REQUIRED_RESULT Collection::Rights accessRightsFilter() const; + + /** + * @param options new collection dialog options + */ + void changeCollectionDialogOptions(CollectionDialog::CollectionDialogOptions options); + + /** + * Allow to specify collection content mimetype when we create new one. + * @since 4.14.6 + */ + void setContentMimeTypes(const QStringList &mimetypes); + +protected: + void changeEvent(QEvent *event) override; + +public Q_SLOTS: + /** + * Sets the @p collection of the requester. + */ + void setCollection(const Akonadi::Collection &collection); + +Q_SIGNALS: + /** + * This signal is emitted when the selected collection has changed. + * + * @param collection The selected collection. + * + * @since 4.5 + */ + void collectionChanged(const Akonadi::Collection &collection); + +private: + class Private; + Private *const d; +}; + +} // namespace Akonadi + diff --git a/src/widgets/collectionstatisticsdelegate.cpp b/src/widgets/collectionstatisticsdelegate.cpp new file mode 100644 index 0000000..83ff38c --- /dev/null +++ b/src/widgets/collectionstatisticsdelegate.cpp @@ -0,0 +1,332 @@ +/* + SPDX-FileCopyrightText: 2008 Thomas McGuire + SPDX-FileCopyrightText: 2012-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionstatisticsdelegate.h" + +#include "akonadiwidgets_debug.h" +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "collection.h" +#include "collectionstatistics.h" +#include "entitytreemodel.h" +#include "progressspinnerdelegate_p.h" + +using namespace Akonadi; + +namespace Akonadi +{ +enum CountType { + UnreadCount, + TotalCount, +}; + +class CollectionStatisticsDelegatePrivate +{ +public: + QAbstractItemView *const parent; + bool drawUnreadAfterFolder = false; + DelegateAnimator *animator = nullptr; + QColor mSelectedUnreadColor; + QColor mDeselectedUnreadColor; + + explicit CollectionStatisticsDelegatePrivate(QAbstractItemView *treeView) + : parent(treeView) + { + updateColor(); + } + + void getCountRecursive(const QModelIndex &index, qint64 &totalCount, qint64 &unreadCount, qint64 &totalSize) const + { + auto collection = qvariant_cast(index.data(EntityTreeModel::CollectionRole)); + // Do not assert on invalid collections, since a collection may be deleted + // in the meantime and deleted collections are invalid. + if (collection.isValid()) { + CollectionStatistics statistics = collection.statistics(); + totalCount += qMax(0LL, statistics.count()); + unreadCount += qMax(0LL, statistics.unreadCount()); + totalSize += qMax(0LL, statistics.size()); + if (index.model()->hasChildren(index)) { + const int rowCount = index.model()->rowCount(index); + for (int row = 0; row < rowCount; row++) { + static const int column = 0; + getCountRecursive(index.model()->index(row, column, index), totalCount, unreadCount, totalSize); + } + } + } + } + + void updateColor() + { + mSelectedUnreadColor = KColorScheme(QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::LinkText).color(); + mDeselectedUnreadColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color(); + } +}; + +} // namespace Akonadi + +CollectionStatisticsDelegate::CollectionStatisticsDelegate(QAbstractItemView *parent) + : QStyledItemDelegate(parent) + , d_ptr(new CollectionStatisticsDelegatePrivate(parent)) +{ +} + +CollectionStatisticsDelegate::CollectionStatisticsDelegate(QTreeView *parent) + : QStyledItemDelegate(parent) + , d_ptr(new CollectionStatisticsDelegatePrivate(parent)) +{ +} + +CollectionStatisticsDelegate::~CollectionStatisticsDelegate() +{ + delete d_ptr; +} + +void CollectionStatisticsDelegate::setUnreadCountShown(bool enable) +{ + Q_D(CollectionStatisticsDelegate); + d->drawUnreadAfterFolder = enable; +} + +bool CollectionStatisticsDelegate::unreadCountShown() const +{ + Q_D(const CollectionStatisticsDelegate); + return d->drawUnreadAfterFolder; +} + +void CollectionStatisticsDelegate::setProgressAnimationEnabled(bool enable) +{ + Q_D(CollectionStatisticsDelegate); + if (enable == (d->animator != nullptr)) { + return; + } + if (enable) { + Q_ASSERT(!d->animator); + auto animator = new Akonadi::DelegateAnimator(d->parent); + d->animator = animator; + } else { + delete d->animator; + d->animator = nullptr; + } +} + +bool CollectionStatisticsDelegate::progressAnimationEnabled() const +{ + Q_D(const CollectionStatisticsDelegate); + return (d->animator != nullptr); +} + +void CollectionStatisticsDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const +{ + Q_D(const CollectionStatisticsDelegate); + + auto noTextOption = qstyleoption_cast(option); + QStyledItemDelegate::initStyleOption(noTextOption, index); + if (option->decorationPosition != QStyleOptionViewItem::Top) { + if (noTextOption) { + noTextOption->text.clear(); + } + } + + if (d->animator) { + const QVariant fetchState = index.data(Akonadi::EntityTreeModel::FetchStateRole); + if (!fetchState.isValid() || fetchState.toInt() != Akonadi::EntityTreeModel::FetchingState) { + d->animator->pop(index); + return; + } + + d->animator->push(index); + + if (auto v4 = qstyleoption_cast(option)) { + v4->icon = d->animator->sequenceFrame(index); + } + } +} + +class PainterStateSaver +{ +public: + explicit PainterStateSaver(QPainter *painter) + { + mPainter = painter; + mPainter->save(); + } + + ~PainterStateSaver() + { + mPainter->restore(); + } + +private: + Q_DISABLE_COPY(PainterStateSaver) + QPainter *mPainter = nullptr; +}; + +void CollectionStatisticsDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + Q_D(const CollectionStatisticsDelegate); + PainterStateSaver stateSaver(painter); + + const auto textColor = index.data(Qt::ForegroundRole).value(); + // First, paint the basic, but without the text. We remove the text + // in initStyleOption(), which gets called by QStyledItemDelegate::paint(). + QStyledItemDelegate::paint(painter, option, index); + + // Now, we retrieve the correct style option by calling initStyleOption from + // the superclass. + QStyleOptionViewItem option4 = option; + QStyledItemDelegate::initStyleOption(&option4, index); + QString text = option4.text; + + // Now calculate the rectangle for the text + QStyle *s = d->parent->style(); + const QWidget *widget = option4.widget; + const QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &option4, widget); + + // When checking if the item is expanded, we need to check that for the first + // column, as Qt only recognizes the index as expanded for the first column + const QModelIndex firstColumn = index.sibling(index.row(), 0); + auto treeView = qobject_cast(d->parent); + bool expanded = treeView && treeView->isExpanded(firstColumn); + + if (index.data(EntityTreeModel::PendingCutRole).toBool()) { + painter->setPen(option.palette.color(QPalette::Disabled, QPalette::Text)); + } else if (option.state & QStyle::State_Selected) { + painter->setPen(textColor.isValid() ? textColor : option.palette.highlightedText().color()); + } else { + painter->setPen(textColor.isValid() ? textColor : option.palette.text().color()); + } + + auto collection = firstColumn.data(EntityTreeModel::CollectionRole).value(); + + if (!collection.isValid()) { + qCCritical(AKONADIWIDGETS_LOG) << "Invalid collection at index" << firstColumn << firstColumn.data().toString() << "sibling of" << index + << "rowCount=" << index.model()->rowCount(index.parent()) << "parent=" << index.parent().data().toString(); + return; + } + + CollectionStatistics statistics = collection.statistics(); + + qint64 unreadCount = qMax(0LL, statistics.unreadCount()); + qint64 totalRecursiveCount = 0; + qint64 unreadRecursiveCount = 0; + qint64 totalSize = 0; + bool needRecursiveCounts = false; + bool needTotalSize = false; + if ((d->drawUnreadAfterFolder && index.column() == 0) || (index.column() == 1 || index.column() == 2)) { + needRecursiveCounts = true; + } else if (index.column() == 3 && !expanded) { + needTotalSize = true; + } + + if (needRecursiveCounts || needTotalSize) { + d->getCountRecursive(firstColumn, totalRecursiveCount, unreadRecursiveCount, totalSize); + } + + // Draw the unread count after the folder name (in parenthesis) + if (d->drawUnreadAfterFolder && index.column() == 0) { + // Construct the string which will appear after the foldername (with the + // unread count) + QString unread; + // qCDebug(AKONADIWIDGETS_LOG) << expanded << unreadCount << unreadRecursiveCount; + if (expanded && unreadCount > 0) { + unread = QStringLiteral(" (%1)").arg(unreadCount); + } else if (!expanded) { + if (unreadCount != unreadRecursiveCount) { + unread = QStringLiteral(" (%1 + %2)").arg(unreadCount).arg(unreadRecursiveCount - unreadCount); + } else if (unreadCount > 0) { + unread = QStringLiteral(" (%1)").arg(unreadCount); + } + } + + PainterStateSaver stateSaver(painter); + + if (!unread.isEmpty()) { + QFont font = painter->font(); + font.setBold(true); + painter->setFont(font); + } + + const QColor unreadColor = (option.state & QStyle::State_Selected) ? d->mSelectedUnreadColor : d->mDeselectedUnreadColor; + const QRect iconRect = s->subElementRect(QStyle::SE_ItemViewItemDecoration, &option4, widget); + + if (option.decorationPosition == QStyleOptionViewItem::Left || option.decorationPosition == QStyleOptionViewItem::Right) { + // Squeeze the folder text if it is to big and calculate the rectangles + // where the folder text and the unread count will be drawn to + QString folderName = text; + QFontMetrics fm(painter->fontMetrics()); + const int unreadWidth = fm.horizontalAdvance(unread); + int folderWidth(fm.horizontalAdvance(folderName)); + const bool enoughPlaceForText = (option.rect.width() > (folderWidth + unreadWidth + iconRect.width())); + + if (!enoughPlaceForText && (folderWidth + unreadWidth > textRect.width())) { + folderName = fm.elidedText(folderName, Qt::ElideRight, option.rect.width() - unreadWidth - iconRect.width()); + folderWidth = fm.horizontalAdvance(folderName); + } + QRect folderRect = textRect; + QRect unreadRect = textRect; + folderRect.setRight(textRect.left() + folderWidth); + unreadRect = QRect(folderRect.right(), folderRect.top(), unreadWidth, unreadRect.height()); + + // Draw folder name and unread count + painter->drawText(folderRect, Qt::AlignLeft | Qt::AlignVCenter, folderName); + painter->setPen(unreadColor); + painter->drawText(unreadRect, Qt::AlignLeft | Qt::AlignVCenter, unread); + } else if (option.decorationPosition == QStyleOptionViewItem::Top) { + if (unreadCount > 0) { + // draw over the icon + // the iconRect is enlarged to the whole width of the item, in case the text is wider than the underlying icon + painter->setPen(unreadColor); + painter->drawText(QRect(option.rect.x(), iconRect.y(), option.rect.width(), iconRect.height()), Qt::AlignCenter, QString::number(unreadCount)); + } + } + return; + } + + // For the unread/total column, paint the summed up count if the item + // is collapsed + if ((index.column() == 1 || index.column() == 2)) { + QFont savedFont = painter->font(); + QString sumText; + if (index.column() == 1 && ((!expanded && unreadRecursiveCount > 0) || (expanded && unreadCount > 0))) { + QFont font = painter->font(); + font.setBold(true); + painter->setFont(font); + sumText = QString::number(expanded ? unreadCount : unreadRecursiveCount); + } else { + qint64 totalCount = statistics.count(); + if (index.column() == 2 && ((!expanded && totalRecursiveCount > 0) || (expanded && totalCount > 0))) { + sumText = QString::number(expanded ? totalCount : totalRecursiveCount); + } + } + + painter->drawText(textRect, Qt::AlignRight | Qt::AlignVCenter, sumText); + painter->setFont(savedFont); + return; + } + + // total size + if (index.column() == 3 && !expanded) { + painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, KFormat().formatByteSize(totalSize)); + return; + } + + painter->drawText(textRect, option4.displayAlignment | Qt::AlignVCenter, text); +} + +void CollectionStatisticsDelegate::updatePalette() +{ + Q_D(CollectionStatisticsDelegate); + d->updateColor(); +} diff --git a/src/widgets/collectionstatisticsdelegate.h b/src/widgets/collectionstatisticsdelegate.h new file mode 100644 index 0000000..8699568 --- /dev/null +++ b/src/widgets/collectionstatisticsdelegate.h @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2008 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +class QAbstractItemView; +class QTreeView; + +namespace Akonadi +{ +class CollectionStatisticsDelegatePrivate; + +/** + * @short A delegate that draws unread and total count for StatisticsProxyModel. + * + * The delegate provides the following features: + * + * - Collections with unread items will have the foldername and the unread + * column marked in bold. + * - If a folder is collapsed, the unread and the total column will contain + * the total sum of all child folders + * - It has the possibility to draw the unread count directly after the + * foldername, see toggleUnreadAfterFolderName(). + * + * Example: + * @code + * + * Akonadi::EntityTreeView *view = new Akonadi::EntityTreeView( this ); + * + * Akonadi::StatisticsProxyModel *statisticsProxy = new Akonadi::StatisticsProxyModel( view ); + * view->setModel( statisticsProxy ); + * + * Akonadi::CollectionStatisticsDelegate *delegate = new Akonadi::CollectionStatisticsDelegate( view ); + * view->setItemDelegate( delegate ); + * + * @endcode + * + * @note This proxy model is intended to be used on top of the EntityTreeModel. One of the proxies + * between the EntityTreeModel (the root model) and the view must be a StatisticsProxyModel. That + * proxy model may appear anywhere in the chain. + * + * @author Thomas McGuire + */ +class AKONADIWIDGETS_EXPORT CollectionStatisticsDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + /** + * Creates a new collection statistics delegate. + * + * @param parent The parent item view, which will also take ownership. + * + * @since 4.6 + */ + explicit CollectionStatisticsDelegate(QAbstractItemView *parent); + + /** + * Creates a new collection statistics delegate. + * + * @param parent The parent tree view, which will also take ownership. + */ + explicit CollectionStatisticsDelegate(QTreeView *parent); + + /** + * Destroys the collection statistics delegate. + */ + ~CollectionStatisticsDelegate() override; + + /** + * @since 4.9.1 + */ + void updatePalette(); + + /** + * Sets whether the unread count is drawn next to the folder name. + * + * You probably want to enable this when the unread count is hidden only. + * This is disabled by default. + * + * @param enable If @c true, the unread count is drawn next to the folder name, + * if @c false, the folder name will be drawn normally. + */ + void setUnreadCountShown(bool enable); + + /** + * Returns whether the unread count is drawn next to the folder name. + */ + bool unreadCountShown() const; + + /** + * @param enable new mode of progress animation + */ + void setProgressAnimationEnabled(bool enable); + + bool progressAnimationEnabled() const; + +protected: + /** + * @param painter pointer for QPainter to use in method + * @param option style options + * @param index model index (QModelIndex) + */ + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + /** + * @param option style option view item + * @param index model index (QModelIndex) + */ + void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override; + +private: + /// @cond PRIVATE + CollectionStatisticsDelegatePrivate *const d_ptr; + /// @endcond + + Q_DECLARE_PRIVATE(CollectionStatisticsDelegate) +}; + +} + diff --git a/src/widgets/collectionview.cpp b/src/widgets/collectionview.cpp new file mode 100644 index 0000000..0e62a3a --- /dev/null +++ b/src/widgets/collectionview.cpp @@ -0,0 +1,253 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectionview.h" + +#include "akonadiwidgets_debug.h" +#include "collection.h" +#include "controlgui.h" +#include "entitytreemodel.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN CollectionView::Private +{ +public: + explicit Private(CollectionView *parent) + : mParent(parent) + { + } + + void init(); + void dragExpand(); + void itemClicked(const QModelIndex &index); + void itemCurrentChanged(const QModelIndex &index); + bool hasParent(const QModelIndex &idx, Collection::Id parentId) const; + + CollectionView *const mParent; + QModelIndex dragOverIndex; + QTimer dragExpandTimer; + + KXMLGUIClient *xmlGuiClient = nullptr; +}; + +void CollectionView::Private::init() +{ + mParent->header()->setSectionsClickable(true); + mParent->header()->setStretchLastSection(false); + + mParent->setSortingEnabled(true); + mParent->sortByColumn(0, Qt::AscendingOrder); + mParent->setEditTriggers(QAbstractItemView::EditKeyPressed); + mParent->setAcceptDrops(true); + mParent->setDropIndicatorShown(true); + mParent->setDragDropMode(DragDrop); + mParent->setDragEnabled(true); + + dragExpandTimer.setSingleShot(true); + mParent->connect(&dragExpandTimer, &QTimer::timeout, mParent, [this]() { + dragExpand(); + }); + + mParent->connect(mParent, &QAbstractItemView::clicked, mParent, [this](const QModelIndex &mi) { + itemClicked(mi); + }); + + ControlGui::widgetNeedsAkonadi(mParent); +} + +bool CollectionView::Private::hasParent(const QModelIndex &idx, Collection::Id parentId) const +{ + QModelIndex idx2 = idx; + while (idx2.isValid()) { + if (mParent->model()->data(idx2, EntityTreeModel::CollectionIdRole).toLongLong() == parentId) { + return true; + } + + idx2 = idx2.parent(); + } + return false; +} + +void CollectionView::Private::dragExpand() +{ + mParent->setExpanded(dragOverIndex, true); + dragOverIndex = QModelIndex(); +} + +void CollectionView::Private::itemClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value(); + if (!collection.isValid()) { + return; + } + + Q_EMIT mParent->clicked(collection); +} + +void CollectionView::Private::itemCurrentChanged(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value(); + if (!collection.isValid()) { + return; + } + + Q_EMIT mParent->currentChanged(collection); +} + +CollectionView::CollectionView(QWidget *parent) + : QTreeView(parent) + , d(new Private(this)) +{ + d->init(); +} + +CollectionView::CollectionView(KXMLGUIClient *xmlGuiClient, QWidget *parent) + : QTreeView(parent) + , d(new Private(this)) +{ + d->xmlGuiClient = xmlGuiClient; + d->init(); +} + +CollectionView::~CollectionView() +{ + delete d; +} + +void CollectionView::setModel(QAbstractItemModel *model) +{ + QTreeView::setModel(model); + header()->setStretchLastSection(true); + + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex &mi) { + d->itemCurrentChanged(mi); + }); +} + +void CollectionView::dragMoveEvent(QDragMoveEvent *event) +{ + QModelIndex index = indexAt(event->pos()); + if (d->dragOverIndex != index) { + d->dragExpandTimer.stop(); + if (index.isValid() && !isExpanded(index) && itemsExpandable()) { + d->dragExpandTimer.start(QApplication::startDragTime()); + d->dragOverIndex = index; + } + } + + // Check if the collection under the cursor accepts this data type + const QStringList supportedContentTypes = model()->data(index, EntityTreeModel::CollectionRole).value().contentMimeTypes(); + const QMimeData *mimeData = event->mimeData(); + if (!mimeData) { + return; + } + const QList urls = mimeData->urls(); + for (const QUrl &url : urls) { + const Collection collection = Collection::fromUrl(url); + if (collection.isValid()) { + if (!supportedContentTypes.contains(QLatin1String("inode/directory"))) { + break; + } + + // Check if we don't try to drop on one of the children + if (d->hasParent(index, collection.id())) { + break; + } + } else { + const QList> query = QUrlQuery(url).queryItems(); + const int numberOfQuery(query.count()); + for (int i = 0; i < numberOfQuery; ++i) { + if (query.at(i).first == QLatin1String("type")) { + const QString type = query.at(i).second; + if (!supportedContentTypes.contains(type)) { + break; + } + } + } + } + + QTreeView::dragMoveEvent(event); + return; + } + + event->setDropAction(Qt::IgnoreAction); +} + +void CollectionView::dragLeaveEvent(QDragLeaveEvent *event) +{ + d->dragExpandTimer.stop(); + d->dragOverIndex = QModelIndex(); + QTreeView::dragLeaveEvent(event); +} + +void CollectionView::dropEvent(QDropEvent *event) +{ + d->dragExpandTimer.stop(); + d->dragOverIndex = QModelIndex(); + + // open a context menu offering different drop actions (move, copy and cancel) + // TODO If possible, hide non available actions ... + QMenu popup(this); + QAction *moveDropAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("&Move here")); + QAction *copyDropAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy here")); + popup.addSeparator(); + popup.addAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18n("Cancel")); + + QAction *activatedAction = popup.exec(QCursor::pos()); + if (activatedAction == moveDropAction) { + event->setDropAction(Qt::MoveAction); + } else if (activatedAction == copyDropAction) { + event->setDropAction(Qt::CopyAction); + } else { + return; + } + + QTreeView::dropEvent(event); +} + +void CollectionView::contextMenuEvent(QContextMenuEvent *event) +{ + if (!d->xmlGuiClient) { + return; + } + QMenu *popup = static_cast(d->xmlGuiClient->factory()->container(QStringLiteral("akonadi_collectionview_contextmenu"), d->xmlGuiClient)); + if (popup) { + popup->exec(event->globalPos()); + } +} + +void CollectionView::setXmlGuiClient(KXMLGUIClient *xmlGuiClient) +{ + d->xmlGuiClient = xmlGuiClient; +} + +#include "moc_collectionview.cpp" diff --git a/src/widgets/collectionview.h b/src/widgets/collectionview.h new file mode 100644 index 0000000..10ddd7a --- /dev/null +++ b/src/widgets/collectionview.h @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include + +class KXMLGUIClient; +class KXmlGuiWindow; +class QDragMoveEvent; + +namespace Akonadi +{ +class Collection; + +/** + * @short A view to show a collection tree provided by a EntityTreeModel. + * + * When a KXmlGuiWindow is passed to the constructor, the XMLGUI + * defined context menu @c akonadi_collectionview_contextmenu is + * used if available. + * + * Example: + * + * @code + * + * class MyWindow : public KXmlGuiWindow + * { + * public: + * MyWindow() + * : KXmlGuiWindow() + * { + * Akonadi::CollectionView *view = new Akonadi::CollectionView(this, this); + * setCentralWidget(view); + * + * Akonadi::Monitor *monitor = new Akonadi::Monitor(this); + * Akonadi::EntityTreeModel *model = new Akonadi::EntityTreeModel(monitor, this); + * view->setModel(model); + * } + * } + * + * @endcode + * + * @deprecated Use EntityTreeView or EntityListView on top of EntityTreeModel instead. + * + * @author Volker Krause + */ +class AKONADIWIDGETS_DEPRECATED_EXPORT CollectionView : public QTreeView +{ + Q_OBJECT + +public: + /** + * Creates a new collection view. + * + * @param parent The parent widget. + */ + explicit CollectionView(QWidget *parent = nullptr); + + /** + * Creates a new collection view. + * + * @param xmlGuiClient The KXmlGuiClient the view is used in. + * This is needed for the XMLGUI based context menu. + * Passing 0 is ok and will disable the builtin context menu. + * @param parent The parent widget. + */ + explicit CollectionView(KXMLGUIClient *xmlGuiClient, QWidget *parent = nullptr); + + /** + * Destroys the collection view. + */ + ~CollectionView() override; + + /** + * Sets the KXMLGUIClient which the view is used in. + * This is needed if you want to use the built-in context menu. + * + * @param xmlGuiClient The KXMLGUIClient the view is used in. + * @since 4.3 + */ + void setXmlGuiClient(KXMLGUIClient *xmlGuiClient); + + void setModel(QAbstractItemModel *model) override; + +Q_SIGNALS: + /** + * This signal is emitted whenever the user has clicked + * a collection in the view. + * + * @param collection The clicked collection. + */ + void clicked(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the current collection + * in the view has changed. + * + * @param collection The new current collection. + */ + void currentChanged(const Akonadi::Collection &collection); + +protected: + using QTreeView::currentChanged; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void itemClicked(const QModelIndex &)) + Q_PRIVATE_SLOT(d, void itemCurrentChanged(const QModelIndex &)) + /// @endcond +}; + +} + diff --git a/src/widgets/conflictresolvedialog.cpp b/src/widgets/conflictresolvedialog.cpp new file mode 100644 index 0000000..ff6883e --- /dev/null +++ b/src/widgets/conflictresolvedialog.cpp @@ -0,0 +1,309 @@ +/* + SPDX-FileCopyrightText: 2010 KDAB + SPDX-FileContributor: Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "conflictresolvedialog_p.h" + +#include "abstractdifferencesreporter.h" +#include "differencesalgorithminterface.h" +#include "typepluginloader_p.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; +using namespace AkRanges; + +static inline QString textToHTML(const QString &text) +{ + return Qt::convertFromPlainText(text); +} + +class HtmlDifferencesReporter : public AbstractDifferencesReporter +{ +public: + HtmlDifferencesReporter() = default; + + QString toHtml() const + { + return header() + mContent + footer(); + } + + QString plainText() const + { + return mTextContent; + } + + void setPropertyNameTitle(const QString &title) override + { + mNameTitle = title; + } + + void setLeftPropertyValueTitle(const QString &title) override + { + mLeftTitle = title; + } + + void setRightPropertyValueTitle(const QString &title) override + { + mRightTitle = title; + } + + void addProperty(Mode mode, const QString &name, const QString &leftValue, const QString &rightValue) override + { + switch (mode) { + case NormalMode: + mContent.append(QStringLiteral("%1:%2%3") + .arg(name, textToHTML(leftValue), textToHTML(rightValue))); + mTextContent.append(QStringLiteral("%1:\n%2\n%3\n\n").arg(name, leftValue, rightValue)); + break; + case ConflictMode: + mContent.append( + QStringLiteral("%1:%2%3") + .arg(name, textToHTML(leftValue), textToHTML(rightValue))); + mTextContent.append(QStringLiteral("%1:\n%2\n%3\n\n").arg(name, leftValue, rightValue)); + break; + case AdditionalLeftMode: + mContent.append(QStringLiteral("%1:%2") + .arg(name, textToHTML(leftValue))); + mTextContent.append(QStringLiteral("%1:\n%2\n\n").arg(name, leftValue)); + break; + case AdditionalRightMode: + mContent.append(QStringLiteral("%1:%2") + .arg(name, textToHTML(rightValue))); + mTextContent.append(QStringLiteral("%1:\n%2\n\n").arg(name, rightValue)); + break; + } + } + +private: + QString header() const + { + QString header = QStringLiteral(""); + header += QStringLiteral("") + .arg(KColorScheme(QPalette::Active, KColorScheme::View).foreground().color().name(), + KColorScheme(QPalette::Active, KColorScheme::View).background().color().name()); + header += QLatin1String("
"); + header += QStringLiteral("") + .arg(mNameTitle, mLeftTitle, mRightTitle); + + return header; + } + + QString footer() const + { + return QStringLiteral( + "
%1%2 %3
" + "" + ""); + } + + QString mContent; + QString mNameTitle; + QString mLeftTitle; + QString mRightTitle; + QString mTextContent; +}; + +static void compareItems(AbstractDifferencesReporter *reporter, const Akonadi::Item &localItem, const Akonadi::Item &otherItem) +{ + if (localItem.modificationTime() != otherItem.modificationTime()) { + reporter->addProperty(AbstractDifferencesReporter::ConflictMode, + i18n("Modification Time"), + QLocale().toString(localItem.modificationTime(), QLocale::ShortFormat), + QLocale().toString(otherItem.modificationTime(), QLocale::ShortFormat)); + } + + if (localItem.flags() != otherItem.flags()) { + const auto toQString = [](const QByteArray &s) { + return QString::fromUtf8(s); + }; + const auto localFlags = localItem.flags() | Views::transform(toQString) | Actions::toQList; + const auto otherFlags = otherItem.flags() | Views::transform(toQString) | Actions::toQList; + reporter->addProperty(AbstractDifferencesReporter::ConflictMode, + i18n("Flags"), + localFlags.join(QLatin1String(", ")), + otherFlags.join(QLatin1String(", "))); + } + + const auto toPair = [](Attribute *attr) { + return std::pair{attr->type(), attr->serialized()}; + }; + const auto localAttributes = localItem.attributes() | Views::transform(toPair) | Actions::toQHash; + const auto otherAttributes = otherItem.attributes() | Views::transform(toPair) | Actions::toQHash; + + if (localAttributes != otherAttributes) { + for (const QByteArray &localKey : localAttributes) { + if (!otherAttributes.contains(localKey)) { + reporter->addProperty(AbstractDifferencesReporter::AdditionalLeftMode, + i18n("Attribute: %1", QString::fromUtf8(localKey)), + QString::fromUtf8(localAttributes.value(localKey)), + QString()); + } else { + const QByteArray localValue = localAttributes.value(localKey); + const QByteArray otherValue = otherAttributes.value(localKey); + if (localValue != otherValue) { + reporter->addProperty(AbstractDifferencesReporter::ConflictMode, + i18n("Attribute: %1", QString::fromUtf8(localKey)), + QString::fromUtf8(localValue), + QString::fromUtf8(otherValue)); + } + } + } + + for (const QByteArray &otherKey : otherAttributes) { + if (!localAttributes.contains(otherKey)) { + reporter->addProperty(AbstractDifferencesReporter::AdditionalRightMode, + i18n("Attribute: %1", QString::fromUtf8(otherKey)), + QString(), + QString::fromUtf8(otherAttributes.value(otherKey))); + } + } + } +} + +ConflictResolveDialog::ConflictResolveDialog(QWidget *parent) + : QDialog(parent) + , mResolveStrategy(ConflictHandler::UseBothItems) +{ + setWindowTitle(i18nc("@title:window", "Conflict Resolution")); + + auto mainLayout = new QVBoxLayout(this); + // Don't use QDialogButtonBox, order is very important (left on the left, right on the right) + auto buttonLayout = new QHBoxLayout(); + auto takeLeftButton = new QPushButton(this); + takeLeftButton->setText(i18nc("@action:button", "Take my version")); + connect(takeLeftButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseLocalItemChoosen); + buttonLayout->addWidget(takeLeftButton); + takeLeftButton->setObjectName(QStringLiteral("takeLeftButton")); + + auto takeRightButton = new QPushButton(this); + takeRightButton->setText(i18nc("@action:button", "Take their version")); + takeRightButton->setObjectName(QStringLiteral("takeRightButton")); + connect(takeRightButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseOtherItemChoosen); + buttonLayout->addWidget(takeRightButton); + + auto keepBothButton = new QPushButton(this); + keepBothButton->setText(i18nc("@action:button", "Keep both versions")); + keepBothButton->setObjectName(QStringLiteral("keepBothButton")); + buttonLayout->addWidget(keepBothButton); + connect(keepBothButton, &QPushButton::clicked, this, &ConflictResolveDialog::slotUseBothItemsChoosen); + + keepBothButton->setDefault(true); + + mView = new QTextBrowser(this); + mView->setObjectName(QStringLiteral("view")); + mView->setOpenLinks(false); + + auto docuLabel = + new QLabel(i18n("Your changes conflict with those made by someone else meanwhile.
" + "Unless one version can just be thrown away, you will have to integrate those changes manually.
" + "Click on
\"Open text editor\" to keep a copy of the texts, then select which version is most correct, " + "then re-open it and modify it again to add what's missing.")); + connect(docuLabel, &QLabel::linkActivated, this, &ConflictResolveDialog::slotOpenEditor); + docuLabel->setContextMenuPolicy(Qt::NoContextMenu); + + docuLabel->setWordWrap(true); + docuLabel->setObjectName(QStringLiteral("doculabel")); + + mainLayout->addWidget(mView); + mainLayout->addWidget(docuLabel); + mainLayout->addLayout(buttonLayout); + + // default size is tiny, and there's usually lots of text, so make it much bigger + create(); // ensure a window is created + const QSize availableSize = windowHandle()->screen()->availableSize(); + windowHandle()->resize(static_cast(availableSize.width() * 0.7), static_cast(availableSize.height() * 0.5)); + KWindowConfig::restoreWindowSize(windowHandle(), KSharedConfig::openConfig()->group("ConflictResolveDialog")); + resize(windowHandle()->size()); // workaround for QTBUG-40584 +} + +ConflictResolveDialog::~ConflictResolveDialog() +{ + KConfigGroup group(KSharedConfig::openConfig()->group("ConflictResolveDialog")); + KWindowConfig::saveWindowSize(windowHandle(), group); +} + +void ConflictResolveDialog::setConflictingItems(const Akonadi::Item &localItem, const Akonadi::Item &otherItem) +{ + mLocalItem = localItem; + mOtherItem = otherItem; + + HtmlDifferencesReporter reporter; + compareItems(&reporter, localItem, otherItem); + + if (mLocalItem.hasPayload() && mOtherItem.hasPayload()) { + QObject *object = TypePluginLoader::objectForMimeTypeAndClass(localItem.mimeType(), localItem.availablePayloadMetaTypeIds()); + if (object) { + DifferencesAlgorithmInterface *algorithm = qobject_cast(object); + if (algorithm) { + algorithm->compare(&reporter, localItem, otherItem); + mView->setHtml(reporter.toHtml()); + mTextContent = reporter.plainText(); + return; + } + } + + reporter.addProperty(HtmlDifferencesReporter::NormalMode, + i18n("Data"), + QString::fromUtf8(mLocalItem.payloadData()), + QString::fromUtf8(mOtherItem.payloadData())); + } + + mView->setHtml(reporter.toHtml()); + mTextContent = reporter.plainText(); +} + +void ConflictResolveDialog::slotOpenEditor() +{ + QTemporaryFile file(QDir::tempPath() + QStringLiteral("/akonadi-XXXXXX.txt")); + if (file.open()) { + file.setAutoRemove(false); + file.write(mTextContent.toLocal8Bit()); + const QString fileName = file.fileName(); + file.close(); + QDesktopServices::openUrl(QUrl::fromLocalFile(fileName)); + } +} + +ConflictHandler::ResolveStrategy ConflictResolveDialog::resolveStrategy() const +{ + return mResolveStrategy; +} + +void ConflictResolveDialog::slotUseLocalItemChoosen() +{ + mResolveStrategy = ConflictHandler::UseLocalItem; + accept(); +} + +void ConflictResolveDialog::slotUseOtherItemChoosen() +{ + mResolveStrategy = ConflictHandler::UseOtherItem; + accept(); +} + +void ConflictResolveDialog::slotUseBothItemsChoosen() +{ + mResolveStrategy = ConflictHandler::UseBothItems; + accept(); +} + +#include "moc_conflictresolvedialog_p.cpp" diff --git a/src/widgets/conflictresolvedialog_p.h b/src/widgets/conflictresolvedialog_p.h new file mode 100644 index 0000000..72bbfd2 --- /dev/null +++ b/src/widgets/conflictresolvedialog_p.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2010 KDAB + SPDX-FileContributor: Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "akonadiwidgetstests_export.h" +#include "conflicthandler_p.h" + +class QTextBrowser; + +namespace Akonadi +{ +/** + * @short A dialog to ask the user for a resolve strategy for conflicts. + * + * @author Tobias Koenig + */ +class AKONADIWIDGET_TESTS_EXPORT ConflictResolveDialog : public QDialog +{ + Q_OBJECT + +public: + /** + * Creates a new conflict resolve dialog. + * + * @param parent The parent widget. + */ + explicit ConflictResolveDialog(QWidget *parent = nullptr); + + ~ConflictResolveDialog(); + + /** + * Sets the items that causes the conflict. + * + * @param localItem The local item which causes the conflict. + * @param otherItem The conflicting item from the Akonadi storage. + * + * @note Both items need the full payload set. + */ + void setConflictingItems(const Akonadi::Item &localItem, const Akonadi::Item &otherItem); + + /** + * Returns the resolve strategy the user choose. + */ + Q_REQUIRED_RESULT ConflictHandler::ResolveStrategy resolveStrategy() const; + +private Q_SLOTS: + void slotUseLocalItemChoosen(); + void slotUseOtherItemChoosen(); + void slotUseBothItemsChoosen(); + void slotOpenEditor(); + +private: + ConflictHandler::ResolveStrategy mResolveStrategy; + + Akonadi::Item mLocalItem; + Akonadi::Item mOtherItem; + + QTextBrowser *mView = nullptr; + QString mTextContent; +}; + +} + diff --git a/src/widgets/controlgui.cpp b/src/widgets/controlgui.cpp new file mode 100644 index 0000000..ad8268d --- /dev/null +++ b/src/widgets/controlgui.cpp @@ -0,0 +1,262 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "controlgui.h" +#include "akonadiwidgets_debug.h" +#include "erroroverlay_p.h" +#include "selftestdialog.h" +#include "servermanager.h" +#include "ui_controlprogressindicator.h" + +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +namespace Internal +{ +class ControlProgressIndicator : public QFrame +{ + Q_OBJECT +public: + explicit ControlProgressIndicator(QWidget *parent = nullptr) + : QFrame(parent) + { + setWindowModality(Qt::ApplicationModal); + resize(400, 100); + setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + ui.setupUi(this); + + setFrameShadow(QFrame::Plain); + setFrameShape(QFrame::Box); + } + + void setMessage(const QString &msg) + { + ui.statusLabel->setText(msg); + } + + Ui::ControlProgressIndicator ui; +}; + +class StaticControlGui : public ControlGui +{ + Q_OBJECT +}; + +} // namespace Internal + +Q_GLOBAL_STATIC(Internal::StaticControlGui, s_instance) // NOLINT(readability-redundant-member-init) + +/** + * @internal + */ +class Q_DECL_HIDDEN ControlGui::Private +{ +public: + explicit Private(ControlGui *parent) + : mParent(parent) + , mEventLoop(nullptr) + , mProgressIndicator(nullptr) + , mSuccess(false) + , mStarting(false) + , mStopping(false) + { + } + + ~Private() + { + delete mProgressIndicator; + } + + void setupProgressIndicator(const QString &msg, QWidget *parent = nullptr) + { + if (!mProgressIndicator) { + mProgressIndicator = new Internal::ControlProgressIndicator(parent); + } + + mProgressIndicator->setMessage(msg); + } + + void createErrorOverlays() + { + for (QWidget *widget : std::as_const(mPendingOverlays)) { + if (widget) { + new ErrorOverlay(widget); + } + } + mPendingOverlays.clear(); + } + + void cleanup() + { + // delete s_instance; + } + + bool exec(); + void serverStateChanged(ServerManager::State state); + + QPointer mParent; + QEventLoop *mEventLoop = nullptr; + QPointer mProgressIndicator; + QList> mPendingOverlays; + bool mSuccess; + + bool mStarting; + bool mStopping; +}; + +bool ControlGui::Private::exec() +{ + if (mProgressIndicator) { + mProgressIndicator->show(); + } + qCDebug(AKONADIWIDGETS_LOG) << "Starting/Stopping Akonadi (using an event loop)."; + mEventLoop = new QEventLoop(mParent); + mEventLoop->exec(); + mEventLoop->deleteLater(); + mEventLoop = nullptr; + + if (!mSuccess) { + qCWarning(AKONADIWIDGETS_LOG) << "Could not start/stop Akonadi!"; + if (mProgressIndicator && mStarting) { + QPointer dlg = new SelfTestDialog(mProgressIndicator->parentWidget()); + dlg->exec(); + delete dlg; + if (!mParent) { + return false; + } + } + } + + delete mProgressIndicator; + mProgressIndicator = nullptr; + mStarting = false; + mStopping = false; + + const bool rv = mSuccess; + mSuccess = false; + return rv; +} + +void ControlGui::Private::serverStateChanged(ServerManager::State state) +{ + qCDebug(AKONADIWIDGETS_LOG) << "Server state changed to" << state; + if (mEventLoop && mEventLoop->isRunning()) { + // ignore transient states going into the right direction + if ((mStarting && (state == ServerManager::Starting || state == ServerManager::Upgrading)) || (mStopping && state == ServerManager::Stopping)) { + return; + } + mEventLoop->quit(); + mSuccess = (mStarting && state == ServerManager::Running) || (mStopping && state == ServerManager::NotRunning); + } +} + +ControlGui::ControlGui() + : d(new Private(this)) +{ + connect(ServerManager::self(), &ServerManager::stateChanged, this, [this](Akonadi::ServerManager::State state) { + d->serverStateChanged(state); + }); + // mProgressIndicator is a widget, so it better be deleted before the QApplication is deleted + // Otherwise we get a crash in QCursor code with Qt-4.5 + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { + d->cleanup(); + }); + } +} + +ControlGui::~ControlGui() +{ + delete d; +} + +bool ControlGui::start() +{ + if (ServerManager::state() == ServerManager::Stopping) { + qCDebug(AKONADIWIDGETS_LOG) << "Server is currently being stopped, wont try to start it now"; + return false; + } + if (ServerManager::isRunning() || s_instance->d->mEventLoop) { + qCDebug(AKONADIWIDGETS_LOG) << "Server is already running"; + return true; + } + s_instance->d->mStarting = true; + if (!ServerManager::start()) { + qCDebug(AKONADIWIDGETS_LOG) << "ServerManager::start failed -> return false"; + return false; + } + return s_instance->d->exec(); +} + +bool ControlGui::stop() +{ + if (ServerManager::state() == ServerManager::Starting) { + return false; + } + if (!ServerManager::isRunning() || s_instance->d->mEventLoop) { + return true; + } + s_instance->d->mStopping = true; + if (!ServerManager::stop()) { + return false; + } + return s_instance->d->exec(); +} + +bool ControlGui::restart() +{ + if (ServerManager::isRunning()) { + if (!stop()) { + return false; + } + } + return start(); +} + +bool ControlGui::start(QWidget *parent) +{ + s_instance->d->setupProgressIndicator(i18n("Starting Akonadi server..."), parent); + return start(); +} + +bool ControlGui::stop(QWidget *parent) +{ + s_instance->d->setupProgressIndicator(i18n("Stopping Akonadi server..."), parent); + return stop(); +} + +bool ControlGui::restart(QWidget *parent) +{ + if (ServerManager::isRunning()) { + if (!stop(parent)) { + return false; + } + } + return start(parent); +} + +void ControlGui::widgetNeedsAkonadi(QWidget *widget) +{ + s_instance->d->mPendingOverlays.append(widget); + // delay the overlay creation since we rely on widget being reparented + // correctly already + QTimer::singleShot(0, s_instance, []() { + s_instance->d->createErrorOverlays(); + }); +} + +} // namespace Akonadi + +#include "controlgui.moc" diff --git a/src/widgets/controlgui.h b/src/widgets/controlgui.h new file mode 100644 index 0000000..d5421b2 --- /dev/null +++ b/src/widgets/controlgui.h @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +namespace Akonadi +{ +/** + * @short Provides methods to ControlGui the Akonadi server process. + * + * This class provides synchronous methods (ie. use a sub-eventloop) + * to ControlGui the Akonadi service. For asynchronous methods see + * Akonadi::ServerManager. + * + * The most important method in here is widgetNeedsAkonadi(). It is + * recommended to call it with every top-level widget of your application + * as argument, assuming your application relies on Akonadi being operational + * of course. + * + * While the Akonadi server automatically started by Akonadi::Session + * on first use, it might be necessary for some use-cases to guarantee + * a running Akonadi service at some point. This can be done using + * start(). + * + * Example: + * + * @code + * + * if ( !Akonadi::ControlGui::start() ) { + * qDebug() << "Unable to start Akonadi server, exit application"; + * return 1; + * } else { + * ... + * } + * + * @endcode + * + * @author Volker Krause + * + * @see Akonadi::ServerManager + */ +class AKONADIWIDGETS_EXPORT ControlGui : public QObject +{ + Q_OBJECT + +public: + /** + * Destroys the ControlGui object. + */ + ~ControlGui(); + + /** + * Starts the Akonadi server synchronously if it is not already running. + * @return @c true if the server was started successfully or was already + * running, @c false otherwise + */ + static bool start(); + + /** + * Same as start(), but with GUI feedback. + * @param parent The parent widget. + * @since 4.2 + */ + static bool start(QWidget *parent); + + /** + * Stops the Akonadi server synchronously if it is currently running. + * @return @c true if the server was shutdown successfully or was + * not running at all, @c false otherwise. + * @since 4.2 + */ + static bool stop(); + + /** + * Same as stop(), but with GUI feedback. + * @param parent The parent widget. + * @since 4.2 + */ + static bool stop(QWidget *parent); + + /** + * Restarts the Akonadi server synchronously. + * @return @c true if the restart was successful, @c false otherwise, + * the server state is undefined in this case. + * @since 4.2 + */ + static bool restart(); + + /** + * Same as restart(), but with GUI feedback. + * @param parent The parent widget. + * @since 4.2 + */ + static bool restart(QWidget *parent); + + /** + * Disable the given widget when Akonadi is not operational and show + * an error overlay (given enough space). Cascading use is automatically + * detected and resolved. + * @param widget The widget depending on Akonadi being operational. + * @since 4.2 + */ + static void widgetNeedsAkonadi(QWidget *widget); + +protected: + /** + * Creates the ControlGui object. + */ + ControlGui(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/controlprogressindicator.ui b/src/widgets/controlprogressindicator.ui new file mode 100644 index 0000000..72f43b5 --- /dev/null +++ b/src/widgets/controlprogressindicator.ui @@ -0,0 +1,37 @@ + + ControlProgressIndicator + + + + 0 + 0 + 400 + 52 + + + + + + + TextLabel + + + + + + + 0 + + + 775 + + + false + + + + + + + + diff --git a/src/widgets/dragdropmanager.cpp b/src/widgets/dragdropmanager.cpp new file mode 100644 index 0000000..bafa3e3 --- /dev/null +++ b/src/widgets/dragdropmanager.cpp @@ -0,0 +1,324 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "akonadiwidgets_debug.h" +#include "collectionutils.h" +#include "dragdropmanager_p.h" +#include "specialcollectionattribute.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "collection.h" +#include "entitytreemodel.h" + +using namespace Akonadi; + +DragDropManager::DragDropManager(QAbstractItemView *view) + : m_view(view) +{ +} + +Akonadi::Collection DragDropManager::currentDropTarget(QDropEvent *event) const +{ + const QModelIndex index = m_view->indexAt(event->pos()); + auto collection = m_view->model()->data(index, EntityTreeModel::CollectionRole).value(); + if (!collection.isValid()) { + const Item item = m_view->model()->data(index, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + collection = m_view->model()->data(index.parent(), EntityTreeModel::CollectionRole).value(); + } + } + + return collection; +} + +bool DragDropManager::dropAllowed(QDragMoveEvent *event) const +{ + // Check if the collection under the cursor accepts this data type + const Collection targetCollection = currentDropTarget(event); + if (targetCollection.isValid()) { + const QStringList supportedContentTypes = targetCollection.contentMimeTypes(); + + const QMimeData *data = event->mimeData(); + if (!data) { + return false; + } + const QList urls = data->urls(); + for (const QUrl &url : urls) { + const Collection collection = Collection::fromUrl(url); + if (collection.isValid()) { + if (!supportedContentTypes.contains(Collection::mimeType()) && !supportedContentTypes.contains(Collection::virtualMimeType())) { + break; + } + + // Check if we don't try to drop on one of the children + if (hasAncestor(m_view->indexAt(event->pos()), collection.id())) { + break; + } + } else { // This is an item. + const QList> query = QUrlQuery(url).queryItems(); + for (int i = 0; i < query.count(); ++i) { + if (query.at(i).first == QLatin1String("type")) { + const QString type = query.at(i).second; + if (!supportedContentTypes.contains(type)) { + break; + } + } + } + } + return true; + } + } + + return false; +} + +bool DragDropManager::hasAncestor(const QModelIndex &_index, Collection::Id parentId) const +{ + QModelIndex index(_index); + while (index.isValid()) { + if (m_view->model()->data(index, EntityTreeModel::CollectionIdRole).toLongLong() == parentId) { + return true; + } + + index = index.parent(); + } + + return false; +} + +bool DragDropManager::processDropEvent(QDropEvent *event, bool &menuCanceled, bool dropOnItem) +{ + const Collection targetCollection = currentDropTarget(event); + if (!targetCollection.isValid()) { + return false; + } + + if (!mIsManualSortingActive && !dropOnItem) { + return false; + } + + const QMimeData *data = event->mimeData(); + if (!data) { + return false; + } + const QList urls = data->urls(); + for (const QUrl &url : urls) { + const Collection collection = Collection::fromUrl(url); + if (!collection.isValid()) { + if (!dropOnItem) { + return false; + } + } + } + + int actionCount = 0; + Qt::DropAction defaultAction; + // TODO check if the source supports moving + + bool moveAllowed; + bool copyAllowed; + bool linkAllowed; + moveAllowed = copyAllowed = linkAllowed = false; + + if ((targetCollection.rights() & (Collection::CanCreateCollection | Collection::CanCreateItem)) && (event->possibleActions() & Qt::MoveAction)) { + moveAllowed = true; + } + if ((targetCollection.rights() & (Collection::CanCreateCollection | Collection::CanCreateItem)) && (event->possibleActions() & Qt::CopyAction)) { + copyAllowed = true; + } + + if ((targetCollection.rights() & Collection::CanLinkItem) && (event->possibleActions() & Qt::LinkAction)) { + linkAllowed = true; + } + + if (mIsManualSortingActive && !dropOnItem) { + moveAllowed = true; + copyAllowed = false; + linkAllowed = false; + } + + if (!moveAllowed && !copyAllowed && !linkAllowed) { + qCDebug(AKONADIWIDGETS_LOG) << "Cannot drop here:" << event->possibleActions() << m_view->model()->supportedDragActions() + << m_view->model()->supportedDropActions(); + return false; + } + + // first check whether the user pressed a modifier key to select a specific action + if ((QApplication::keyboardModifiers() & Qt::ControlModifier) && (QApplication::keyboardModifiers() & Qt::ShiftModifier)) { + if (linkAllowed) { + defaultAction = Qt::LinkAction; + actionCount = 1; + } else { + return false; + } + } else if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) { + if (copyAllowed) { + defaultAction = Qt::CopyAction; + actionCount = 1; + } else { + return false; + } + } else if ((QApplication::keyboardModifiers() & Qt::ShiftModifier)) { + if (moveAllowed) { + defaultAction = Qt::MoveAction; + actionCount = 1; + } else { + return false; + } + } + + if (actionCount == 1) { + qCDebug(AKONADIWIDGETS_LOG) << "Selecting drop action" << defaultAction << ", there are no other possibilities"; + event->setDropAction(defaultAction); + return true; + } + + if (!mShowDropActionMenu) { + if (moveAllowed) { + defaultAction = Qt::MoveAction; + } else if (copyAllowed) { + defaultAction = Qt::CopyAction; + } else if (linkAllowed) { + defaultAction = Qt::LinkAction; + } else { + return false; + } + event->setDropAction(defaultAction); + return true; + } + + // otherwise show up a menu to allow the user to select an action + QMenu popup(m_view); + QAction *moveDropAction = nullptr; + QAction *copyDropAction = nullptr; + QAction *linkAction = nullptr; + QString sequence; + + if (moveAllowed) { + sequence = QKeySequence(Qt::ShiftModifier).toString(); + sequence.chop(1); // chop superfluous '+' + moveDropAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-move"), QIcon::fromTheme(QStringLiteral("go-jump"))), + i18n("&Move Here") + QLatin1Char('\t') + sequence); + } + + if (copyAllowed) { + sequence = QKeySequence(Qt::ControlModifier).toString(); + sequence.chop(1); // chop superfluous '+' + copyDropAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("&Copy Here") + QLatin1Char('\t') + sequence); + } + + if (linkAllowed) { + sequence = QKeySequence(Qt::ControlModifier | Qt::ShiftModifier).toString(); + sequence.chop(1); // chop superfluous '+' + linkAction = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-link")), i18n("&Link Here") + QLatin1Char('\t') + sequence); + } + + popup.addSeparator(); + QAction *cancelAction = popup.addAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18n("C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString()); + + QAction *activatedAction = popup.exec(m_view->viewport()->mapToGlobal(event->pos())); + if (!activatedAction || (activatedAction == cancelAction)) { + menuCanceled = true; + return false; + } else if (activatedAction == moveDropAction) { + event->setDropAction(Qt::MoveAction); + } else if (activatedAction == copyDropAction) { + event->setDropAction(Qt::CopyAction); + } else if (activatedAction == linkAction) { + event->setDropAction(Qt::LinkAction); + } + return true; +} + +void DragDropManager::startDrag(Qt::DropActions supportedActions) +{ + QModelIndexList indexes; + bool sourceDeletable = true; + const QModelIndexList lstModel = m_view->selectionModel()->selectedRows(); + for (const QModelIndex &index : lstModel) { + if (!m_view->model()->flags(index).testFlag(Qt::ItemIsDragEnabled)) { + continue; + } + + if (sourceDeletable) { + auto source = index.data(EntityTreeModel::CollectionRole).value(); + if (!source.isValid()) { + // index points to an item + source = index.data(EntityTreeModel::ParentCollectionRole).value(); + sourceDeletable = source.rights() & Collection::CanDeleteItem; + } else { + // index points to a collection + sourceDeletable = + (source.rights() & Collection::CanDeleteCollection) && !source.hasAttribute() && !source.isVirtual(); + } + } + indexes.append(index); + } + + if (indexes.isEmpty()) { + return; + } + + QMimeData *mimeData = m_view->model()->mimeData(indexes); + if (!mimeData) { + return; + } + + auto drag = new QDrag(m_view); + drag->setMimeData(mimeData); + if (indexes.size() > 1) { + drag->setPixmap(QIcon::fromTheme(QStringLiteral("document-multiple")).pixmap(QSize(22, 22))); + } else { + QPixmap pixmap = indexes.first().data(Qt::DecorationRole).value().pixmap(QSize(22, 22)); + if (pixmap.isNull()) { + pixmap = QIcon::fromTheme(QStringLiteral("text-plain")).pixmap(QSize(22, 22)); + } + drag->setPixmap(pixmap); + } + + if (!sourceDeletable) { + supportedActions &= ~Qt::MoveAction; + } + + Qt::DropAction defaultAction = Qt::IgnoreAction; + if ((QApplication::keyboardModifiers() & Qt::ControlModifier) && (QApplication::keyboardModifiers() & Qt::ShiftModifier)) { + defaultAction = Qt::LinkAction; + } else if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) { + defaultAction = Qt::CopyAction; + } else if ((QApplication::keyboardModifiers() & Qt::ShiftModifier)) { + defaultAction = Qt::MoveAction; + } + + drag->exec(supportedActions, defaultAction); +} + +bool DragDropManager::showDropActionMenu() const +{ + return mShowDropActionMenu; +} + +void DragDropManager::setShowDropActionMenu(bool show) +{ + mShowDropActionMenu = show; +} + +bool DragDropManager::isManualSortingActive() const +{ + return mIsManualSortingActive; +} + +void DragDropManager::setManualSortingActive(bool active) +{ + mIsManualSortingActive = active; +} diff --git a/src/widgets/dragdropmanager_p.h b/src/widgets/dragdropmanager_p.h new file mode 100644 index 0000000..fd28290 --- /dev/null +++ b/src/widgets/dragdropmanager_p.h @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2009 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collection.h" +#include + +namespace Akonadi +{ +class DragDropManager +{ +public: + explicit DragDropManager(QAbstractItemView *view); + + /** + * @returns True if the drop described in @p event is allowed. + */ + bool dropAllowed(QDragMoveEvent *event) const; + + /** + * Process an attempted drop event. + * + * Attempts to show a popup menu with possible actions for @p event. + * + * @returns True if the event should be further processed, and false otherwise. + */ + bool processDropEvent(QDropEvent *event, bool &menuCanceled, bool dropOnItem = true); + + /** + * Starts a drag if possible and sets the appropriate supported actions to allow moves. + * + * Also sets the pixmap for hte drag to something appropriately small, overriding the Qt + * behaviour of creating a painting of all selected rows when dragging. + */ + void startDrag(Qt::DropActions supportedActions); + + /** + * Sets whether to @p show the drop action menu on drop operation. + */ + void setShowDropActionMenu(bool show); + + /** + * Returns whether the drop action menu is shown on drop operation. + */ + bool showDropActionMenu() const; + + bool isManualSortingActive() const; + + /** + * Set true if we automatic sorting + */ + void setManualSortingActive(bool active); + +private: + Collection currentDropTarget(QDropEvent *event) const; + + bool hasAncestor(const QModelIndex &index, Collection::Id parentId) const; + bool mShowDropActionMenu = true; + bool mIsManualSortingActive = false; + QAbstractItemView *const m_view; +}; + +} + diff --git a/src/widgets/entitylistview.cpp b/src/widgets/entitylistview.cpp new file mode 100644 index 0000000..a90612e --- /dev/null +++ b/src/widgets/entitylistview.cpp @@ -0,0 +1,241 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + SPDX-FileCopyrightText: 2008 Stephen Kelly + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitylistview.h" + +#include "dragdropmanager_p.h" + +#include +#include + +#include "akonadiwidgets_debug.h" +#include +#include + +#include "collection.h" +#include "controlgui.h" +#include "entitytreemodel.h" +#include "item.h" +#include "progressspinnerdelegate_p.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN EntityListView::Private +{ +public: + explicit Private(EntityListView *parent) + : mParent(parent) +#ifndef QT_NO_DRAGANDDROP + , mDragDropManager(new DragDropManager(mParent)) +#endif + { + } + + void init(); + void itemClicked(const QModelIndex &index) const; + void itemDoubleClicked(const QModelIndex &index) const; + void itemCurrentChanged(const QModelIndex &index) const; + + EntityListView *const mParent; + DragDropManager *mDragDropManager = nullptr; + KXMLGUIClient *mXmlGuiClient = nullptr; +}; + +void EntityListView::Private::init() +{ + mParent->setEditTriggers(QAbstractItemView::EditKeyPressed); + mParent->setAcceptDrops(true); +#ifndef QT_NO_DRAGANDDROP + mParent->setDropIndicatorShown(true); + mParent->setDragDropMode(DragDrop); + mParent->setDragEnabled(true); +#endif + mParent->connect(mParent, &QAbstractItemView::clicked, mParent, [this](const auto &index) { + itemClicked(index); + }); + mParent->connect(mParent, &QAbstractItemView::doubleClicked, mParent, [this](const auto &index) { + itemDoubleClicked(index); + }); + + auto animator = new DelegateAnimator(mParent); + auto customDelegate = new ProgressSpinnerDelegate(animator, mParent); + mParent->setItemDelegate(customDelegate); + + ControlGui::widgetNeedsAkonadi(mParent); +} + +void EntityListView::Private::itemClicked(const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + + const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + Q_EMIT mParent->clicked(collection); + } else { + const Item item = index.model()->data(index, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + Q_EMIT mParent->clicked(item); + } + } +} + +void EntityListView::Private::itemDoubleClicked(const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + + const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + Q_EMIT mParent->doubleClicked(collection); + } else { + const Item item = index.model()->data(index, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + Q_EMIT mParent->doubleClicked(item); + } + } +} + +void EntityListView::Private::itemCurrentChanged(const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + + const auto collection = index.model()->data(index, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + Q_EMIT mParent->currentChanged(collection); + } else { + const Item item = index.model()->data(index, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + Q_EMIT mParent->currentChanged(item); + } + } +} + +EntityListView::EntityListView(QWidget *parent) + : QListView(parent) + , d(new Private(this)) +{ + setSelectionMode(QAbstractItemView::SingleSelection); + d->init(); +} + +EntityListView::EntityListView(KXMLGUIClient *xmlGuiClient, QWidget *parent) + : QListView(parent) + , d(new Private(this)) +{ + d->mXmlGuiClient = xmlGuiClient; + d->init(); +} + +EntityListView::~EntityListView() +{ + delete d->mDragDropManager; + delete d; +} + +void EntityListView::setModel(QAbstractItemModel *model) +{ + if (selectionModel()) { + disconnect(selectionModel(), &QItemSelectionModel::currentChanged, this, nullptr); + } + + QListView::setModel(model); + + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const QModelIndex &index) { + d->itemCurrentChanged(index); + }); +} + +#ifndef QT_NO_DRAGANDDROP +void EntityListView::dragMoveEvent(QDragMoveEvent *event) +{ + if (d->mDragDropManager->dropAllowed(event)) { + // All urls are supported. process the event. + QListView::dragMoveEvent(event); + return; + } + + event->setDropAction(Qt::IgnoreAction); +} + +void EntityListView::dropEvent(QDropEvent *event) +{ + bool menuCanceled = false; + if (d->mDragDropManager->processDropEvent(event, menuCanceled) && !menuCanceled) { + QListView::dropEvent(event); + } +} +#endif + +#ifndef QT_NO_CONTEXTMENU +void EntityListView::contextMenuEvent(QContextMenuEvent *event) +{ + if (!d->mXmlGuiClient) { + return; + } + + const QModelIndex index = indexAt(event->pos()); + + QMenu *popup = nullptr; + + // check if the index under the cursor is a collection or item + const auto collection = model()->data(index, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + popup = static_cast(d->mXmlGuiClient->factory()->container(QStringLiteral("akonadi_favoriteview_contextmenu"), d->mXmlGuiClient)); + } else { + popup = + static_cast(d->mXmlGuiClient->factory()->container(QStringLiteral("akonadi_favoriteview_emptyselection_contextmenu"), d->mXmlGuiClient)); + } + + if (popup) { + popup->exec(event->globalPos()); + } +} +#endif + +void EntityListView::setXmlGuiClient(KXMLGUIClient *xmlGuiClient) +{ + d->mXmlGuiClient = xmlGuiClient; +} + +KXMLGUIClient *EntityListView::xmlGuiClient() const +{ + return d->mXmlGuiClient; +} + +#ifndef QT_NO_DRAGANDDROP +void EntityListView::startDrag(Qt::DropActions supportedActions) +{ + d->mDragDropManager->startDrag(supportedActions); +} +#endif + +void EntityListView::setDropActionMenuEnabled(bool enabled) +{ +#ifndef QT_NO_DRAGANDDROP + d->mDragDropManager->setShowDropActionMenu(enabled); +#endif +} + +bool EntityListView::isDropActionMenuEnabled() const +{ +#ifndef QT_NO_DRAGANDDROP + return d->mDragDropManager->showDropActionMenu(); +#else + return false; +#endif +} + +#include "moc_entitylistview.cpp" diff --git a/src/widgets/entitylistview.h b/src/widgets/entitylistview.h new file mode 100644 index 0000000..34bf944 --- /dev/null +++ b/src/widgets/entitylistview.h @@ -0,0 +1,193 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + SPDX-FileCopyrightText: 2008 Stephen Kelly + SPDX-FileCopyrightText: 2009 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +class KXMLGUIClient; +class QDragMoveEvent; + +namespace Akonadi +{ +class Collection; +class Item; + +/** + * @short A view to show an item/collection list provided by an EntityTreeModel. + * + * When a KXmlGuiWindow is passed to the constructor, the XMLGUI + * defined context menu @c akonadi_collectionview_contextmenu or + * @c akonadi_itemview_contextmenu is used if available. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * class MyWindow : public KXmlGuiWindow + * { + * public: + * MyWindow() + * : KXmlGuiWindow() + * { + * EntityListView *view = new EntityListView( this, this ); + * setCentralWidget( view ); + * + * EntityTreeModel *model = new EntityTreeModel( ... ); + * + * KDescendantsProxyModel *flatModel = new KDescendantsProxyModel( this ); + * flatModel->setSourceModel( model ); + * + * view->setModel( flatModel ); + * } + * } + * + * @endcode + * + * @author Volker Krause + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADIWIDGETS_EXPORT EntityListView : public QListView +{ + Q_OBJECT + +public: + /** + * Creates a new favorite collections view. + * + * @param parent The parent widget. + */ + explicit EntityListView(QWidget *parent = nullptr); + + /** + * Creates a new favorite collections view. + * + * @param xmlGuiClient The KXMLGUIClient the view is used in. + * This is needed for the XMLGUI based context menu. + * Passing 0 is ok and will disable the builtin context menu. + * @param parent The parent widget. + */ + explicit EntityListView(KXMLGUIClient *xmlGuiClient, QWidget *parent = nullptr); + + /** + * Destroys the favorite collections view. + */ + ~EntityListView() override; + + /** + * Sets the XML GUI client which the view is used in. + * + * This is needed if you want to use the built-in context menu. + * + * @param xmlGuiClient The KXMLGUIClient the view is used in. + */ + void setXmlGuiClient(KXMLGUIClient *xmlGuiClient); + + /** + * Return the XML GUI client which the view is used in. + * @since 4.12 + */ + KXMLGUIClient *xmlGuiClient() const; + + /** + * @reimp + * @param model the model to set + */ + void setModel(QAbstractItemModel *model) override; + + /** + * Sets whether the drop action menu is @p enabled and will + * be shown on drop operation. + * @param enabled enables drop action menu if set as @c true + * @since 4.7 + */ + void setDropActionMenuEnabled(bool enabled); + + /** + * Returns whether the drop action menu is enabled and will + * be shown on drop operation. + * + * @since 4.7 + */ + Q_REQUIRED_RESULT bool isDropActionMenuEnabled() const; + +Q_SIGNALS: + /** + * This signal is emitted whenever the user has clicked + * a collection in the view. + * + * @param collection The clicked collection. + */ + void clicked(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the user has clicked + * an item in the view. + * + * @param item The clicked item. + */ + void clicked(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the user has double clicked + * a collection in the view. + * + * @param collection The double clicked collection. + */ + void doubleClicked(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the user has double clicked + * an item in the view. + * + * @param item The double clicked item. + */ + void doubleClicked(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the current collection + * in the view has changed. + * + * @param collection The new current collection. + */ + void currentChanged(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the current item + * in the view has changed. + * + * @param item The new current item. + */ + void currentChanged(const Akonadi::Item &item); + +protected: + using QListView::currentChanged; +#ifndef QT_NO_DRAGANDDROP + void startDrag(Qt::DropActions supportedActions) override; + void dropEvent(QDropEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; +#endif + +#ifndef QT_NO_CONTEXTMENU + void contextMenuEvent(QContextMenuEvent *event) override; +#endif + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/entitytreeview.cpp b/src/widgets/entitytreeview.cpp new file mode 100644 index 0000000..831fcc1 --- /dev/null +++ b/src/widgets/entitytreeview.cpp @@ -0,0 +1,327 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + SPDX-FileCopyrightText: 2008 Stephen Kelly + SPDX-FileCopyrightText: 2012-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "entitytreeview.h" + +#include "dragdropmanager_p.h" + +#include +#include +#include +#include +#include + +#include "collection.h" +#include "controlgui.h" +#include "entitytreemodel.h" +#include "item.h" + +#include +#include + +#include "progressspinnerdelegate_p.h" + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN EntityTreeView::Private +{ +public: + explicit Private(EntityTreeView *parent) + : mParent(parent) +#ifndef QT_NO_DRAGANDDROP + , mDragDropManager(new DragDropManager(mParent)) +#endif + , mDefaultPopupMenu(QStringLiteral("akonadi_collectionview_contextmenu")) + { + } + + void init(); + void itemClicked(const QModelIndex &index) const; + void itemDoubleClicked(const QModelIndex &index) const; + void itemCurrentChanged(const QModelIndex &index) const; + + void slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) const; + + EntityTreeView *const mParent; + QBasicTimer mDragExpandTimer; + DragDropManager *mDragDropManager = nullptr; + KXMLGUIClient *mXmlGuiClient = nullptr; + QString mDefaultPopupMenu; +}; + +void EntityTreeView::Private::init() +{ + auto animator = new Akonadi::DelegateAnimator(mParent); + auto customDelegate = new Akonadi::ProgressSpinnerDelegate(animator, mParent); + mParent->setItemDelegate(customDelegate); + + mParent->header()->setSectionsClickable(true); + mParent->header()->setStretchLastSection(false); + // mParent->setRootIsDecorated( false ); + + // QTreeView::autoExpandDelay has very strange behaviour. It toggles the collapse/expand state + // of the item the cursor is currently over when a timer event fires. + // The behaviour we want is to expand a collapsed row on drag-over, but not collapse it. + // mDragExpandTimer is used to achieve this. + // mParent->setAutoExpandDelay ( QApplication::startDragTime() ); + + mParent->setSortingEnabled(true); + mParent->sortByColumn(0, Qt::AscendingOrder); + mParent->setEditTriggers(QAbstractItemView::EditKeyPressed); + mParent->setAcceptDrops(true); +#ifndef QT_NO_DRAGANDDROP + mParent->setDropIndicatorShown(true); + mParent->setDragDropMode(DragDrop); + mParent->setDragEnabled(true); +#endif + + mParent->connect(mParent, &QAbstractItemView::clicked, mParent, [this](const auto &index) { + itemClicked(index); + }); + mParent->connect(mParent, &QAbstractItemView::doubleClicked, mParent, [this](const auto &index) { + itemDoubleClicked(index); + }); + + ControlGui::widgetNeedsAkonadi(mParent); +} + +void EntityTreeView::Private::slotSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) const +{ + Q_UNUSED(deselected) + const int column = 0; + for (const QItemSelectionRange &range : selected) { + const QModelIndex index = range.topLeft(); + + if (index.column() > 0) { + continue; + } + + for (int row = index.row(); row <= range.bottomRight().row(); ++row) { + // Don't use canFetchMore here. We need to bypass the check in + // the EntityFilterModel when it shows only collections. + mParent->model()->fetchMore(index.sibling(row, column)); + } + } + + if (selected.size() == 1) { + const QItemSelectionRange &range = selected.first(); + if (range.topLeft().row() == range.bottomRight().row()) { + mParent->scrollTo(range.topLeft(), QTreeView::EnsureVisible); + } + } +} + +void EntityTreeView::Private::itemClicked(const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + QModelIndex idx = index.sibling(index.row(), 0); + + const auto collection = idx.model()->data(idx, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + Q_EMIT mParent->clicked(collection); + } else { + const Item item = idx.model()->data(idx, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + Q_EMIT mParent->clicked(item); + } + } +} + +void EntityTreeView::Private::itemDoubleClicked(const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + QModelIndex idx = index.sibling(index.row(), 0); + const auto collection = idx.model()->data(idx, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + Q_EMIT mParent->doubleClicked(collection); + } else { + const Item item = idx.model()->data(idx, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + Q_EMIT mParent->doubleClicked(item); + } + } +} + +void EntityTreeView::Private::itemCurrentChanged(const QModelIndex &index) const +{ + if (!index.isValid()) { + return; + } + QModelIndex idx = index.sibling(index.row(), 0); + const auto collection = idx.model()->data(idx, EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + Q_EMIT mParent->currentChanged(collection); + } else { + const Item item = idx.model()->data(idx, EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + Q_EMIT mParent->currentChanged(item); + } + } +} + +EntityTreeView::EntityTreeView(QWidget *parent) + : QTreeView(parent) + , d(new Private(this)) +{ + setSelectionMode(QAbstractItemView::SingleSelection); + d->init(); +} + +EntityTreeView::EntityTreeView(KXMLGUIClient *xmlGuiClient, QWidget *parent) + : QTreeView(parent) + , d(new Private(this)) +{ + d->mXmlGuiClient = xmlGuiClient; + d->init(); +} + +EntityTreeView::~EntityTreeView() +{ + delete d->mDragDropManager; + delete d; +} + +void EntityTreeView::setModel(QAbstractItemModel *model) +{ + if (selectionModel()) { + disconnect(selectionModel(), &QItemSelectionModel::currentChanged, this, nullptr); + disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, nullptr); + } + + QTreeView::setModel(model); + header()->setStretchLastSection(true); + + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const auto &index) { + d->itemCurrentChanged(index); + }); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this](const auto &oldSel, const auto &newSel) { + d->slotSelectionChanged(oldSel, newSel); + }); +} + +void EntityTreeView::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == d->mDragExpandTimer.timerId()) { + const QPoint pos = viewport()->mapFromGlobal(QCursor::pos()); + if (state() == QAbstractItemView::DraggingState && viewport()->rect().contains(pos)) { + setExpanded(indexAt(pos), true); + } + } + + QTreeView::timerEvent(event); +} + +#ifndef QT_NO_DRAGANDDROP +void EntityTreeView::dragMoveEvent(QDragMoveEvent *event) +{ + d->mDragExpandTimer.start(QApplication::startDragTime(), this); + + if (d->mDragDropManager->dropAllowed(event)) { + // All urls are supported. process the event. + QTreeView::dragMoveEvent(event); + return; + } + + event->setDropAction(Qt::IgnoreAction); +} + +void EntityTreeView::dropEvent(QDropEvent *event) +{ + d->mDragExpandTimer.stop(); + bool menuCanceled = false; + if (d->mDragDropManager->processDropEvent(event, menuCanceled, (dropIndicatorPosition() == QAbstractItemView::OnItem))) { + QTreeView::dropEvent(event); + } +} +#endif + +#ifndef QT_NO_CONTEXTMENU +void EntityTreeView::contextMenuEvent(QContextMenuEvent *event) +{ + if (!d->mXmlGuiClient || !model()) { + return; + } + + const QModelIndex index = indexAt(event->pos()); + QString popupName = d->mDefaultPopupMenu; + + if (index.isValid()) { // popup not over empty space + // check whether the index under the cursor is a collection or item + const Item item = model()->data(index, EntityTreeModel::ItemRole).value(); + popupName = (item.isValid() ? QStringLiteral("akonadi_itemview_contextmenu") : QStringLiteral("akonadi_collectionview_contextmenu")); + } + + auto popup = static_cast(d->mXmlGuiClient->factory()->container(popupName, d->mXmlGuiClient)); + if (popup) { + popup->exec(event->globalPos()); + } +} +#endif + +void EntityTreeView::setXmlGuiClient(KXMLGUIClient *xmlGuiClient) +{ + d->mXmlGuiClient = xmlGuiClient; +} + +KXMLGUIClient *EntityTreeView::xmlGuiClient() const +{ + return d->mXmlGuiClient; +} + +#ifndef QT_NO_DRAGANDDROP +void EntityTreeView::startDrag(Qt::DropActions supportedActions) +{ + d->mDragDropManager->startDrag(supportedActions); +} +#endif + +void EntityTreeView::setDropActionMenuEnabled(bool enabled) +{ +#ifndef QT_NO_DRAGANDDROP + d->mDragDropManager->setShowDropActionMenu(enabled); +#endif +} + +bool EntityTreeView::isDropActionMenuEnabled() const +{ +#ifndef QT_NO_DRAGANDDROP + return d->mDragDropManager->showDropActionMenu(); +#else + return false; +#endif +} + +void EntityTreeView::setManualSortingActive(bool active) +{ +#ifndef QT_NO_DRAGANDDROP + d->mDragDropManager->setManualSortingActive(active); +#endif +} + +bool EntityTreeView::isManualSortingActive() const +{ +#ifndef QT_NO_DRAGANDDROP + return d->mDragDropManager->isManualSortingActive(); +#else + return false; +#endif +} + +void EntityTreeView::setDefaultPopupMenu(const QString &name) +{ + d->mDefaultPopupMenu = name; +} + +#include "moc_entitytreeview.cpp" diff --git a/src/widgets/entitytreeview.h b/src/widgets/entitytreeview.h new file mode 100644 index 0000000..3782d90 --- /dev/null +++ b/src/widgets/entitytreeview.h @@ -0,0 +1,224 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Volker Krause + SPDX-FileCopyrightText: 2008 Stephen Kelly + SPDX-FileCopyrightText: 2012-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +class KXMLGUIClient; +class QDragMoveEvent; + +namespace Akonadi +{ +class Collection; +class Item; + +/** + * @short A view to show an item/collection tree provided by an EntityTreeModel. + * + * When a KXmlGuiWindow is passed to the constructor, the XMLGUI + * defined context menu @c akonadi_collectionview_contextmenu or + * @c akonadi_itemview_contextmenu is used if available. + * + * Example: + * + * @code + * + * using namespace Akonadi; + * + * class MyWindow : public KXmlGuiWindow + * { + * public: + * MyWindow() + * : KXmlGuiWindow() + * { + * EntityTreeView *view = new EntityTreeView( this, this ); + * setCentralWidget( view ); + * + * EntityTreeModel *model = new EntityTreeModel( ... ); + * view->setModel( model ); + * } + * } + * + * @endcode + * + * @author Volker Krause + * @author Stephen Kelly + * @since 4.4 + */ +class AKONADIWIDGETS_EXPORT EntityTreeView : public QTreeView +{ + Q_OBJECT + +public: + /** + * Creates a new entity tree view. + * + * @param parent The parent widget. + */ + explicit EntityTreeView(QWidget *parent = nullptr); + + /** + * Creates a new entity tree view. + * + * @param xmlGuiClient The KXMLGUIClient the view is used in. + * This is needed for the XMLGUI based context menu. + * Passing 0 is ok and will disable the builtin context menu. + * @param parent The parent widget. + */ + explicit EntityTreeView(KXMLGUIClient *xmlGuiClient, QWidget *parent = nullptr); + + /** + * Destroys the entity tree view. + */ + ~EntityTreeView() override; + + /** + * Sets the XML GUI client which the view is used in. + * + * This is needed if you want to use the built-in context menu. + * + * @param xmlGuiClient The KXMLGUIClient the view is used in. + */ + void setXmlGuiClient(KXMLGUIClient *xmlGuiClient); + + /** + * Return the XML GUI client which the view is used in. + * @since 4.12 + */ + KXMLGUIClient *xmlGuiClient() const; + + /** + * @reimp + * @param model the model to set + */ + void setModel(QAbstractItemModel *model) override; + + /** + * Sets whether the drop action menu is @p enabled and will + * be shown on drop operation. + * @param enabled enables drop action menu if set as @c true + * @since 4.5 + */ + void setDropActionMenuEnabled(bool enabled); + + /** + * Returns whether the drop action menu is enabled and will + * be shown on drop operation. + * + * @since 4.5 + */ + Q_REQUIRED_RESULT bool isDropActionMenuEnabled() const; + + /** + * Return true if we use an manual sorting + * Necessary to fix dnd menu + * We must show just move when we move item between two items + * When automatic no show dnd menu between two items. + * @since 4.8.1 + */ + Q_REQUIRED_RESULT bool isManualSortingActive() const; + + /** + * Set true if we automatic sorting + * @param active enables automatic sorting if set as @c true + * @since 4.8.1 + */ + void setManualSortingActive(bool active); + + /** + * Set the name of the default popup menu (retrieved from the + * application's XMLGUI file). + * + * This menu is used as a fallback if the context of the menu request + * is neither an item nor a collection, e.g. the click is on an empty + * area inside the view. If the click is over an entry in the view, + * the menu which is applicable to the clicked entry (either an Item + * or a Collection) is used. + * + * @param name The name of the popup menu + * + * @since 4.9 + * @note For backwards compatibility, the default is the standard + * collection popup menu, "akonadi_collectionview_contextmenu". + * @see KXMLGUIClient, KXMLGUIFactory::container() + */ + void setDefaultPopupMenu(const QString &name); + +Q_SIGNALS: + /** + * This signal is emitted whenever the user has clicked + * a collection in the view. + * + * @param collection The clicked collection. + */ + void clicked(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the user has clicked + * an item in the view. + * + * @param item The clicked item. + */ + void clicked(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the user has double clicked + * a collection in the view. + * + * @param collection The double clicked collection. + */ + void doubleClicked(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the user has double clicked + * an item in the view. + * + * @param item The double clicked item. + */ + void doubleClicked(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the current collection + * in the view has changed. + * + * @param collection The new current collection. + */ + void currentChanged(const Akonadi::Collection &collection); + + /** + * This signal is emitted whenever the current item + * in the view has changed. + * + * @param item The new current item. + */ + void currentChanged(const Akonadi::Item &item); + +protected: + using QTreeView::currentChanged; +#ifndef QT_NO_DRAGANDDROP + void startDrag(Qt::DropActions supportedActions) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; +#endif + void timerEvent(QTimerEvent *event) override; +#ifndef QT_NO_CONTEXTMENU + void contextMenuEvent(QContextMenuEvent *event) override; +#endif + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/erroroverlay.cpp b/src/widgets/erroroverlay.cpp new file mode 100644 index 0000000..4fa5cc1 --- /dev/null +++ b/src/widgets/erroroverlay.cpp @@ -0,0 +1,256 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "erroroverlay_p.h" +#include "ui_erroroverlay.h" +#if 0 +#include "selftestdialog_p.h" +#endif + +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +/// @cond PRIVATE + +class ErrorOverlayStatic +{ +public: + QVector, QPointer>> baseWidgets; +}; + +Q_GLOBAL_STATIC(ErrorOverlayStatic, sInstanceOverlay) // NOLINT(readability-redundant-member-init) + +// return true if o1 is a parent of o2 +static bool isParentOf(QWidget *o1, QWidget *o2) +{ + if (!o1 || !o2) { + return false; + } + if (o1 == o2) { + return true; + } + if (o2->isWindow()) { + return false; + } + return isParentOf(o1, o2->parentWidget()); +} + +ErrorOverlay::ErrorOverlay(QWidget *baseWidget, QWidget *parent) + : QWidget(parent ? parent : baseWidget->window()) + , mBaseWidget(baseWidget) + , mOverlayActive(false) + , mBaseWidgetIsParent(false) + , ui(new Ui::ErrorOverlay) +{ + Q_ASSERT(baseWidget); + + mBaseWidgetIsParent = isParentOf(mBaseWidget, this); + + // check existing overlays to detect cascading + for (QVector, QPointer>>::Iterator it = sInstanceOverlay->baseWidgets.begin(); + it != sInstanceOverlay->baseWidgets.end();) { + if ((*it).first == nullptr || (*it).second == nullptr) { + // garbage collection + it = sInstanceOverlay->baseWidgets.erase(it); + continue; + } + if (isParentOf((*it).first, baseWidget)) { + // parent already has an overlay, kill ourselves + mBaseWidget = nullptr; + hide(); + deleteLater(); + return; + } + if (isParentOf(baseWidget, (*it).first)) { + // child already has overlay, kill that one + delete (*it).second; + it = sInstanceOverlay->baseWidgets.erase(it); + continue; + } + ++it; + } + sInstanceOverlay->baseWidgets.append(qMakePair(mBaseWidget, QPointer(this))); + + connect(baseWidget, &QObject::destroyed, this, &QObject::deleteLater); + mPreviousState = !mBaseWidget->testAttribute(Qt::WA_ForceDisabled); + + ui->setupUi(this); + ui->notRunningIcon->setPixmap(QIcon::fromTheme(QStringLiteral("akonadi")).pixmap(64)); + ui->brokenIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-error")).pixmap(64)); + ui->progressIcon->setPixmap(QIcon::fromTheme(QStringLiteral("akonadi")).pixmap(32)); + ui->quitButton->setText(KStandardGuiItem::quit().text()); + ui->detailsQuitButton->setText(KStandardGuiItem::quit().text()); + + ui->quitButton->hide(); + ui->detailsQuitButton->hide(); + + connect(ui->startButton, &QAbstractButton::clicked, this, &ErrorOverlay::startClicked); + connect(ui->quitButton, &QAbstractButton::clicked, this, &ErrorOverlay::quitClicked); + connect(ui->detailsQuitButton, &QAbstractButton::clicked, this, &ErrorOverlay::quitClicked); + connect(ui->selfTestButton, &QAbstractButton::clicked, this, &ErrorOverlay::selfTestClicked); + + const ServerManager::State state = ServerManager::state(); + mOverlayActive = (state == ServerManager::Running); + serverStateChanged(state); + + connect(ServerManager::self(), &ServerManager::stateChanged, this, &ErrorOverlay::serverStateChanged); + + QPalette p = palette(); + p.setColor(backgroundRole(), QColor(0, 0, 0, 128)); + p.setColor(foregroundRole(), Qt::white); + setPalette(p); + setAutoFillBackground(true); + + mBaseWidget->installEventFilter(this); + + reposition(); +} + +ErrorOverlay::~ErrorOverlay() +{ + if (mBaseWidget && !mBaseWidgetIsParent) { + mBaseWidget->setEnabled(mPreviousState); + } +} + +void ErrorOverlay::reposition() +{ + if (!mBaseWidget) { + return; + } + + // reparent to the current top level widget of the base widget if needed + // needed eg. in dock widgets + if (parentWidget() != mBaseWidget->window()) { + setParent(mBaseWidget->window()); + } + + // follow base widget visibility + // needed eg. in tab widgets + if (!mBaseWidget->isVisible()) { + hide(); + return; + } + if (mOverlayActive) { + show(); + } + + // follow position changes + const QPoint topLevelPos = mBaseWidget->mapTo(window(), QPoint(0, 0)); + const QPoint parentPos = parentWidget()->mapFrom(window(), topLevelPos); + move(parentPos); + + // follow size changes + // TODO: hide/scale icon if we don't have enough space + resize(mBaseWidget->size()); +} + +bool ErrorOverlay::eventFilter(QObject *object, QEvent *event) +{ + if (object == mBaseWidget && mOverlayActive + && (event->type() == QEvent::Move || event->type() == QEvent::Resize || event->type() == QEvent::Show || event->type() == QEvent::Hide + || event->type() == QEvent::ParentChange)) { + reposition(); + } + return QWidget::eventFilter(object, event); +} + +void ErrorOverlay::startClicked() +{ + const ServerManager::State state = ServerManager::state(); + if (state == ServerManager::Running) { + serverStateChanged(state); + } else { + ServerManager::start(); + } +} + +void ErrorOverlay::quitClicked() +{ + qApp->quit(); +} + +void ErrorOverlay::selfTestClicked() +{ +#if 0 + SelfTestDialog dlg; + dlg.exec(); +#endif +} + +void ErrorOverlay::serverStateChanged(ServerManager::State state) +{ + if (!mBaseWidget) { + return; + } + + if (state == ServerManager::Running) { + if (mOverlayActive) { + mOverlayActive = false; + hide(); + if (!mBaseWidgetIsParent) { + mBaseWidget->setEnabled(mPreviousState); + } + } + } else if (!mOverlayActive) { + mOverlayActive = true; + if (mBaseWidget->isVisible()) { + show(); + } + + if (!mBaseWidgetIsParent) { + mPreviousState = !mBaseWidget->testAttribute(Qt::WA_ForceDisabled); + mBaseWidget->setEnabled(false); + } + + reposition(); + } + + if (mOverlayActive) { + switch (state) { + case ServerManager::NotRunning: + ui->stackWidget->setCurrentWidget(ui->notRunningPage); + break; + case ServerManager::Broken: + ui->stackWidget->setCurrentWidget(ui->brokenPage); + if (!ServerManager::brokenReason().isEmpty()) { + ui->brokenDescription->setText( + i18nc("%1 is a reason why", "Cannot connect to the Personal information management service.\n\n%1", ServerManager::brokenReason())); + } + break; + case ServerManager::Starting: + ui->progressPage->setToolTip(i18n("Personal information management service is starting...")); + ui->progressDescription->setText(i18n("Personal information management service is starting...")); + ui->stackWidget->setCurrentWidget(ui->progressPage); + break; + case ServerManager::Stopping: + ui->progressPage->setToolTip(i18n("Personal information management service is shutting down...")); + ui->progressDescription->setText(i18n("Personal information management service is shutting down...")); + ui->stackWidget->setCurrentWidget(ui->progressPage); + break; + case ServerManager::Upgrading: + ui->progressPage->setToolTip(i18n("Personal information management service is performing a database upgrade.")); + ui->progressDescription->setText( + i18n("Personal information management service is performing a database upgrade.\n" + "This happens after a software update and is necessary to optimize performance.\n" + "Depending on the amount of personal information, this might take a few minutes.")); + ui->stackWidget->setCurrentWidget(ui->progressPage); + break; + case ServerManager::Running: + break; + } + } +} + +/// @endcond + +#include "moc_erroroverlay_p.cpp" diff --git a/src/widgets/erroroverlay.ui b/src/widgets/erroroverlay.ui new file mode 100644 index 0000000..6e1be44 --- /dev/null +++ b/src/widgets/erroroverlay.ui @@ -0,0 +1,433 @@ + + + ErrorOverlay + + + + 0 + 0 + 400 + 300 + + + + + + + 0 + + + + The Akonadi personal information management service is not running. This application cannot be used without it. + + + + + + Qt::Vertical + + + + 20 + 95 + + + + + + + + + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 550 + 16777215 + + + + The Akonadi personal information management service is not running. This application cannot be used without it. + + + Qt::AlignCenter + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Start + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 96 + + + + + + + + + The Akonadi personal information management framework is not operational. +Click on "Details..." to obtain detailed information on this problem. + + + + + + Qt::Vertical + + + + 20 + 95 + + + + + + + + + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 550 + 16777215 + + + + The Akonadi personal information management service is not operational. + + + Qt::AlignCenter + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + Details... + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 96 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 113 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::AlignCenter + + + + + + + 0 + + + 7229 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 550 + 16777215 + + + + + + + Qt::AlignCenter + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 113 + + + + + + + + + + + + + diff --git a/src/widgets/erroroverlay_p.h b/src/widgets/erroroverlay_p.h new file mode 100644 index 0000000..9b253a5 --- /dev/null +++ b/src/widgets/erroroverlay_p.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "servermanager.h" + +#include +#include + +namespace Ui +{ +class ErrorOverlay; +} + +namespace Akonadi +{ +/** + * @internal + * Overlay widget to block Akonadi-dependent widgets if the Akonadi server + * is unavailable. + * @todo handle initial parent == 0 case correctly, reparent later and hide as long as parent widget is 0 + * @todo fix hiding in dock widget tabs + */ +class ErrorOverlay : public QWidget +{ + Q_OBJECT +public: + /** + * Create an overlay widget for @p baseWidget. + * @p baseWidget must not be null. + * @p parent must not be equal to @p baseWidget + */ + explicit ErrorOverlay(QWidget *baseWidget, QWidget *parent = nullptr); + ~ErrorOverlay() override; + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + void reposition(); + +private Q_SLOTS: + void startClicked(); + void quitClicked(); + void selfTestClicked(); + void serverStateChanged(Akonadi::ServerManager::State state); + +private: + QPointer mBaseWidget; + bool mPreviousState; + bool mOverlayActive; + bool mBaseWidgetIsParent; + QScopedPointer ui; +}; + +} + diff --git a/src/widgets/etmviewstatesaver.cpp b/src/widgets/etmviewstatesaver.cpp new file mode 100644 index 0000000..8d781bd --- /dev/null +++ b/src/widgets/etmviewstatesaver.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net + SPDX-FileContributor: Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "etmviewstatesaver.h" + +#include + +#include "entitytreemodel.h" + +using namespace Akonadi; + +ETMViewStateSaver::ETMViewStateSaver(QObject *parent) + : KConfigViewStateSaver(parent) +{ +} + +QModelIndex ETMViewStateSaver::indexFromConfigString(const QAbstractItemModel *model, const QString &key) const +{ + if (key.startsWith(QLatin1Char('x'))) { + return QModelIndex(); + } + + Item::Id id = key.mid(1).toLongLong(); + if (id < 0) { + return QModelIndex(); + } + + if (key.startsWith(QLatin1Char('c'))) { + const QModelIndex idx = EntityTreeModel::modelIndexForCollection(model, Collection(id)); + if (!idx.isValid()) { + return QModelIndex(); + } + return idx; + } else if (key.startsWith(QLatin1Char('i'))) { + const QModelIndexList list = EntityTreeModel::modelIndexesForItem(model, Item(id)); + if (list.isEmpty()) { + return QModelIndex(); + } + return list.first(); + } + return QModelIndex(); +} + +QString ETMViewStateSaver::indexToConfigString(const QModelIndex &index) const +{ + if (!index.isValid()) { + return QStringLiteral("x-1"); + } + const auto c = index.data(EntityTreeModel::CollectionRole).value(); + if (c.isValid()) { + return QStringLiteral("c%1").arg(c.id()); + } + auto id = index.data(EntityTreeModel::ItemIdRole).value(); + if (id >= 0) { + return QStringLiteral("i%1").arg(id); + } + return QString(); +} + +void ETMViewStateSaver::selectCollections(const Akonadi::Collection::List &list) +{ + QStringList colStrings; + colStrings.reserve(list.count()); + for (const Collection &col : list) { + colStrings << QStringLiteral("c%1").arg(col.id()); + } + restoreSelection(colStrings); +} + +void ETMViewStateSaver::selectCollections(const QList &list) +{ + QStringList colStrings; + colStrings.reserve(list.count()); + for (const Collection::Id &colId : list) { + colStrings << QStringLiteral("c%1").arg(colId); + } + restoreSelection(colStrings); +} + +void ETMViewStateSaver::selectItems(const Akonadi::Item::List &list) +{ + QStringList itemStrings; + itemStrings.reserve(list.count()); + for (const Item &item : list) { + itemStrings << QStringLiteral("i%1").arg(item.id()); + } + restoreSelection(itemStrings); +} + +void ETMViewStateSaver::selectItems(const QList &list) +{ + QStringList itemStrings; + itemStrings.reserve(list.count()); + for (const Item::Id &itemId : list) { + itemStrings << QStringLiteral("i%1").arg(itemId); + } + restoreSelection(itemStrings); +} + +void ETMViewStateSaver::setCurrentItem(const Akonadi::Item &item) +{ + restoreCurrentItem(QStringLiteral("i%1").arg(item.id())); +} + +void ETMViewStateSaver::setCurrentCollection(const Akonadi::Collection &col) +{ + restoreCurrentItem(QStringLiteral("c%1").arg(col.id())); +} diff --git a/src/widgets/etmviewstatesaver.h b/src/widgets/etmviewstatesaver.h new file mode 100644 index 0000000..e458af3 --- /dev/null +++ b/src/widgets/etmviewstatesaver.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net + SPDX-FileContributor: Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "collection.h" +#include "item.h" + +#include "akonadiwidgets_export.h" + +namespace Akonadi +{ +class AKONADIWIDGETS_EXPORT ETMViewStateSaver : public KConfigViewStateSaver // krazy:exclude=dpointer +{ + Q_OBJECT +public: + explicit ETMViewStateSaver(QObject *parent = nullptr); + + void selectCollections(const Akonadi::Collection::List &list); + void selectCollections(const QList &list); + void selectItems(const Akonadi::Item::List &list); + void selectItems(const QList &list); + + void setCurrentItem(const Akonadi::Item &item); + void setCurrentCollection(const Akonadi::Collection &collection); + +protected: + /* reimp */ + QModelIndex indexFromConfigString(const QAbstractItemModel *model, const QString &key) const override; + QString indexToConfigString(const QModelIndex &index) const override; +}; + +} + diff --git a/src/widgets/itemview.cpp b/src/widgets/itemview.cpp new file mode 100644 index 0000000..34f66b1 --- /dev/null +++ b/src/widgets/itemview.cpp @@ -0,0 +1,167 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemview.h" + +#include "controlgui.h" +#include "entitytreemodel.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN ItemView::Private +{ +public: + explicit Private(ItemView *parent) + : mParent(parent) + { + } + + void init(); + void itemActivated(const QModelIndex &index); + void itemCurrentChanged(const QModelIndex &index); + void itemClicked(const QModelIndex &index); + void itemDoubleClicked(const QModelIndex &index); + + Item itemForIndex(const QModelIndex &index); + + KXMLGUIClient *xmlGuiClient = nullptr; + +private: + ItemView *const mParent; +}; + +void ItemView::Private::init() +{ + mParent->setRootIsDecorated(false); + + mParent->header()->setSectionsClickable(true); + mParent->header()->setStretchLastSection(true); + + mParent->connect(mParent, &QAbstractItemView::activated, mParent, [this](const auto &index) { + itemActivated(index); + }); + mParent->connect(mParent, &QAbstractItemView::clicked, mParent, [this](const auto &index) { + itemClicked(index); + }); + mParent->connect(mParent, &QAbstractItemView::doubleClicked, [this](const auto &index) { + itemDoubleClicked(index); + }); + + ControlGui::widgetNeedsAkonadi(mParent); +} + +Item ItemView::Private::itemForIndex(const QModelIndex &index) +{ + if (!index.isValid()) { + return Item(); + } + + return mParent->model()->data(index, EntityTreeModel::ItemRole).value(); +} + +void ItemView::Private::itemActivated(const QModelIndex &index) +{ + const Item item = itemForIndex(index); + + if (!item.isValid()) { + return; + } + + Q_EMIT mParent->activated(item); +} + +void ItemView::Private::itemCurrentChanged(const QModelIndex &index) +{ + const Item item = itemForIndex(index); + + if (!item.isValid()) { + return; + } + + Q_EMIT mParent->currentChanged(item); +} + +void ItemView::Private::itemClicked(const QModelIndex &index) +{ + const Item item = itemForIndex(index); + + if (!item.isValid()) { + return; + } + + Q_EMIT mParent->clicked(item); +} + +void ItemView::Private::itemDoubleClicked(const QModelIndex &index) +{ + const Item item = itemForIndex(index); + + if (!item.isValid()) { + return; + } + + Q_EMIT mParent->doubleClicked(item); +} + +ItemView::ItemView(QWidget *parent) + : QTreeView(parent) + , d(new Private(this)) +{ + d->init(); +} + +ItemView::ItemView(KXMLGUIClient *xmlGuiClient, QWidget *parent) + : QTreeView(parent) + , d(new Private(this)) +{ + d->xmlGuiClient = xmlGuiClient; + d->init(); +} + +ItemView::~ItemView() +{ + delete d; +} + +void ItemView::setModel(QAbstractItemModel *model) +{ + if (selectionModel()) { + disconnect(selectionModel(), &QItemSelectionModel::currentChanged, this, nullptr); + } + + QTreeView::setModel(model); + + connect(selectionModel(), &QItemSelectionModel::currentChanged, this, [this](const auto &index) { + d->itemCurrentChanged(index); + }); +} + +void ItemView::contextMenuEvent(QContextMenuEvent *event) +{ + if (!d->xmlGuiClient) { + return; + } + QMenu *popup = static_cast(d->xmlGuiClient->factory()->container(QStringLiteral("akonadi_itemview_contextmenu"), d->xmlGuiClient)); + if (popup) { + popup->exec(event->globalPos()); + } +} + +void ItemView::setXmlGuiClient(KXMLGUIClient *xmlGuiClient) +{ + d->xmlGuiClient = xmlGuiClient; +} + +#include "moc_itemview.cpp" diff --git a/src/widgets/itemview.h b/src/widgets/itemview.h new file mode 100644 index 0000000..4fda2b5 --- /dev/null +++ b/src/widgets/itemview.h @@ -0,0 +1,133 @@ +/* + SPDX-FileCopyrightText: 2007 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include + +class KXmlGuiWindow; +class KXMLGUIClient; +namespace Akonadi +{ +class Item; + +/** + * @short A view to show an item list provided by an ItemModel. + * + * When a KXmlGuiWindow is set, the XMLGUI defined context menu + * @c akonadi_itemview_contextmenu is used if available. + * + * Example: + * + * @code + * + * class MyWindow : public KXmlGuiWindow + * { + * public: + * MyWindow() + * : KXmlGuiWindow() + * { + * Akonadi::ItemView *view = new Akonadi::ItemView( this, this ); + * setCentralWidget( view ); + * + * Akonadi::ItemModel *model = new Akonadi::ItemModel( this ); + * view->setModel( model ); + * } + * } + * + * @endcode + * + * @deprecated Use EntityTreeView or EntityListView on top of EntityTreeModel instead. + * + * @author Tobias Koenig + */ +class AKONADIWIDGETS_DEPRECATED_EXPORT ItemView : public QTreeView +{ + Q_OBJECT + +public: + /** + * Creates a new item view. + * + * @param parent The parent widget. + */ + explicit ItemView(QWidget *parent = nullptr); + + /** + * Creates a new item view. + * + * @param xmlGuiClient The KXMLGUIClient this is used in. + * This is needed for the XMLGUI based context menu. + * Passing 0 is ok and will disable the builtin context menu. + * @param parent The parent widget. + * @since 4.3 + */ + explicit ItemView(KXMLGUIClient *xmlGuiClient, QWidget *parent = nullptr); + + /** + * Destroys the item view. + */ + ~ItemView() override; + + /** + * Sets the KXMLGUIFactory which this view is used in. + * This is needed if you want to use the built-in context menu. + * + * @param xmlGuiClient The KXMLGUIClient this view is used in. + */ + void setXmlGuiClient(KXMLGUIClient *xmlGuiClient); + + void setModel(QAbstractItemModel *model) override; + +Q_SIGNALS: + /** + * This signal is emitted whenever the user has activated + * an item in the view. + * + * @param item The activated item. + */ + void activated(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the current item + * in the view has changed. + * + * @param item The current item. + */ + void currentChanged(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the user clicked on an item + * in the view. + * + * @param item The item the user clicked on. + * @since 4.3 + */ + void clicked(const Akonadi::Item &item); + + /** + * This signal is emitted whenever the user double clicked on an item + * in the view. + * + * @param item The item the user double clicked on. + * @since 4.3 + */ + void doubleClicked(const Akonadi::Item &item); + +protected: + using QTreeView::currentChanged; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + /// @cond PRIVATE + class Private; + Private *const d; + /// @endcond +}; + +} + diff --git a/src/widgets/manageaccountwidget.cpp b/src/widgets/manageaccountwidget.cpp new file mode 100644 index 0000000..3aa0bd2 --- /dev/null +++ b/src/widgets/manageaccountwidget.cpp @@ -0,0 +1,223 @@ +/* + SPDX-FileCopyrightText: 2014-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "manageaccountwidget.h" + +#include "agentconfigurationdialog.h" +#include "agentfilterproxymodel.h" +#include "agentinstance.h" +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "agenttypedialog.h" + +#include "ui_manageaccountwidget.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +class Akonadi::ManageAccountWidgetPrivate +{ +public: + QString mSpecialCollectionIdentifier; + + QStringList mMimeTypeFilter; + QStringList mCapabilityFilter; + QStringList mExcludeCapabilities; + + Ui::ManageAccountWidget ui; +}; + +ManageAccountWidget::ManageAccountWidget(QWidget *parent) + : QWidget(parent) + , d(new Akonadi::ManageAccountWidgetPrivate) +{ + d->ui.setupUi(this); + connect(d->ui.mAddAccountButton, &QPushButton::clicked, this, &ManageAccountWidget::slotAddAccount); + + connect(d->ui.mModifyAccountButton, &QPushButton::clicked, this, &ManageAccountWidget::slotModifySelectedAccount); + + connect(d->ui.mRemoveAccountButton, &QPushButton::clicked, this, &ManageAccountWidget::slotRemoveSelectedAccount); + connect(d->ui.mRestartAccountButton, &QPushButton::clicked, this, &ManageAccountWidget::slotRestartSelectedAccount); + + connect(d->ui.mAccountList, &Akonadi::AgentInstanceWidget::clicked, this, &ManageAccountWidget::slotAccountSelected); + connect(d->ui.mAccountList, &Akonadi::AgentInstanceWidget::doubleClicked, this, &ManageAccountWidget::slotModifySelectedAccount); + + d->ui.mAccountList->view()->setSelectionMode(QAbstractItemView::SingleSelection); + + connect(d->ui.mFilterAccount, &QLineEdit::textChanged, this, &ManageAccountWidget::slotSearchAgentType); + + d->ui.mFilterAccount->installEventFilter(this); + slotAccountSelected(d->ui.mAccountList->currentAgentInstance()); +} + +ManageAccountWidget::~ManageAccountWidget() = default; + +void ManageAccountWidget::slotSearchAgentType(const QString &str) +{ + d->ui.mAccountList->agentFilterProxyModel()->setFilterRegularExpression(str); +} + +void ManageAccountWidget::disconnectAddAccountButton() +{ + disconnect(d->ui.mAddAccountButton, &QPushButton::clicked, this, &ManageAccountWidget::slotAddAccount); +} + +QPushButton *ManageAccountWidget::addAccountButton() const +{ + return d->ui.mAddAccountButton; +} + +void ManageAccountWidget::setDescriptionLabelText(const QString &text) +{ + d->ui.label->setText(text); +} + +bool ManageAccountWidget::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::KeyPress && obj == d->ui.mFilterAccount) { + auto key = static_cast(event); + if ((key->key() == Qt::Key_Enter) || (key->key() == Qt::Key_Return)) { + event->accept(); + return true; + } + } + return QWidget::eventFilter(obj, event); +} + +QAbstractItemView *ManageAccountWidget::view() const +{ + return d->ui.mAccountList->view(); +} + +void ManageAccountWidget::setSpecialCollectionIdentifier(const QString &identifier) +{ + d->mSpecialCollectionIdentifier = identifier; +} + +void ManageAccountWidget::slotAddAccount() +{ + Akonadi::AgentTypeDialog dlg(this); + + Akonadi::AgentFilterProxyModel *filter = dlg.agentFilterProxyModel(); + for (const QString &filterStr : std::as_const(d->mMimeTypeFilter)) { + filter->addMimeTypeFilter(filterStr); + } + for (const QString &capa : std::as_const(d->mCapabilityFilter)) { + filter->addCapabilityFilter(capa); + } + for (const QString &capa : std::as_const(d->mExcludeCapabilities)) { + filter->excludeCapabilities(capa); + } + if (dlg.exec()) { + const Akonadi::AgentType agentType = dlg.agentType(); + + if (agentType.isValid()) { + auto job = new Akonadi::AgentInstanceCreateJob(agentType, this); + job->configure(this); + job->start(); + } + } +} + +QStringList ManageAccountWidget::excludeCapabilities() const +{ + return d->mExcludeCapabilities; +} + +void ManageAccountWidget::setExcludeCapabilities(const QStringList &excludeCapabilities) +{ + d->mExcludeCapabilities = excludeCapabilities; + for (const QString &capability : std::as_const(d->mExcludeCapabilities)) { + d->ui.mAccountList->agentFilterProxyModel()->excludeCapabilities(capability); + } +} + +void ManageAccountWidget::setItemDelegate(QAbstractItemDelegate *delegate) +{ + d->ui.mAccountList->view()->setItemDelegate(delegate); +} + +QStringList ManageAccountWidget::capabilityFilter() const +{ + return d->mCapabilityFilter; +} + +void ManageAccountWidget::setCapabilityFilter(const QStringList &capabilityFilter) +{ + d->mCapabilityFilter = capabilityFilter; + for (const QString &capability : std::as_const(d->mCapabilityFilter)) { + d->ui.mAccountList->agentFilterProxyModel()->addCapabilityFilter(capability); + } +} + +QStringList ManageAccountWidget::mimeTypeFilter() const +{ + return d->mMimeTypeFilter; +} + +void ManageAccountWidget::setMimeTypeFilter(const QStringList &mimeTypeFilter) +{ + d->mMimeTypeFilter = mimeTypeFilter; + for (const QString &mimeType : std::as_const(d->mMimeTypeFilter)) { + d->ui.mAccountList->agentFilterProxyModel()->addMimeTypeFilter(mimeType); + } +} + +void ManageAccountWidget::slotModifySelectedAccount() +{ + Akonadi::AgentInstance instance = d->ui.mAccountList->currentAgentInstance(); + if (instance.isValid()) { + KWindowSystem::allowExternalProcessWindowActivation(); + QPointer dlg(new AgentConfigurationDialog(instance, this)); + dlg->exec(); + delete dlg; + } +} + +void ManageAccountWidget::slotRestartSelectedAccount() +{ + const Akonadi::AgentInstance instance = d->ui.mAccountList->currentAgentInstance(); + if (instance.isValid()) { + instance.restart(); + } +} + +void ManageAccountWidget::slotRemoveSelectedAccount() +{ + const Akonadi::AgentInstance instance = d->ui.mAccountList->currentAgentInstance(); + + const int rc = KMessageBox::questionYesNo(this, i18n("Do you want to remove account '%1'?", instance.name()), i18n("Remove account?")); + if (rc == KMessageBox::No) { + return; + } + + if (instance.isValid()) { + Akonadi::AgentManager::self()->removeInstance(instance); + } + + slotAccountSelected(d->ui.mAccountList->currentAgentInstance()); +} + +void ManageAccountWidget::slotAccountSelected(const Akonadi::AgentInstance ¤t) +{ + if (current.isValid()) { + d->ui.mModifyAccountButton->setEnabled(!current.type().capabilities().contains(QLatin1String("NoConfig"))); + d->ui.mRemoveAccountButton->setEnabled(d->mSpecialCollectionIdentifier != current.identifier()); + // Restarting an agent is not possible if it's in Running status... (see AgentProcessInstance::restartWhenIdle) + d->ui.mRestartAccountButton->setEnabled((current.status() != 1)); + } else { + d->ui.mModifyAccountButton->setEnabled(false); + d->ui.mRemoveAccountButton->setEnabled(false); + d->ui.mRestartAccountButton->setEnabled(false); + } +} diff --git a/src/widgets/manageaccountwidget.h b/src/widgets/manageaccountwidget.h new file mode 100644 index 0000000..e4048a7 --- /dev/null +++ b/src/widgets/manageaccountwidget.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2014-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include + +class QAbstractItemDelegate; +class QAbstractItemView; +class QPushButton; + +namespace Akonadi +{ +class AgentInstance; +class ManageAccountWidgetPrivate; + +class AKONADIWIDGETS_EXPORT ManageAccountWidget : public QWidget +{ + Q_OBJECT +public: + explicit ManageAccountWidget(QWidget *parent); + ~ManageAccountWidget() override; + + /** + * Sets the text of the label above the list of accounts. + * Example: "Incoming accounts:" in an email client, or "Calendars:" in an organizer. + */ + void setDescriptionLabelText(const QString &text); + + void setSpecialCollectionIdentifier(const QString &identifier); + + Q_REQUIRED_RESULT QStringList mimeTypeFilter() const; + void setMimeTypeFilter(const QStringList &mimeTypeFilter); + + Q_REQUIRED_RESULT QStringList capabilityFilter() const; + void setCapabilityFilter(const QStringList &capabilityFilter); + + Q_REQUIRED_RESULT QStringList excludeCapabilities() const; + void setExcludeCapabilities(const QStringList &excludeCapabilities); + + void setItemDelegate(QAbstractItemDelegate *delegate); + + Q_REQUIRED_RESULT QAbstractItemView *view() const; + + Q_REQUIRED_RESULT QPushButton *addAccountButton() const; + void disconnectAddAccountButton(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +public Q_SLOTS: + void slotAddAccount(); + +private Q_SLOTS: + void slotAccountSelected(const Akonadi::AgentInstance ¤t); + void slotRemoveSelectedAccount(); + void slotRestartSelectedAccount(); + void slotModifySelectedAccount(); + +private: + void slotSearchAgentType(const QString &str); + QScopedPointer const d; +}; +} + diff --git a/src/widgets/manageaccountwidget.ui b/src/widgets/manageaccountwidget.ui new file mode 100644 index 0000000..f273829 --- /dev/null +++ b/src/widgets/manageaccountwidget.ui @@ -0,0 +1,108 @@ + + + ManageAccountWidget + + + + 0 + 0 + 692 + 578 + + + + + + + + + Incoming accounts (add at least one): + + + + + + + + + + + + + + + + + A&dd... + + + + + + + false + + + &Modify... + + + + + + + false + + + R&emove + + + + + + + Qt::Horizontal + + + + + + + false + + + Restart + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + Akonadi::AgentInstanceWidget + QWidget +
agentinstancewidget.h
+
+
+ + +
diff --git a/src/widgets/progressspinnerdelegate.cpp b/src/widgets/progressspinnerdelegate.cpp new file mode 100644 index 0000000..db76636 --- /dev/null +++ b/src/widgets/progressspinnerdelegate.cpp @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net + SPDX-FileContributor: Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "progressspinnerdelegate_p.h" + +#include "entitytreemodel.h" + +#include + +#include +#include + +using namespace Akonadi; + +DelegateAnimator::DelegateAnimator(QAbstractItemView *view) + : QObject(view) + , m_view(view) + , m_timerId(-1) +{ + m_pixmapSequence = KIconLoader::global()->loadPixmapSequence(QStringLiteral("process-working"), 22); +} + +void DelegateAnimator::push(const QModelIndex &index) +{ + if (m_animations.isEmpty()) { + m_timerId = startTimer(200); + } + m_animations.insert(Animation(index)); +} + +void DelegateAnimator::pop(const QModelIndex &index) +{ + if (m_animations.remove(Animation(index))) { + if (m_animations.isEmpty() && m_timerId != -1) { + killTimer(m_timerId); + m_timerId = -1; + } + } +} + +void DelegateAnimator::timerEvent(QTimerEvent *event) +{ + if (!(event->timerId() == m_timerId && m_view)) { + QObject::timerEvent(event); + return; + } + + QRegion region; + // Do no port this to for(:)! The pop() inside the loop invalidates (even implicit) iterators. + Q_FOREACH (const Animation &animation, m_animations) { + // Check if loading is finished (we might not be notified, if the index is scrolled out of view) + const QVariant fetchState = animation.index.data(Akonadi::EntityTreeModel::FetchStateRole); + if (fetchState.toInt() != Akonadi::EntityTreeModel::FetchingState) { + pop(animation.index); + continue; + } + + // This repaints the entire delegate (icon and text). + // TODO: See if there's a way to repaint only part of it (the icon). + animation.nextFrame(); + const QRect rect = m_view->visualRect(animation.index); + region += rect; + } + + if (!region.isEmpty()) { + m_view->viewport()->update(region); + } +} + +QPixmap DelegateAnimator::sequenceFrame(const QModelIndex &index) +{ + for (const Animation &animation : std::as_const(m_animations)) { + if (animation.index == index) { + return m_pixmapSequence.frameAt(animation.frame); + } + } + return QPixmap(); +} + +ProgressSpinnerDelegate::ProgressSpinnerDelegate(DelegateAnimator *animator, QObject *parent) + : QStyledItemDelegate(parent) + , m_animator(animator) +{ +} + +void ProgressSpinnerDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const +{ + QStyledItemDelegate::initStyleOption(option, index); + + const QVariant fetchState = index.data(Akonadi::EntityTreeModel::FetchStateRole); + if (!fetchState.isValid() || fetchState.toInt() != Akonadi::EntityTreeModel::FetchingState) { + m_animator->pop(index); + return; + } + + m_animator->push(index); + + if (auto v = qstyleoption_cast(option)) { + v->icon = m_animator->sequenceFrame(index); + } +} + +uint Akonadi::qHash(const Akonadi::DelegateAnimator::Animation &anim) +{ + return qHash(anim.index); +} diff --git a/src/widgets/progressspinnerdelegate_p.h b/src/widgets/progressspinnerdelegate_p.h new file mode 100644 index 0000000..9bd359f --- /dev/null +++ b/src/widgets/progressspinnerdelegate_p.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, + a KDAB Group company, info@kdab.net + SPDX-FileContributor: Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +namespace Akonadi +{ +class DelegateAnimator : public QObject +{ + Q_OBJECT +public: + explicit DelegateAnimator(QAbstractItemView *view); + + void push(const QModelIndex &index); + void pop(const QModelIndex &index); + + QPixmap sequenceFrame(const QModelIndex &index); + + static const int sCount = 7; + struct Animation { + inline Animation(const QPersistentModelIndex &idx) + : frame(0) + , index(idx) + { + } + + bool operator==(const Animation &other) const + { + return index == other.index; + } + + inline void nextFrame() const + { + frame = (frame + 1) % sCount; + } + mutable int frame; + QPersistentModelIndex index; + }; + +protected: + void timerEvent(QTimerEvent *event) override; + +private: + QSet m_animations; + QAbstractItemView *const m_view; + KPixmapSequence m_pixmapSequence; + int m_timerId; +}; + +uint qHash(const Akonadi::DelegateAnimator::Animation &anim); + +/** + * + */ +class ProgressSpinnerDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit ProgressSpinnerDelegate(DelegateAnimator *animator, QObject *parent = nullptr); + +protected: + void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override; + +private: + DelegateAnimator *m_animator; +}; + +} + diff --git a/src/widgets/recentcollectionaction.cpp b/src/widgets/recentcollectionaction.cpp new file mode 100644 index 0000000..9eb9ccb --- /dev/null +++ b/src/widgets/recentcollectionaction.cpp @@ -0,0 +1,152 @@ +/* + * SPDX-FileCopyrightText: 2011-2021 Laurent Montel + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "entitytreemodel.h" +#include "recentcollectionaction_p.h" +#include +#include +#include + +#include +#include +using namespace Akonadi; + +static const int s_maximumRecentCollection = 10; + +static QStringList readConfig() +{ + const KSharedConfig::Ptr akonadiConfig = KSharedConfig::openConfig(QStringLiteral("akonadikderc")); + const KConfigGroup group(akonadiConfig, QStringLiteral("Recent Collections")); + return (group.readEntry("Collections", QStringList())); +} + +static void writeConfig(const QStringList &list) +{ + KSharedConfig::Ptr akonadiConfig = KSharedConfig::openConfig(QStringLiteral("akonadikderc")); + KConfigGroup group(akonadiConfig, QStringLiteral("Recent Collections")); + group.writeEntry("Collections", list); + group.sync(); +} + +RecentCollectionAction::RecentCollectionAction(Akonadi::StandardActionManager::Type type, + const Akonadi::Collection::List &selectedCollectionsList, + const QAbstractItemModel *model, + QMenu *menu) + : QObject(menu) + , mMenu(menu) + , mModel(model) +{ + mListRecentCollection = readConfig(); + mRecentAction = mMenu->addAction(i18n("Recent Folder")); + mMenu->addSeparator(); + fillRecentCollection(type, selectedCollectionsList); +} + +RecentCollectionAction::~RecentCollectionAction() +{ + // if (needToDeleteMenu) { + // delete mRecentAction->menu(); + // } +} + +bool RecentCollectionAction::clear() +{ + delete mRecentAction->menu(); + needToDeleteMenu = false; + if (mListRecentCollection.isEmpty()) { + mRecentAction->setEnabled(false); + return true; + } + return false; +} + +void RecentCollectionAction::fillRecentCollection(Akonadi::StandardActionManager::Type type, const Akonadi::Collection::List &selectedCollectionsList) +{ + if (clear()) { + return; + } + + auto popup = new QMenu; + mRecentAction->setMenu(popup); + needToDeleteMenu = true; + + const int numberOfRecentCollection(mListRecentCollection.count()); + for (int i = 0; i < numberOfRecentCollection; ++i) { + const QModelIndex index = Akonadi::EntityTreeModel::modelIndexForCollection(mModel, Akonadi::Collection(mListRecentCollection.at(i).toLongLong())); + const auto collection = mModel->data(index, Akonadi::EntityTreeModel::CollectionRole).value(); + if (index.isValid()) { + const bool collectionIsSelected = selectedCollectionsList.contains(collection); + if (type == Akonadi::StandardActionManager::MoveCollectionToMenu && collectionIsSelected) { + continue; + } + + const bool canCreateNewItems = (collection.rights() & Collection::CanCreateItem); + QAction *action = popup->addAction(actionName(index)); + const auto icon = mModel->data(index, Qt::DecorationRole).value(); + action->setIcon(icon); + action->setData(QVariant::fromValue(index)); + action->setEnabled(canCreateNewItems); + } + } +} + +QString RecentCollectionAction::actionName(QModelIndex index) +{ + QString name = index.data().toString(); + name.replace(QLatin1Char('&'), QStringLiteral("&&")); + + index = index.parent(); + QString topLevelName; + while (index != QModelIndex()) { + topLevelName = index.data().toString(); + index = index.parent(); + } + if (topLevelName.isEmpty()) { + return name; + } else { + topLevelName.replace(QLatin1Char('&'), QStringLiteral("&&")); + return QStringLiteral("%1 - %2").arg(name, topLevelName); + } +} + +void RecentCollectionAction::addRecentCollection(Akonadi::StandardActionManager::Type type, Akonadi::Collection::Id id) +{ + mListRecentCollection = addRecentCollection(id); + fillRecentCollection(type, Akonadi::Collection::List()); +} + +/* static */ QStringList RecentCollectionAction::addRecentCollection(Akonadi::Collection::Id id) +{ + QStringList listRecentCollection = readConfig(); + + const QString newCollectionID = QString::number(id); + if (listRecentCollection.contains(newCollectionID)) { + // first() is safe to use if we get here + if (listRecentCollection.first() == newCollectionID) { + // already most recently used, nothing to do + return (listRecentCollection); + } + + listRecentCollection.removeAll(newCollectionID); + } + + listRecentCollection.prepend(newCollectionID); + while (listRecentCollection.count() > s_maximumRecentCollection) { + listRecentCollection.removeLast(); + } + + writeConfig(listRecentCollection); + return (listRecentCollection); +} + +void RecentCollectionAction::cleanRecentCollection() +{ + mListRecentCollection.clear(); + writeConfig(mListRecentCollection); + clear(); +} + +#include "moc_recentcollectionaction_p.cpp" diff --git a/src/widgets/recentcollectionaction_p.h b/src/widgets/recentcollectionaction_p.h new file mode 100644 index 0000000..ce518d7 --- /dev/null +++ b/src/widgets/recentcollectionaction_p.h @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2011-2021 Laurent Montel + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ +#pragma once + +#include "collection.h" +#include +#include +#include + +class QMenu; +class QAbstractItemModel; +class QAction; +/** + * @short A class to manage recent selected folder. + * + * @author Montel Laurent + * @since 4.8 + */ + +namespace Akonadi +{ +class RecentCollectionAction : public QObject +{ + Q_OBJECT +public: + /** + * Creates a new collection recent action + */ + explicit RecentCollectionAction(Akonadi::StandardActionManager::Type type, + const Akonadi::Collection::List &selectedCollectionsList, + const QAbstractItemModel *model, + QMenu *menu); + /** + * Destroys the collection recent action. + */ + ~RecentCollectionAction(); + + /** + * Add new collection. Will create a new item. + */ + void addRecentCollection(StandardActionManager::Type type, Akonadi::Collection::Id id); + + void cleanRecentCollection(); + + /** + * Add a new collection to the global list. + * + * @param id the collection ID + * @since 5.18 + */ + static QStringList addRecentCollection(Akonadi::Collection::Id id); + +private: + void fillRecentCollection(Akonadi::StandardActionManager::Type type, const Akonadi::Collection::List &selectedCollectionsList); + QString actionName(QModelIndex index); + bool clear(); + +private: + QStringList mListRecentCollection; + QMenu *const mMenu; + const QAbstractItemModel *mModel = nullptr; + QAction *mRecentAction = nullptr; + bool needToDeleteMenu = false; +}; +} diff --git a/src/widgets/renamefavoritedialog.cpp b/src/widgets/renamefavoritedialog.cpp new file mode 100644 index 0000000..7b8ddb5 --- /dev/null +++ b/src/widgets/renamefavoritedialog.cpp @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2011-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "renamefavoritedialog_p.h" + +#include + +#include + +using namespace Akonadi; + +RenameFavoriteDialog::RenameFavoriteDialog(const QString &value, const QString &defaultName, QWidget *parent) + : QDialog(parent) + , m_defaultName(defaultName) +{ + ui.setupUi(this); + + connect(ui.lineEdit, &QLineEdit::textChanged, this, [this](const QString &text) { + ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.trimmed().isEmpty()); + }); + connect(ui.buttonBox, &QDialogButtonBox::accepted, this, &RenameFavoriteDialog::accept); + connect(ui.buttonBox, &QDialogButtonBox::rejected, this, &RenameFavoriteDialog::reject); + connect(ui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this]() { + ui.lineEdit->setText(m_defaultName); + }); + + ui.lineEdit->setText(value); +} + +QString RenameFavoriteDialog::newName() const +{ + return ui.lineEdit->text(); +} diff --git a/src/widgets/renamefavoritedialog.ui b/src/widgets/renamefavoritedialog.ui new file mode 100644 index 0000000..859c3b6 --- /dev/null +++ b/src/widgets/renamefavoritedialog.ui @@ -0,0 +1,56 @@ + + + RenameFavoriteDialog + + + + 0 + 0 + 400 + 108 + + + + + 0 + 0 + + + + + 350 + 0 + + + + Rename Favorite + + + true + + + + + + Name: + + + lineEdit + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + + + + + + diff --git a/src/widgets/renamefavoritedialog_p.h b/src/widgets/renamefavoritedialog_p.h new file mode 100644 index 0000000..531c54c --- /dev/null +++ b/src/widgets/renamefavoritedialog_p.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2011-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_renamefavoritedialog.h" + +namespace Akonadi +{ +class RenameFavoriteDialog : public QDialog +{ + Q_OBJECT +public: + explicit RenameFavoriteDialog(const QString &value, const QString &defaultName, QWidget *parent); + + Q_REQUIRED_RESULT QString newName() const; + +private: + const QString m_defaultName; + Ui::RenameFavoriteDialog ui; +}; + +} + diff --git a/src/widgets/selftestdialog.cpp b/src/widgets/selftestdialog.cpp new file mode 100644 index 0000000..671d94c --- /dev/null +++ b/src/widgets/selftestdialog.cpp @@ -0,0 +1,670 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "selftestdialog.h" +#include "agentmanager.h" +#include "private/protocol_p.h" +#include "private/standarddirs_p.h" +#include "servermanager.h" +#include "servermanager_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/// @cond PRIVATE + +using namespace Akonadi; + +static QString makeLink(const QString &file) +{ + return QStringLiteral("%2").arg(file, file); +} + +enum SelfTestRole { + ResultTypeRole = Qt::UserRole, + FileIncludeRole, + ListDirectoryRole, + EnvVarRole, + SummaryRole, + DetailsRole, +}; + +SelfTestDialog::SelfTestDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(i18nc("@title:window", "Akonadi Server Self-Test")); + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this); + auto mainWidget = new QWidget(this); + auto mainLayout = new QVBoxLayout(this); + mainLayout->addWidget(mainWidget); + auto user1Button = new QPushButton(this); + buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole); + auto user2Button = new QPushButton(this); + buttonBox->addButton(user2Button, QDialogButtonBox::ActionRole); + connect(buttonBox, &QDialogButtonBox::rejected, this, &SelfTestDialog::reject); + mainLayout->addWidget(buttonBox); + user1Button->setText(i18n("Save Report...")); + user1Button->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); + user2Button->setText(i18n("Copy Report to Clipboard")); + user2Button->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); + ui.setupUi(mainWidget); + + mTestModel = new QStandardItemModel(this); + ui.testView->setModel(mTestModel); + connect(ui.testView->selectionModel(), &QItemSelectionModel::currentChanged, this, &SelfTestDialog::selectionChanged); + connect(ui.detailsLabel, &QLabel::linkActivated, this, &SelfTestDialog::linkActivated); + + connect(user1Button, &QPushButton::clicked, this, &SelfTestDialog::saveReport); + connect(user2Button, &QPushButton::clicked, this, &SelfTestDialog::copyReport); + + connect(ServerManager::self(), &ServerManager::stateChanged, this, &SelfTestDialog::runTests); + runTests(); +} + +void SelfTestDialog::hideIntroduction() +{ + ui.introductionLabel->hide(); +} + +QStandardItem *SelfTestDialog::report(ResultType type, const KLocalizedString &summary, const KLocalizedString &details) +{ + auto item = new QStandardItem(summary.toString()); + switch (type) { + case Skip: + item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok"))); + break; + case Success: + item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply"))); + break; + case Warning: + item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + break; + case Error: + item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error"))); + break; + } + item->setEditable(false); + item->setWhatsThis(details.toString()); + item->setData(type, ResultTypeRole); + item->setData(summary.toString(nullptr), SummaryRole); + item->setData(details.toString(nullptr), DetailsRole); + mTestModel->appendRow(item); + return item; +} + +void SelfTestDialog::selectionChanged(const QModelIndex &index) +{ + if (index.isValid()) { + ui.detailsLabel->setText(index.data(Qt::WhatsThisRole).toString()); + ui.detailsGroup->setEnabled(true); + } else { + ui.detailsLabel->setText(QString()); + ui.detailsGroup->setEnabled(false); + } +} + +void SelfTestDialog::runTests() +{ + mTestModel->clear(); + + const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); + testSQLDriver(); + if (driver == QLatin1String("QPSQL")) { + testPSQLServer(); + } else { +#ifndef Q_OS_WIN + testRootUser(); +#endif + testMySQLServer(); + testMySQLServerLog(); + testMySQLServerConfig(); + } + testAkonadiCtl(); + testServerStatus(); + testProtocolVersion(); + testResources(); + testServerLog(); + testControlLog(); +} + +QVariant SelfTestDialog::serverSetting(const QString &group, const char *key, const QVariant &def) const +{ + const QString serverConfigFile = StandardDirs::serverConfigFile(StandardDirs::ReadOnly); + QSettings settings(serverConfigFile, QSettings::IniFormat); + settings.beginGroup(group); + return settings.value(QString::fromLatin1(key), def); +} + +bool SelfTestDialog::useStandaloneMysqlServer() const +{ + const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); + if (driver != QLatin1String("QMYSQL")) { + return false; + } + const bool startServer = serverSetting(driver, "StartServer", true).toBool(); + return startServer; +} + +bool SelfTestDialog::runProcess(const QString &app, const QStringList &args, QString &result) const +{ + QProcess proc; + proc.start(app, args); + const bool rv = proc.waitForFinished(); + result.clear(); + result = QString::fromLocal8Bit(proc.readAllStandardError()); + result += QString::fromLocal8Bit(proc.readAllStandardOutput()); + return rv; +} + +void SelfTestDialog::testSQLDriver() +{ + const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); + const QStringList availableDrivers = QSqlDatabase::drivers(); + const KLocalizedString detailsOk = + ki18n("The QtSQL driver '%1' is required by your current Akonadi server configuration and was found on your system.").subs(driver); + const KLocalizedString detailsFail = ki18n( + "The QtSQL driver '%1' is required by your current Akonadi server configuration.\n" + "The following drivers are installed: %2.\n" + "Make sure the required driver is installed.") + .subs(driver) + .subs(availableDrivers.join(QLatin1String(", "))); + QStandardItem *item = nullptr; + if (availableDrivers.contains(driver)) { + item = report(Success, ki18n("Database driver found."), detailsOk); + } else { + item = report(Error, ki18n("Database driver not found."), detailsFail); + } + item->setData(StandardDirs::serverConfigFile(StandardDirs::ReadOnly), FileIncludeRole); +} + +void SelfTestDialog::testMySQLServer() +{ + if (!useStandaloneMysqlServer()) { + report(Skip, ki18n("MySQL server executable not tested."), ki18n("The current configuration does not require an internal MySQL server.")); + return; + } + + const QString driver = serverSetting(QStringLiteral("General"), "Driver", QStringLiteral("QMYSQL")).toString(); + const QString serverPath = serverSetting(driver, "ServerPath", QString()).toString(); // ### default? + + const KLocalizedString details = ki18n( + "You have currently configured Akonadi to use the MySQL server '%1'.\n" + "Make sure you have the MySQL server installed, set the correct path and ensure you have the " + "necessary read and execution rights on the server executable. The server executable is typically " + "called 'mysqld'; its location varies depending on the distribution.") + .subs(serverPath); + + QFileInfo info(serverPath); + if (!info.exists()) { + report(Error, ki18n("MySQL server not found."), details); + } else if (!info.isReadable()) { + report(Error, ki18n("MySQL server not readable."), details); + } else if (!info.isExecutable()) { + report(Error, ki18n("MySQL server not executable."), details); + } else if (!serverPath.contains(QLatin1String("mysqld"))) { + report(Warning, ki18n("MySQL found with unexpected name."), details); + } else { + report(Success, ki18n("MySQL server found."), details); + } + + // be extra sure and get the server version while we are at it + QString result; + if (runProcess(serverPath, QStringList() << QStringLiteral("--version"), result)) { + const KLocalizedString details = ki18n("MySQL server found: %1").subs(result); + report(Success, ki18n("MySQL server is executable."), details); + } else { + const KLocalizedString details = ki18n("Executing the MySQL server '%1' failed with the following error message: '%2'").subs(serverPath).subs(result); + report(Error, ki18n("Executing the MySQL server failed."), details); + } +} + +void SelfTestDialog::testMySQLServerLog() +{ + if (!useStandaloneMysqlServer()) { + report(Skip, ki18n("MySQL server error log not tested."), ki18n("The current configuration does not require an internal MySQL server.")); + return; + } + + const QString logFileName = StandardDirs::saveDir("data", QStringLiteral("db_data")) + QLatin1String("/mysql.err"); + const QFileInfo logFileInfo(logFileName); + if (!logFileInfo.exists() || logFileInfo.size() == 0) { + report(Success, + ki18n("No current MySQL error log found."), + ki18n("The MySQL server did not report any errors during this startup. The log can be found in '%1'.").subs(logFileName)); + return; + } + QFile logFile(logFileName); + if (!logFile.open(QFile::ReadOnly | QFile::Text)) { + report(Error, + ki18n("MySQL error log not readable."), + ki18n("A MySQL server error log file was found but is not readable: %1").subs(makeLink(logFileName))); + return; + } + bool warningsFound = false; + QStandardItem *item = nullptr; + while (!logFile.atEnd()) { + const QString line = QString::fromUtf8(logFile.readLine()); + if (line.contains(QLatin1String("error"), Qt::CaseInsensitive)) { + item = report(Error, + ki18n("MySQL server log contains errors."), + ki18n("The MySQL server error log file '%1' contains errors.").subs(makeLink(logFileName))); + item->setData(logFileName, FileIncludeRole); + return; + } + if (!warningsFound && line.contains(QLatin1String("warn"), Qt::CaseInsensitive)) { + warningsFound = true; + } + } + if (warningsFound) { + item = report(Warning, + ki18n("MySQL server log contains warnings."), + ki18n("The MySQL server log file '%1' contains warnings.").subs(makeLink(logFileName))); + } else { + item = report(Success, + ki18n("MySQL server log contains no errors."), + ki18n("The MySQL server log file '%1' does not contain any errors or warnings.").subs(makeLink(logFileName))); + } + item->setData(logFileName, FileIncludeRole); + + logFile.close(); +} + +void SelfTestDialog::testMySQLServerConfig() +{ + if (!useStandaloneMysqlServer()) { + report(Skip, ki18n("MySQL server configuration not tested."), ki18n("The current configuration does not require an internal MySQL server.")); + return; + } + + QStandardItem *item = nullptr; + const QString globalConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-global.conf")); + const QFileInfo globalConfigInfo(globalConfig); + if (!globalConfig.isEmpty() && globalConfigInfo.exists() && globalConfigInfo.isReadable()) { + item = report(Success, + ki18n("MySQL server default configuration found."), + ki18n("The default configuration for the MySQL server was found and is readable at %1.").subs(makeLink(globalConfig))); + item->setData(globalConfig, FileIncludeRole); + } else { + report(Error, + ki18n("MySQL server default configuration not found."), + ki18n("The default configuration for the MySQL server was not found or was not readable. " + "Check your Akonadi installation is complete and you have all required access rights.")); + } + + const QString localConfig = StandardDirs::locateResourceFile("config", QStringLiteral("mysql-local.conf")); + const QFileInfo localConfigInfo(localConfig); + if (localConfig.isEmpty() || !localConfigInfo.exists()) { + report(Skip, + ki18n("MySQL server custom configuration not available."), + ki18n("The custom configuration for the MySQL server was not found but is optional.")); + } else if (localConfigInfo.exists() && localConfigInfo.isReadable()) { + item = report(Success, + ki18n("MySQL server custom configuration found."), + ki18n("The custom configuration for the MySQL server was found and is readable at %1").subs(makeLink(localConfig))); + item->setData(localConfig, FileIncludeRole); + } else { + report(Error, + ki18n("MySQL server custom configuration not readable."), + ki18n("The custom configuration for the MySQL server was found at %1 but is not readable. " + "Check your access rights.") + .subs(makeLink(localConfig))); + } + + const QString actualConfig = StandardDirs::saveDir("data") + QStringLiteral("/mysql.conf"); + const QFileInfo actualConfigInfo(actualConfig); + if (actualConfig.isEmpty() || !actualConfigInfo.exists() || !actualConfigInfo.isReadable()) { + report(Error, + ki18n("MySQL server configuration not found or not readable."), + ki18n("The MySQL server configuration was not found or is not readable.")); + } else { + item = report(Success, + ki18n("MySQL server configuration is usable."), + ki18n("The MySQL server configuration was found at %1 and is readable.").subs(makeLink(actualConfig))); + item->setData(actualConfig, FileIncludeRole); + } +} + +void SelfTestDialog::testPSQLServer() +{ + const QString dbname = serverSetting(QStringLiteral("QPSQL"), "Name", QStringLiteral("akonadi")).toString(); + const QString hostname = serverSetting(QStringLiteral("QPSQL"), "Host", QStringLiteral("localhost")).toString(); + const QString username = serverSetting(QStringLiteral("QPSQL"), "User", QString()).toString(); + const QString password = serverSetting(QStringLiteral("QPSQL"), "Password", QString()).toString(); + const int port = serverSetting(QStringLiteral("QPSQL"), "Port", 5432).toInt(); + + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QPSQL")); + db.setHostName(hostname); + db.setDatabaseName(dbname); + + if (!username.isEmpty()) { + db.setUserName(username); + } + + if (!password.isEmpty()) { + db.setPassword(password); + } + + db.setPort(port); + + if (!db.open()) { + const KLocalizedString details = ki18n(db.lastError().text().toLatin1().constData()); + report(Error, ki18n("Cannot connect to PostgreSQL server."), details); + } else { + report(Success, ki18n("PostgreSQL server found."), ki18n("The PostgreSQL server was found and connection is working.")); + } + db.close(); +} + +void SelfTestDialog::testAkonadiCtl() +{ + const QString path = Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadictl")); + if (path.isEmpty()) { + report(Error, + ki18n("akonadictl not found"), + ki18n("The program 'akonadictl' needs to be accessible in $PATH. " + "Make sure you have the Akonadi server installed.")); + return; + } + QString result; + if (runProcess(path, QStringList() << QStringLiteral("--version"), result)) { + report(Success, + ki18n("akonadictl found and usable"), + ki18n("The program '%1' to control the Akonadi server was found " + "and could be executed successfully.\nResult:\n%2") + .subs(path) + .subs(result)); + } else { + report(Error, + ki18n("akonadictl found but not usable"), + ki18n("The program '%1' to control the Akonadi server was found " + "but could not be executed successfully.\nResult:\n%2\n" + "Make sure the Akonadi server is installed correctly.") + .subs(path) + .subs(result)); + } +} + +void SelfTestDialog::testServerStatus() +{ + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Control))) { + report(Success, + ki18n("Akonadi control process registered at D-Bus."), + ki18n("The Akonadi control process is registered at D-Bus which typically indicates it is operational.")); + } else { + report(Error, + ki18n("Akonadi control process not registered at D-Bus."), + ki18n("The Akonadi control process is not registered at D-Bus which typically means it was not started " + "or encountered a fatal error during startup.")); + } + + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(ServerManager::serviceName(ServerManager::Server))) { + report(Success, + ki18n("Akonadi server process registered at D-Bus."), + ki18n("The Akonadi server process is registered at D-Bus which typically indicates it is operational.")); + } else { + report(Error, + ki18n("Akonadi server process not registered at D-Bus."), + ki18n("The Akonadi server process is not registered at D-Bus which typically means it was not started " + "or encountered a fatal error during startup.")); + } +} + +void SelfTestDialog::testProtocolVersion() +{ + if (Internal::serverProtocolVersion() < 0) { + report(Skip, + ki18n("Protocol version check not possible."), + ki18n("Without a connection to the server it is not possible to check if the protocol version meets the requirements.")); + return; + } + if (Internal::serverProtocolVersion() < Protocol::version()) { + report(Error, + ki18n("Server protocol version is too old."), + ki18n("The server protocol version is %1, but version %2 is required by the client. " + "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.") + .subs(Internal::serverProtocolVersion()) + .subs(Protocol::version())); + } else if (Internal::serverProtocolVersion() > Protocol::version()) { + report(Error, + ki18n("Server protocol version is too new."), + ki18n("The server protocol version is %1, but version %2 is required by the client. " + "If you recently updated KDE PIM, please make sure to restart both Akonadi and KDE PIM applications.") + .subs(Internal::serverProtocolVersion()) + .subs(Protocol::version())); + } else { + report(Success, ki18n("Server protocol version matches."), ki18n("The current Protocol version is %1.").subs(Internal::serverProtocolVersion())); + } +} + +void SelfTestDialog::testResources() +{ + const AgentType::List agentTypes = AgentManager::self()->types(); + bool resourceFound = false; + for (const AgentType &type : agentTypes) { + if (type.capabilities().contains(QLatin1String("Resource"))) { + resourceFound = true; + break; + } + } + + const auto pathList = StandardDirs::locateAllResourceDirs(QStringLiteral("akonadi/agents")); + QStandardItem *item = nullptr; + if (resourceFound) { + item = report(Success, ki18n("Resource agents found."), ki18n("At least one resource agent has been found.")); + } else { + item = report(Error, + ki18n("No resource agents found."), + ki18n("No resource agents have been found, Akonadi is not usable without at least one. " + "This usually means that no resource agents are installed or that there is a setup problem. " + "The following paths have been searched: '%1'. " + "The XDG_DATA_DIRS environment variable is set to '%2'; make sure this includes all paths " + "where Akonadi agents are installed.") + .subs(pathList.join(QLatin1Char(' '))) + .subs(QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS")))); + } + item->setData(pathList, ListDirectoryRole); + item->setData(QByteArray("XDG_DATA_DIRS"), EnvVarRole); +} + +void SelfTestDialog::testServerLog() +{ + QString serverLog = StandardDirs::saveDir("data") + QLatin1String("/akonadiserver.error"); + QFileInfo info(serverLog); + if (!info.exists() || info.size() <= 0) { + report(Success, ki18n("No current Akonadi server error log found."), ki18n("The Akonadi server did not report any errors during its current startup.")); + } else { + QStandardItem *item = + report(Error, + ki18n("Current Akonadi server error log found."), + ki18n("The Akonadi server reported errors during its current startup. The log can be found in %1.").subs(makeLink(serverLog))); + item->setData(serverLog, FileIncludeRole); + } + + serverLog += QStringLiteral(".old"); + info.setFile(serverLog); + if (!info.exists() || info.size() <= 0) { + report(Success, + ki18n("No previous Akonadi server error log found."), + ki18n("The Akonadi server did not report any errors during its previous startup.")); + } else { + QStandardItem *item = + report(Error, + ki18n("Previous Akonadi server error log found."), + ki18n("The Akonadi server reported errors during its previous startup. The log can be found in %1.").subs(makeLink(serverLog))); + item->setData(serverLog, FileIncludeRole); + } +} + +void SelfTestDialog::testControlLog() +{ + QString controlLog = StandardDirs::saveDir("data") + QLatin1String("/akonadi_control.error"); + QFileInfo info(controlLog); + if (!info.exists() || info.size() <= 0) { + report(Success, + ki18n("No current Akonadi control error log found."), + ki18n("The Akonadi control process did not report any errors during its current startup.")); + } else { + QStandardItem *item = + report(Error, + ki18n("Current Akonadi control error log found."), + ki18n("The Akonadi control process reported errors during its current startup. The log can be found in %1.").subs(makeLink(controlLog))); + item->setData(controlLog, FileIncludeRole); + } + + controlLog += QStringLiteral(".old"); + info.setFile(controlLog); + if (!info.exists() || info.size() <= 0) { + report(Success, + ki18n("No previous Akonadi control error log found."), + ki18n("The Akonadi control process did not report any errors during its previous startup.")); + } else { + QStandardItem *item = + report(Error, + ki18n("Previous Akonadi control error log found."), + ki18n("The Akonadi control process reported errors during its previous startup. The log can be found in %1.").subs(makeLink(controlLog))); + item->setData(controlLog, FileIncludeRole); + } +} + +void SelfTestDialog::testRootUser() +{ + KUser user; + if (user.isSuperUser()) { + report(Error, + ki18n("Akonadi was started as root"), + ki18n("Running Internet-facing applications as root/administrator exposes you to many security risks. MySQL, used by this Akonadi installation, " + "will not allow itself to run as root, to protect you from these risks.")); + } else { + report(Success, + ki18n("Akonadi is not running as root"), + ki18n("Akonadi is not running as a root/administrator user, which is the recommended setup for a secure system.")); + } +} + +QString SelfTestDialog::createReport() +{ + QString result; + QTextStream s(&result); + s << "Akonadi Server Self-Test Report"; + s << "==============================="; + + for (int i = 0; i < mTestModel->rowCount(); ++i) { + QStandardItem *item = mTestModel->item(i); + s << '\n'; + s << "Test " << (i + 1) << ": "; + switch (item->data(ResultTypeRole).toInt()) { + case Skip: + s << "SKIP"; + break; + case Success: + s << "SUCCESS"; + break; + case Warning: + s << "WARNING"; + break; + case Error: + default: + s << "ERROR"; + break; + } + s << "\n--------\n"; + s << '\n'; + s << item->data(SummaryRole).toString() << '\n'; + s << "Details: " << item->data(DetailsRole).toString() << '\n'; + if (item->data(FileIncludeRole).isValid()) { + s << '\n'; + const QString fileName = item->data(FileIncludeRole).toString(); + QFile f(fileName); + if (f.open(QFile::ReadOnly)) { + s << "File content of '" << fileName << "':" << '\n'; + s << f.readAll() << '\n'; + } else { + s << "File '" << fileName << "' could not be opened\n"; + } + } + if (item->data(ListDirectoryRole).isValid()) { + s << '\n'; + const QStringList pathList = item->data(ListDirectoryRole).toStringList(); + if (pathList.isEmpty()) { + s << "Directory list is empty.\n"; + } + for (const QString &path : pathList) { + s << "Directory listing of '" << path << "':\n"; + QDir dir(path); + dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + const QStringList listEntries(dir.entryList()); + for (const QString &entry : listEntries) { + s << entry << '\n'; + } + } + } + if (item->data(EnvVarRole).isValid()) { + s << '\n'; + const QByteArray envVarName = item->data(EnvVarRole).toByteArray(); + const QByteArray envVarValue = qgetenv(envVarName.constData()); + s << "Environment variable " << envVarName << " is set to '" << envVarValue << "'\n"; + } + } + + s << '\n'; + s.flush(); + return result; +} + +void SelfTestDialog::saveReport() +{ + const QString defaultFileName = + QStringLiteral("akonadi-selftest-report-") + QDate::currentDate().toString(QStringLiteral("yyyyMMdd")) + QStringLiteral(".txt"); + const QString fileName = QFileDialog::getSaveFileName(this, i18n("Save Test Report"), defaultFileName); + if (fileName.isEmpty()) { + return; + } + + QFile file(fileName); + if (!file.open(QFile::ReadWrite)) { + QMessageBox::critical(this, i18n("Error"), i18n("Could not open file '%1'", fileName)); + return; + } + + file.write(createReport().toUtf8()); + file.close(); +} + +void SelfTestDialog::copyReport() +{ +#ifndef QT_NO_CLIPBOARD + QApplication::clipboard()->setText(createReport()); +#endif +} + +void SelfTestDialog::linkActivated(const QString &link) +{ + QDesktopServices::openUrl(QUrl::fromLocalFile(link)); +} + +/// @endcond diff --git a/src/widgets/selftestdialog.h b/src/widgets/selftestdialog.h new file mode 100644 index 0000000..e56e3c8 --- /dev/null +++ b/src/widgets/selftestdialog.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "ui_selftestdialog.h" +#include + +class QStandardItem; +class QStandardItemModel; +namespace Akonadi +{ +/** + * @internal + * + * @short A dialog that checks the current status of the Akonadi system. + * + * This dialog checks the current status of the Akonadi system and + * displays a summary of the checks. + * + * @author Volker Krause + */ +class AKONADIWIDGETS_EXPORT SelfTestDialog : public QDialog +{ + Q_OBJECT +public: + /** + * Creates a new self test dialog. + * + * @param parent The parent widget. + */ + explicit SelfTestDialog(QWidget *parent = nullptr); + + /** + * Hides the label with the introduction message. + */ + void hideIntroduction(); + +private Q_SLOTS: + void selectionChanged(const QModelIndex &index); + void saveReport(); + void copyReport(); + void linkActivated(const QString &link); + void runTests(); + +private: + enum ResultType { + Skip, + Success, + Warning, + Error, + }; + QStandardItem *report(ResultType type, const KLocalizedString &summary, const KLocalizedString &details); + QVariant serverSetting(const QString &group, const char *key, const QVariant &def) const; + bool useStandaloneMysqlServer() const; + bool runProcess(const QString &app, const QStringList &args, QString &result) const; + + void testSQLDriver(); + void testMySQLServer(); + void testMySQLServerLog(); + void testMySQLServerConfig(); + void testPSQLServer(); + void testAkonadiCtl(); + void testServerStatus(); + void testProtocolVersion(); + void testResources(); + void testServerLog(); + void testControlLog(); + void testRootUser(); + + QString createReport(); + + Ui::SelfTestDialog ui; + QStandardItemModel *mTestModel = nullptr; +}; +} diff --git a/src/widgets/selftestdialog.ui b/src/widgets/selftestdialog.ui new file mode 100644 index 0000000..865fdfc --- /dev/null +++ b/src/widgets/selftestdialog.ui @@ -0,0 +1,78 @@ + + SelfTestDialog + + + + 0 + 0 + 412 + 436 + + + + + + + An error occurred during the startup of the Akonadi server. The following self-tests are supposed to help with tracking down and solving this problem. When requesting support or reporting bugs, please always include this report. + + + true + + + + + + + false + + + true + + + + + + + false + + + Details + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + <p>For more troubleshooting tips please refer to <a href="https://userbase.kde.org/Akonadi">userbase.kde.org/Akonadi</a>.</p> + + + true + + + true + + + Qt::TextBrowserInteraction + + + + + + + + diff --git a/src/widgets/standardactionmanager.cpp b/src/widgets/standardactionmanager.cpp new file mode 100644 index 0000000..af8aba7 --- /dev/null +++ b/src/widgets/standardactionmanager.cpp @@ -0,0 +1,1990 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "standardactionmanager.h" + +#include "actionstatemanager_p.h" +#include "agentfilterproxymodel.h" +#include "agentinstancecreatejob.h" +#include "agentmanager.h" +#include "agenttypedialog.h" +#include "collectioncreatejob.h" +#include "collectiondeletejob.h" +#include "collectiondialog.h" +#include "collectionpropertiesdialog.h" +#include "collectionpropertiespage.h" +#include "collectionutils.h" +#include "entitydeletedattribute.h" +#include "entitytreemodel.h" +#include "favoritecollectionsmodel.h" +#include "itemdeletejob.h" +#include "pastehelper_p.h" +#include "recentcollectionaction_p.h" +#include "renamefavoritedialog_p.h" +#include "specialcollectionattribute.h" +#include "subscriptiondialog.h" +#include "trashjob.h" +#include "trashrestorejob.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +/// @cond PRIVATE + +enum ActionType { + NormalAction, + ActionWithAlternative, // Normal action, but with an alternative state + ActionAlternative, // Alternative state of the ActionWithAlternative + MenuAction, + ToggleAction +}; + +struct StandardActionData { // NOLINT(clang-analyzer-optin.performance.Padding) FIXME + const char *name; + const char *label; + const char *iconLabel; + const char *icon; + const char *altIcon; + int shortcut; + const char *slot; + ActionType actionType; +}; + +static const StandardActionData standardActionData[] = { + {"akonadi_collection_create", I18N_NOOP("&New Folder..."), I18N_NOOP("New"), "folder-new", nullptr, 0, SLOT(slotCreateCollection()), NormalAction}, + {"akonadi_collection_copy", nullptr, nullptr, "edit-copy", nullptr, 0, SLOT(slotCopyCollections()), NormalAction}, + {"akonadi_collection_delete", I18N_NOOP("&Delete Folder"), I18N_NOOP("Delete"), "edit-delete", nullptr, 0, SLOT(slotDeleteCollection()), NormalAction}, + {"akonadi_collection_sync", + I18N_NOOP("&Synchronize Folder"), + I18N_NOOP("Synchronize"), + "view-refresh", + nullptr, + Qt::Key_F5, + SLOT(slotSynchronizeCollection()), + NormalAction}, + {"akonadi_collection_properties", + I18N_NOOP("Folder &Properties"), + I18N_NOOP("Properties"), + "configure", + nullptr, + 0, + SLOT(slotCollectionProperties()), + NormalAction}, + {"akonadi_item_copy", nullptr, nullptr, "edit-copy", nullptr, 0, SLOT(slotCopyItems()), NormalAction}, + {"akonadi_paste", I18N_NOOP("&Paste"), I18N_NOOP("Paste"), "edit-paste", nullptr, Qt::CTRL | Qt::Key_V, SLOT(slotPaste()), NormalAction}, + {"akonadi_item_delete", nullptr, nullptr, "edit-delete", nullptr, 0, SLOT(slotDeleteItems()), NormalAction}, + {"akonadi_manage_local_subscriptions", + I18N_NOOP("Manage Local &Subscriptions..."), + I18N_NOOP("Manage Local Subscriptions"), + "folder-bookmarks", + nullptr, + 0, + SLOT(slotLocalSubscription()), + NormalAction}, + {"akonadi_collection_add_to_favorites", + I18N_NOOP("Add to Favorite Folders"), + I18N_NOOP("Add to Favorite"), + "bookmark-new", + nullptr, + 0, + SLOT(slotAddToFavorites()), + NormalAction}, + {"akonadi_collection_remove_from_favorites", + I18N_NOOP("Remove from Favorite Folders"), + I18N_NOOP("Remove from Favorite"), + "edit-delete", + nullptr, + 0, + SLOT(slotRemoveFromFavorites()), + NormalAction}, + {"akonadi_collection_rename_favorite", + I18N_NOOP("Rename Favorite..."), + I18N_NOOP("Rename"), + "edit-rename", + nullptr, + 0, + SLOT(slotRenameFavorite()), + NormalAction}, + {"akonadi_collection_copy_to_menu", + I18N_NOOP("Copy Folder To..."), + I18N_NOOP("Copy To"), + "edit-copy", + nullptr, + 0, + SLOT(slotCopyCollectionTo(QAction *)), + MenuAction}, + {"akonadi_item_copy_to_menu", I18N_NOOP("Copy Item To..."), I18N_NOOP("Copy To"), "edit-copy", nullptr, 0, SLOT(slotCopyItemTo(QAction *)), MenuAction}, + {"akonadi_item_move_to_menu", I18N_NOOP("Move Item To..."), I18N_NOOP("Move To"), "edit-move", "go-jump", 0, SLOT(slotMoveItemTo(QAction *)), MenuAction}, + {"akonadi_collection_move_to_menu", + I18N_NOOP("Move Folder To..."), + I18N_NOOP("Move To"), + "edit-move", + "go-jump", + 0, + SLOT(slotMoveCollectionTo(QAction *)), + MenuAction}, + {"akonadi_item_cut", I18N_NOOP("&Cut Item"), I18N_NOOP("Cut"), "edit-cut", nullptr, Qt::CTRL | Qt::Key_X, SLOT(slotCutItems()), NormalAction}, + {"akonadi_collection_cut", I18N_NOOP("&Cut Folder"), I18N_NOOP("Cut"), "edit-cut", nullptr, Qt::CTRL | Qt::Key_X, SLOT(slotCutCollections()), NormalAction}, + {"akonadi_resource_create", I18N_NOOP("Create Resource"), nullptr, "folder-new", nullptr, 0, SLOT(slotCreateResource()), NormalAction}, + {"akonadi_resource_delete", I18N_NOOP("Delete Resource"), nullptr, "edit-delete", nullptr, 0, SLOT(slotDeleteResource()), NormalAction}, + {"akonadi_resource_properties", + I18N_NOOP("&Resource Properties"), + I18N_NOOP("Properties"), + "configure", + nullptr, + 0, + SLOT(slotResourceProperties()), + NormalAction}, + {"akonadi_resource_synchronize", + I18N_NOOP("Synchronize Resource"), + I18N_NOOP("Synchronize"), + "view-refresh", + nullptr, + 0, + SLOT(slotSynchronizeResource()), + NormalAction}, + {"akonadi_work_offline", I18N_NOOP("Work Offline"), nullptr, "user-offline", nullptr, 0, SLOT(slotToggleWorkOffline(bool)), ToggleAction}, + {"akonadi_collection_copy_to_dialog", + I18N_NOOP("Copy Folder To..."), + I18N_NOOP("Copy To"), + "edit-copy", + nullptr, + 0, + SLOT(slotCopyCollectionTo()), + NormalAction}, + {"akonadi_collection_move_to_dialog", + I18N_NOOP("Move Folder To..."), + I18N_NOOP("Move To"), + "edit-move", + "go-jump", + 0, + SLOT(slotMoveCollectionTo()), + NormalAction}, + {"akonadi_item_copy_to_dialog", I18N_NOOP("Copy Item To..."), I18N_NOOP("Copy To"), "edit-copy", nullptr, 0, SLOT(slotCopyItemTo()), NormalAction}, + {"akonadi_item_move_to_dialog", I18N_NOOP("Move Item To..."), I18N_NOOP("Move To"), "edit-move", "go-jump", 0, SLOT(slotMoveItemTo()), NormalAction}, + {"akonadi_collection_sync_recursive", + I18N_NOOP("&Synchronize Folder Recursively"), + I18N_NOOP("Synchronize Recursively"), + "view-refresh", + nullptr, + Qt::CTRL | Qt::Key_F5, + SLOT(slotSynchronizeCollectionRecursive()), + NormalAction}, + {"akonadi_move_collection_to_trash", + I18N_NOOP("&Move Folder To Trash"), + I18N_NOOP("Move Folder To Trash"), + "edit-delete", + nullptr, + 0, + SLOT(slotMoveCollectionToTrash()), + NormalAction}, + {"akonadi_move_item_to_trash", + I18N_NOOP("&Move Item To Trash"), + I18N_NOOP("Move Item To Trash"), + "edit-delete", + nullptr, + 0, + SLOT(slotMoveItemToTrash()), + NormalAction}, + {"akonadi_restore_collection_from_trash", + I18N_NOOP("&Restore Folder From Trash"), + I18N_NOOP("Restore Folder From Trash"), + "view-refresh", + nullptr, + 0, + SLOT(slotRestoreCollectionFromTrash()), + NormalAction}, + {"akonadi_restore_item_from_trash", + I18N_NOOP("&Restore Item From Trash"), + I18N_NOOP("Restore Item From Trash"), + "view-refresh", + nullptr, + 0, + SLOT(slotRestoreItemFromTrash()), + NormalAction}, + {"akonadi_collection_trash_restore", + I18N_NOOP("&Restore Folder From Trash"), + I18N_NOOP("Restore Folder From Trash"), + "edit-delete", + nullptr, + 0, + SLOT(slotTrashRestoreCollection()), + ActionWithAlternative}, + {nullptr, I18N_NOOP("&Restore Collection From Trash"), I18N_NOOP("Restore Collection From Trash"), "view-refresh", nullptr, 0, nullptr, ActionAlternative}, + {"akonadi_item_trash_restore", + I18N_NOOP("&Restore Item From Trash"), + I18N_NOOP("Restore Item From Trash"), + "edit-delete", + nullptr, + 0, + SLOT(slotTrashRestoreItem()), + ActionWithAlternative}, + {nullptr, I18N_NOOP("&Restore Item From Trash"), I18N_NOOP("Restore Item From Trash"), "view-refresh", nullptr, 0, nullptr, ActionAlternative}, + {"akonadi_collection_sync_favorite_folders", + I18N_NOOP("&Synchronize Favorite Folders"), + I18N_NOOP("Synchronize Favorite Folders"), + "view-refresh", + nullptr, + Qt::CTRL | Qt::SHIFT | Qt::Key_L, + SLOT(slotSynchronizeFavoriteCollections()), + NormalAction}, + {"akonadi_resource_synchronize_collectiontree", + I18N_NOOP("Synchronize Folder Tree"), + I18N_NOOP("Synchronize"), + "view-refresh", + nullptr, + 0, + SLOT(slotSynchronizeCollectionTree()), + NormalAction}, + +}; +static const int numStandardActionData = sizeof standardActionData / sizeof *standardActionData; + +static QIcon standardActionDataIcon(const StandardActionData &data) +{ + if (data.altIcon) { + return QIcon::fromTheme(QString::fromLatin1(data.icon), QIcon::fromTheme(QString::fromLatin1(data.altIcon))); + } + return QIcon::fromTheme(QString::fromLatin1(data.icon)); +} + +static_assert(numStandardActionData == StandardActionManager::LastType, "StandardActionData table does not match StandardActionManager types"); + +static bool canCreateCollection(const Akonadi::Collection &collection) +{ + return !!(collection.rights() & Akonadi::Collection::CanCreateCollection); +} + +static void setWorkOffline(bool offline) +{ + KConfig config(QStringLiteral("akonadikderc")); + KConfigGroup group(&config, QStringLiteral("Actions")); + + group.writeEntry("WorkOffline", offline); +} + +static bool workOffline() +{ + KConfig config(QStringLiteral("akonadikderc")); + const KConfigGroup group(&config, QStringLiteral("Actions")); + + return group.readEntry("WorkOffline", false); +} + +static QModelIndexList safeSelectedRows(QItemSelectionModel *selectionModel) +{ + QModelIndexList selectedRows = selectionModel->selectedRows(); + if (!selectedRows.isEmpty()) { + return selectedRows; + } + + // try harder for selected rows that don't span the full row for some reason (e.g. due to buggy column adding proxy models etc) + const auto selection = selectionModel->selection(); + for (const auto &range : selection) { + if (!range.isValid() || range.isEmpty()) { + continue; + } + const QModelIndex parent = range.parent(); + for (int row = range.top(); row <= range.bottom(); ++row) { + const QModelIndex index = range.model()->index(row, range.left(), parent); + const Qt::ItemFlags flags = range.model()->flags(index); + if ((flags & Qt::ItemIsSelectable) && (flags & Qt::ItemIsEnabled)) { + selectedRows.push_back(index); + } + } + } + + return selectedRows; +} + +/** + * @internal + */ +class Q_DECL_HIDDEN StandardActionManager::Private +{ +public: + explicit Private(StandardActionManager *parent) + : q(parent) + , actionCollection(nullptr) + , parentWidget(nullptr) + , collectionSelectionModel(nullptr) + , itemSelectionModel(nullptr) + , favoritesModel(nullptr) + , favoriteSelectionModel(nullptr) + , insideSelectionSlot(false) + { + actions.fill(nullptr, StandardActionManager::LastType); + + pluralLabels.insert(StandardActionManager::CopyCollections, ki18np("&Copy Folder", "&Copy %1 Folders")); + pluralLabels.insert(StandardActionManager::CopyItems, ki18np("&Copy Item", "&Copy %1 Items")); + pluralLabels.insert(StandardActionManager::CutItems, ki18ncp("@action", "&Cut Item", "&Cut %1 Items")); + pluralLabels.insert(StandardActionManager::CutCollections, ki18ncp("@action", "&Cut Folder", "&Cut %1 Folders")); + pluralLabels.insert(StandardActionManager::DeleteItems, ki18np("&Delete Item", "&Delete %1 Items")); + pluralLabels.insert(StandardActionManager::DeleteCollections, ki18ncp("@action", "&Delete Folder", "&Delete %1 Folders")); + pluralLabels.insert(StandardActionManager::SynchronizeCollections, ki18ncp("@action", "&Synchronize Folder", "&Synchronize %1 Folders")); + pluralLabels.insert(StandardActionManager::DeleteResources, ki18np("&Delete Resource", "&Delete %1 Resources")); + pluralLabels.insert(StandardActionManager::SynchronizeResources, ki18np("&Synchronize Resource", "&Synchronize %1 Resources")); + + pluralIconLabels.insert(StandardActionManager::CopyCollections, ki18np("Copy Folder", "Copy %1 Folders")); + pluralIconLabels.insert(StandardActionManager::CopyItems, ki18np("Copy Item", "Copy %1 Items")); + pluralIconLabels.insert(StandardActionManager::CutItems, ki18np("Cut Item", "Cut %1 Items")); + pluralIconLabels.insert(StandardActionManager::CutCollections, ki18np("Cut Folder", "Cut %1 Folders")); + pluralIconLabels.insert(StandardActionManager::DeleteItems, ki18np("Delete Item", "Delete %1 Items")); + pluralIconLabels.insert(StandardActionManager::DeleteCollections, ki18np("Delete Folder", "Delete %1 Folders")); + pluralIconLabels.insert(StandardActionManager::SynchronizeCollections, ki18np("Synchronize Folder", "Synchronize %1 Folders")); + pluralIconLabels.insert(StandardActionManager::DeleteResources, ki18ncp("@action", "Delete Resource", "Delete %1 Resources")); + pluralIconLabels.insert(StandardActionManager::SynchronizeResources, ki18ncp("@action", "Synchronize Resource", "Synchronize %1 Resources")); + + setContextText(StandardActionManager::CreateCollection, StandardActionManager::DialogTitle, i18nc("@title:window", "New Folder")); + setContextText(StandardActionManager::CreateCollection, StandardActionManager::DialogText, i18nc("@label:textbox name of Akonadi folder", "Name")); + setContextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageText, ki18n("Could not create folder: %1")); + setContextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageTitle, i18n("Folder creation failed")); + + setContextText( + StandardActionManager::DeleteCollections, + StandardActionManager::MessageBoxText, + ki18np("Do you really want to delete this folder and all its sub-folders?", "Do you really want to delete %1 folders and all their sub-folders?")); + setContextText(StandardActionManager::DeleteCollections, + StandardActionManager::MessageBoxTitle, + ki18ncp("@title:window", "Delete folder?", "Delete folders?")); + setContextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageText, ki18n("Could not delete folder: %1")); + setContextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageTitle, i18n("Folder deletion failed")); + + setContextText(StandardActionManager::CollectionProperties, StandardActionManager::DialogTitle, ki18nc("@title:window", "Properties of Folder %1")); + + setContextText(StandardActionManager::DeleteItems, + StandardActionManager::MessageBoxText, + ki18np("Do you really want to delete the selected item?", "Do you really want to delete %1 items?")); + setContextText(StandardActionManager::DeleteItems, StandardActionManager::MessageBoxTitle, ki18ncp("@title:window", "Delete item?", "Delete items?")); + setContextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageText, ki18n("Could not delete item: %1")); + setContextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageTitle, i18n("Item deletion failed")); + + setContextText(StandardActionManager::RenameFavoriteCollection, StandardActionManager::DialogTitle, i18nc("@title:window", "Rename Favorite")); + setContextText(StandardActionManager::RenameFavoriteCollection, StandardActionManager::DialogText, i18nc("@label:textbox name of the folder", "Name:")); + + setContextText(StandardActionManager::CreateResource, StandardActionManager::DialogTitle, i18nc("@title:window", "New Resource")); + setContextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageText, ki18n("Could not create resource: %1")); + setContextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageTitle, i18n("Resource creation failed")); + + setContextText(StandardActionManager::DeleteResources, + StandardActionManager::MessageBoxText, + ki18np("Do you really want to delete this resource?", "Do you really want to delete %1 resources?")); + setContextText(StandardActionManager::DeleteResources, + StandardActionManager::MessageBoxTitle, + ki18ncp("@title:window", "Delete Resource?", "Delete Resources?")); + + setContextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageText, ki18n("Could not paste data: %1")); + setContextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageTitle, i18n("Paste failed")); + + mDelayedUpdateTimer.setSingleShot(true); + connect(&mDelayedUpdateTimer, &QTimer::timeout, q, [this]() { + updateActions(); + }); + + qRegisterMetaType("Akonadi::Item::List"); + } + + void enableAction(int type, bool enable) // private slot, called by ActionStateManager + { + enableAction(static_cast(type), enable); + } + + void enableAction(StandardActionManager::Type type, bool enable) + { + Q_ASSERT(type < StandardActionManager::LastType); + if (actions[type]) { + actions[type]->setEnabled(enable); + } + + // Update the action menu + auto actionMenu = qobject_cast(actions[type]); + if (actionMenu) { + // get rid of the submenus, they are re-created in enableAction. clear() is not enough, doesn't remove the submenu object instances. + QMenu *menu = actionMenu->menu(); + // Not necessary to delete and recreate menu when it was not created + if (menu->property("actionType").isValid() && menu->isEmpty()) { + return; + } + mRecentCollectionsMenu.remove(type); + delete menu; + menu = new QMenu(); + + menu->setProperty("actionType", static_cast(type)); + q->connect(menu, &QMenu::aboutToShow, q, [this]() { + aboutToShowMenu(); + }); + q->connect(menu, SIGNAL(triggered(QAction *)), standardActionData[type].slot); // clazy:exclude=old-style-connect + actionMenu->setMenu(menu); + } + } + + void aboutToShowMenu() + { + auto menu = qobject_cast(q->sender()); + if (!menu) { + return; + } + + if (!menu->isEmpty()) { + return; + } + // collect all selected collections + const Akonadi::Collection::List selectedCollectionsList = selectedCollections(); + const StandardActionManager::Type type = static_cast(menu->property("actionType").toInt()); + + QPointer recentCollection = new RecentCollectionAction(type, selectedCollectionsList, collectionSelectionModel->model(), menu); + mRecentCollectionsMenu.insert(type, recentCollection); + const QSet mimeTypes = mimeTypesOfSelection(type); + fillFoldersMenu(selectedCollectionsList, mimeTypes, type, menu, collectionSelectionModel->model(), QModelIndex()); + } + + void createActionFolderMenu(QMenu *menu, StandardActionManager::Type type) + { + if (type == CopyCollectionToMenu || type == CopyItemToMenu || type == MoveItemToMenu || type == MoveCollectionToMenu) { + new RecentCollectionAction(type, Akonadi::Collection::List(), collectionSelectionModel->model(), menu); + Collection::List selectedCollectionsList = selectedCollections(); + const QSet mimeTypes = mimeTypesOfSelection(type); + fillFoldersMenu(selectedCollectionsList, mimeTypes, type, menu, collectionSelectionModel->model(), QModelIndex()); + } + } + + void updateAlternatingAction(int type) // private slot, called by ActionStateManager + { + updateAlternatingAction(static_cast(type)); + } + + void updateAlternatingAction(StandardActionManager::Type type) + { + Q_ASSERT(type < StandardActionManager::LastType); + if (!actions[type]) { + return; + } + + /* + * The same action is stored at the ActionWithAlternative indexes as well as the corresponding ActionAlternative indexes in the actions array. + * The following simply changes the standardActionData + */ + if ((standardActionData[type].actionType == ActionWithAlternative) || (standardActionData[type].actionType == ActionAlternative)) { + actions[type]->setText(i18n(standardActionData[type].label)); + actions[type]->setIcon(standardActionDataIcon(standardActionData[type])); + + if (pluralLabels.contains(type) && !pluralLabels.value(type).isEmpty()) { + actions[type]->setText(pluralLabels.value(type).subs(1).toString()); + } else if (standardActionData[type].label) { + actions[type]->setText(i18n(standardActionData[type].label)); + } + + if (pluralIconLabels.contains(type) && !pluralIconLabels.value(type).isEmpty()) { + actions[type]->setIconText(pluralIconLabels.value(type).subs(1).toString()); + } else if (standardActionData[type].iconLabel) { + actions[type]->setIconText(i18n(standardActionData[type].iconLabel)); + } + + if (standardActionData[type].icon) { + actions[type]->setIcon(standardActionDataIcon(standardActionData[type])); + } + + // actions[type]->setShortcut( standardActionData[type].shortcut ); + + /*if ( standardActionData[type].slot ) { + switch ( standardActionData[type].actionType ) { + case NormalAction: + case ActionWithAlternative: + connect( action, SIGNAL(triggered()), standardActionData[type].slot ); + break; + } + }*/ + } + } + + void updatePluralLabel(int type, int count) // private slot, called by ActionStateManager + { + updatePluralLabel(static_cast(type), count); + } + + void updatePluralLabel(StandardActionManager::Type type, int count) // private slot, called by ActionStateManager + { + Q_ASSERT(type < StandardActionManager::LastType); + if (actions[type] && pluralLabels.contains(type) && !pluralLabels.value(type).isEmpty()) { + actions[type]->setText(pluralLabels.value(type).subs(qMax(count, 1)).toString()); + } + } + + bool isFavoriteCollection(const Akonadi::Collection &collection) const // private slot, called by ActionStateManager + { + if (!favoritesModel) { + return false; + } + + return favoritesModel->collectionIds().contains(collection.id()); + } + + void encodeToClipboard(QItemSelectionModel *selectionModel, bool cut = false) + { + Q_ASSERT(selectionModel); + if (safeSelectedRows(selectionModel).isEmpty()) { + return; + } + +#ifndef QT_NO_CLIPBOARD + auto model = const_cast(selectionModel->model()); + QMimeData *mimeData = selectionModel->model()->mimeData(safeSelectedRows(selectionModel)); + model->setData(QModelIndex(), false, EntityTreeModel::PendingCutRole); + markCutAction(mimeData, cut); + QApplication::clipboard()->setMimeData(mimeData); + if (cut) { + const auto rows = safeSelectedRows(selectionModel); + for (const auto &index : rows) { + model->setData(index, true, EntityTreeModel::PendingCutRole); + } + } +#endif + } + + static Akonadi::Collection::List collectionsForIndexes(const QModelIndexList &list) + { + Akonadi::Collection::List collectionList; + for (const QModelIndex &index : list) { + auto collection = index.data(EntityTreeModel::CollectionRole).value(); + if (!collection.isValid()) { + continue; + } + + const auto parentCollection = index.data(EntityTreeModel::ParentCollectionRole).value(); + collection.setParentCollection(parentCollection); + + collectionList << std::move(collection); + } + return collectionList; + } + + void delayedUpdateActions() + { + // Compress changes (e.g. when deleting many rows, do this only once) + mDelayedUpdateTimer.start(0); + } + + void updateActions() + { + // favorite collections + Collection::List selectedFavoriteCollectionsList; + if (favoriteSelectionModel) { + const QModelIndexList rows = safeSelectedRows(favoriteSelectionModel); + selectedFavoriteCollectionsList = collectionsForIndexes(rows); + } + + // collect all selected collections + Collection::List selectedCollectionsList; + if (collectionSelectionModel) { + const QModelIndexList rows = safeSelectedRows(collectionSelectionModel); + selectedCollectionsList = collectionsForIndexes(rows); + } + + // collect all selected items + Item::List selectedItems; + if (itemSelectionModel) { + const QModelIndexList rows = safeSelectedRows(itemSelectionModel); + for (const QModelIndex &index : rows) { + Item item = index.data(EntityTreeModel::ItemRole).value(); + if (!item.isValid()) { + continue; + } + + const auto parentCollection = index.data(EntityTreeModel::ParentCollectionRole).value(); + item.setParentCollection(parentCollection); + + selectedItems << item; + } + } + + mActionStateManager.updateState(selectedCollectionsList, selectedFavoriteCollectionsList, selectedItems); + if (favoritesModel) { + enableAction(StandardActionManager::SynchronizeFavoriteCollections, (favoritesModel->rowCount() > 0)); + } + Q_EMIT q->selectionsChanged(selectedCollectionsList, selectedFavoriteCollectionsList, selectedItems); + Q_EMIT q->actionStateUpdated(); + } + +#ifndef QT_NO_CLIPBOARD + void clipboardChanged(QClipboard::Mode mode) + { + if (mode == QClipboard::Clipboard) { + updateActions(); + } + } +#endif + + QItemSelection mapToEntityTreeModel(const QAbstractItemModel *model, const QItemSelection &selection) const + { + const auto proxy = qobject_cast(model); + if (proxy) { + return mapToEntityTreeModel(proxy->sourceModel(), proxy->mapSelectionToSource(selection)); + } else { + return selection; + } + } + + QItemSelection mapFromEntityTreeModel(const QAbstractItemModel *model, const QItemSelection &selection) const + { + const auto proxy = qobject_cast(model); + if (proxy) { + const QItemSelection select = mapFromEntityTreeModel(proxy->sourceModel(), selection); + return proxy->mapSelectionFromSource(select); + } else { + return selection; + } + } + + // RAII class for setting insideSelectionSlot to true on entering, and false on exiting, the two slots below. + class InsideSelectionSlotBlocker + { + public: + explicit InsideSelectionSlotBlocker(Private *p) + : _p(p) + { + Q_ASSERT(!p->insideSelectionSlot); + p->insideSelectionSlot = true; + } + + ~InsideSelectionSlotBlocker() + { + Q_ASSERT(_p->insideSelectionSlot); + _p->insideSelectionSlot = false; + } + + private: + Q_DISABLE_COPY(InsideSelectionSlotBlocker) + Private *_p; + }; + + void collectionSelectionChanged() + { + if (insideSelectionSlot) { + return; + } + InsideSelectionSlotBlocker block(this); + if (favoriteSelectionModel) { + QItemSelection selection = collectionSelectionModel->selection(); + selection = mapToEntityTreeModel(collectionSelectionModel->model(), selection); + selection = mapFromEntityTreeModel(favoriteSelectionModel->model(), selection); + + favoriteSelectionModel->select(selection, QItemSelectionModel::ClearAndSelect); + } + + updateActions(); + } + + void favoriteSelectionChanged() + { + if (insideSelectionSlot) { + return; + } + QItemSelection selection = favoriteSelectionModel->selection(); + if (selection.isEmpty()) { + return; + } + + selection = mapToEntityTreeModel(favoriteSelectionModel->model(), selection); + selection = mapFromEntityTreeModel(collectionSelectionModel->model(), selection); + + InsideSelectionSlotBlocker block(this); + collectionSelectionModel->select(selection, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + + // Also set the current index. This will trigger KMMainWidget::slotFolderChanged in kmail, which we want. + if (!selection.indexes().isEmpty()) { + collectionSelectionModel->setCurrentIndex(selection.indexes().first(), QItemSelectionModel::NoUpdate); + } + + updateActions(); + } + + void slotCreateCollection() + { + Q_ASSERT(collectionSelectionModel); + if (collectionSelectionModel->selection().indexes().isEmpty()) { + return; + } + + const QModelIndex index = collectionSelectionModel->selection().indexes().at(0); + Q_ASSERT(index.isValid()); + const auto parentCollection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(parentCollection.isValid()); + + if (!canCreateCollection(parentCollection)) { + return; + } + + bool ok = false; + QString name = QInputDialog::getText(parentWidget, + contextText(StandardActionManager::CreateCollection, StandardActionManager::DialogTitle), + contextText(StandardActionManager::CreateCollection, StandardActionManager::DialogText), + {}, + {}, + &ok); + name = name.trimmed(); + if (name.isEmpty() || !ok) { + return; + } + + if (name.contains(QLatin1Char('/'))) { + KMessageBox::error(parentWidget, i18n("We can not add \"/\" in folder name."), i18n("Create new folder error")); + return; + } + if (name.startsWith(QLatin1Char('.')) || name.endsWith(QLatin1Char('.'))) { + KMessageBox::error(parentWidget, i18n("We can not add \".\" at begin or end of folder name."), i18n("Create new folder error")); + return; + } + + Collection collection; + collection.setName(name); + collection.setParentCollection(parentCollection); + if (actions[StandardActionManager::CreateCollection]) { + const QStringList mts = actions[StandardActionManager::CreateCollection]->property("ContentMimeTypes").toStringList(); + if (!mts.isEmpty()) { + collection.setContentMimeTypes(mts); + } + } + if (parentCollection.contentMimeTypes().contains(Collection::virtualMimeType())) { + collection.setVirtual(true); + collection.setContentMimeTypes(collection.contentMimeTypes() << Collection::virtualMimeType()); + } + + auto job = new CollectionCreateJob(collection); + q->connect(job, &KJob::result, q, [this](KJob *job) { + collectionCreationResult(job); + }); + } + + void slotCopyCollections() + { + encodeToClipboard(collectionSelectionModel); + } + + void slotCutCollections() + { + encodeToClipboard(collectionSelectionModel, true); + } + + Collection::List selectedCollections() + { + Collection::List collections; + + Q_ASSERT(collectionSelectionModel); + const QModelIndexList indexes = safeSelectedRows(collectionSelectionModel); + collections.reserve(indexes.count()); + + for (const QModelIndex &index : indexes) { + Q_ASSERT(index.isValid()); + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + collections << collection; + } + + return collections; + } + + void slotDeleteCollection() + { + const Collection::List collections = selectedCollections(); + if (collections.isEmpty()) { + return; + } + + const QString collectionName = collections.first().name(); + const QString text = contextText(StandardActionManager::DeleteCollections, StandardActionManager::MessageBoxText, collections.count(), collectionName); + + if (KMessageBox::questionYesNo( + parentWidget, + text, + contextText(StandardActionManager::DeleteCollections, StandardActionManager::MessageBoxTitle, collections.count(), collectionName), + KStandardGuiItem::del(), + KStandardGuiItem::cancel(), + QString(), + KMessageBox::Dangerous) + != KMessageBox::Yes) { + return; + } + + for (const Collection &collection : collections) { + auto job = new CollectionDeleteJob(collection, q); + q->connect(job, &CollectionDeleteJob::result, q, [this](KJob *job) { + collectionDeletionResult(job); + }); + } + } + + void slotMoveCollectionToTrash() + { + const Collection::List collections = selectedCollections(); + if (collections.isEmpty()) { + return; + } + + for (const Collection &collection : collections) { + auto job = new TrashJob(collection, q); + q->connect(job, &TrashJob::result, q, [this](KJob *job) { + moveCollectionToTrashResult(job); + }); + } + } + + void slotRestoreCollectionFromTrash() + { + const Collection::List collections = selectedCollections(); + if (collections.isEmpty()) { + return; + } + + for (const Collection &collection : collections) { + auto job = new TrashRestoreJob(collection, q); + q->connect(job, &TrashRestoreJob::result, q, [this](KJob *job) { + moveCollectionToTrashResult(job); + }); + } + } + + Item::List selectedItems() const + { + Item::List items; + + Q_ASSERT(itemSelectionModel); + const QModelIndexList indexes = safeSelectedRows(itemSelectionModel); + items.reserve(indexes.count()); + + for (const QModelIndex &index : indexes) { + Q_ASSERT(index.isValid()); + const Item item = index.data(EntityTreeModel::ItemRole).value(); + Q_ASSERT(item.isValid()); + + items << item; + } + + return items; + } + + void slotMoveItemToTrash() + { + const Item::List items = selectedItems(); + if (items.isEmpty()) { + return; + } + + auto job = new TrashJob(items, q); + q->connect(job, &TrashJob::result, q, [this](KJob *job) { + moveItemToTrashResult(job); + }); + } + + void slotRestoreItemFromTrash() + { + const Item::List items = selectedItems(); + if (items.isEmpty()) { + return; + } + + auto job = new TrashRestoreJob(items, q); + q->connect(job, &TrashRestoreJob::result, q, [this](KJob *job) { + moveItemToTrashResult(job); + }); + } + + void slotTrashRestoreCollection() + { + const Collection::List collections = selectedCollections(); + if (collections.isEmpty()) { + return; + } + + bool collectionsAreInTrash = false; + for (const Collection &collection : collections) { + if (collection.hasAttribute()) { + collectionsAreInTrash = true; + break; + } + } + + if (collectionsAreInTrash) { + slotRestoreCollectionFromTrash(); + } else { + slotMoveCollectionToTrash(); + } + } + + void slotTrashRestoreItem() + { + const Item::List items = selectedItems(); + if (items.isEmpty()) { + return; + } + + bool itemsAreInTrash = false; + for (const Item &item : items) { + if (item.hasAttribute()) { + itemsAreInTrash = true; + break; + } + } + + if (itemsAreInTrash) { + slotRestoreItemFromTrash(); + } else { + slotMoveItemToTrash(); + } + } + + void slotSynchronizeCollection() + { + Q_ASSERT(collectionSelectionModel); + const QModelIndexList list = safeSelectedRows(collectionSelectionModel); + if (list.isEmpty()) { + return; + } + + const Collection::List collections = selectedCollections(); + if (collections.isEmpty()) { + return; + } + + for (const Collection &collection : collections) { + if (!testAndSetOnlineResources(collection)) { + break; + } + AgentManager::self()->synchronizeCollection(collection, false); + } + } + + bool testAndSetOnlineResources(const Akonadi::Collection &collection) + { + // Shortcut for the Search resource, which is a virtual resource and thus + // is always online (but AgentManager does not know about it, so it returns + // an invalid AgentInstance, which is "offline"). + // + // FIXME: AgentManager should return a valid AgentInstance even + // for virtual resources, which would be always online. + if (collection.resource() == QLatin1String("akonadi_search_resource")) { + return true; + } + + Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(collection.resource()); + if (!instance.isOnline()) { + if (KMessageBox::questionYesNo( + parentWidget, + i18n("Before syncing folder \"%1\" it is necessary to have the resource online. Do you want to make it online?", collection.displayName()), + i18n("Account \"%1\" is offline", instance.name()), + KGuiItem(i18nc("@action:button", "Go Online")), + KStandardGuiItem::cancel()) + != KMessageBox::Yes) { + return false; + } + instance.setIsOnline(true); + } + return true; + } + + void slotSynchronizeCollectionRecursive() + { + Q_ASSERT(collectionSelectionModel); + const QModelIndexList list = safeSelectedRows(collectionSelectionModel); + if (list.isEmpty()) { + return; + } + + const Collection::List collections = selectedCollections(); + if (collections.isEmpty()) { + return; + } + + for (const Collection &collection : collections) { + if (!testAndSetOnlineResources(collection)) { + break; + } + AgentManager::self()->synchronizeCollection(collection, true); + } + } + + void slotCollectionProperties() const + { + const QModelIndexList list = safeSelectedRows(collectionSelectionModel); + if (list.isEmpty()) { + return; + } + + const QModelIndex index = list.first(); + Q_ASSERT(index.isValid()); + + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + auto dlg = new CollectionPropertiesDialog(collection, mCollectionPropertiesPageNames, parentWidget); + dlg->setWindowTitle(contextText(StandardActionManager::CollectionProperties, StandardActionManager::DialogTitle, collection.displayName())); + dlg->show(); + } + + void slotCopyItems() + { + encodeToClipboard(itemSelectionModel); + } + + void slotCutItems() + { + encodeToClipboard(itemSelectionModel, true); + } + + void slotPaste() + { + Q_ASSERT(collectionSelectionModel); + + const QModelIndexList list = safeSelectedRows(collectionSelectionModel); + if (list.isEmpty()) { + return; + } + + const QModelIndex index = list.first(); + Q_ASSERT(index.isValid()); + +#ifndef QT_NO_CLIPBOARD + // TODO: Copy or move? We can't seem to cut yet + auto model = const_cast(collectionSelectionModel->model()); + const QMimeData *mimeData = QApplication::clipboard()->mimeData(); + model->dropMimeData(mimeData, isCutAction(mimeData) ? Qt::MoveAction : Qt::CopyAction, -1, -1, index); + model->setData(QModelIndex(), false, EntityTreeModel::PendingCutRole); + QApplication::clipboard()->clear(); +#endif + } + + void slotDeleteItems() + { + Q_ASSERT(itemSelectionModel); + + Item::List items; + const QModelIndexList indexes = safeSelectedRows(itemSelectionModel); + items.reserve(indexes.count()); + for (const QModelIndex &index : indexes) { + bool ok; + const qlonglong id = index.data(EntityTreeModel::ItemIdRole).toLongLong(&ok); + Q_ASSERT(ok); + items << Item(id); + } + + if (items.isEmpty()) { + return; + } + + QMetaObject::invokeMethod( + q, + [this, items] { + slotDeleteItemsDeferred(items); + }, + Qt::QueuedConnection); + } + + void slotDeleteItemsDeferred(const Akonadi::Item::List &items) + { + Q_ASSERT(itemSelectionModel); + + if (KMessageBox::questionYesNo(parentWidget, + contextText(StandardActionManager::DeleteItems, StandardActionManager::MessageBoxText, items.count(), QString()), + contextText(StandardActionManager::DeleteItems, StandardActionManager::MessageBoxTitle, items.count(), QString()), + KStandardGuiItem::del(), + KStandardGuiItem::cancel(), + QString(), + KMessageBox::Dangerous) + != KMessageBox::Yes) { + return; + } + + auto job = new ItemDeleteJob(items, q); + q->connect(job, &ItemDeleteJob::result, q, [this](KJob *job) { + itemDeletionResult(job); + }); + } + + void slotLocalSubscription() const + { + auto dlg = new SubscriptionDialog(mMimeTypeFilter, parentWidget); + dlg->showHiddenCollection(true); + dlg->show(); + } + + void slotAddToFavorites() + { + Q_ASSERT(collectionSelectionModel); + Q_ASSERT(favoritesModel); + const QModelIndexList list = safeSelectedRows(collectionSelectionModel); + if (list.isEmpty()) { + return; + } + + for (const QModelIndex &index : list) { + Q_ASSERT(index.isValid()); + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + favoritesModel->addCollection(collection); + } + + updateActions(); + } + + void slotRemoveFromFavorites() + { + Q_ASSERT(favoriteSelectionModel); + Q_ASSERT(favoritesModel); + const QModelIndexList list = safeSelectedRows(favoriteSelectionModel); + if (list.isEmpty()) { + return; + } + + for (const QModelIndex &index : list) { + Q_ASSERT(index.isValid()); + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + favoritesModel->removeCollection(collection); + } + + updateActions(); + } + + void slotRenameFavorite() + { + Q_ASSERT(favoriteSelectionModel); + Q_ASSERT(favoritesModel); + const QModelIndexList list = safeSelectedRows(favoriteSelectionModel); + if (list.isEmpty()) { + return; + } + const QModelIndex index = list.first(); + Q_ASSERT(index.isValid()); + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + QPointer dlg( + new RenameFavoriteDialog(favoritesModel->favoriteLabel(collection), favoritesModel->defaultFavoriteLabel(collection), parentWidget)); + if (dlg->exec() == QDialog::Accepted) { + favoritesModel->setFavoriteLabel(collection, dlg->newName()); + } + delete dlg; + } + + void slotSynchronizeFavoriteCollections() + { + Q_ASSERT(favoritesModel); + const auto collections = favoritesModel->collections(); + for (const auto &collection : collections) { + // there might be virtual collections in favorites which cannot be checked + // so let's be safe here, agentmanager asserts otherwise + if (!collection.resource().isEmpty()) { + AgentManager::self()->synchronizeCollection(collection, false); + } + } + } + + void slotCopyCollectionTo() + { + pasteTo(collectionSelectionModel, collectionSelectionModel->model(), CopyCollectionToMenu, Qt::CopyAction); + } + + void slotCopyItemTo() + { + pasteTo(itemSelectionModel, collectionSelectionModel->model(), CopyItemToMenu, Qt::CopyAction); + } + + void slotMoveCollectionTo() + { + pasteTo(collectionSelectionModel, collectionSelectionModel->model(), MoveCollectionToMenu, Qt::MoveAction); + } + + void slotMoveItemTo() + { + pasteTo(itemSelectionModel, collectionSelectionModel->model(), MoveItemToMenu, Qt::MoveAction); + } + + void slotCopyCollectionTo(QAction *action) + { + pasteTo(collectionSelectionModel, action, Qt::CopyAction); + } + + void slotCopyItemTo(QAction *action) + { + pasteTo(itemSelectionModel, action, Qt::CopyAction); + } + + void slotMoveCollectionTo(QAction *action) + { + pasteTo(collectionSelectionModel, action, Qt::MoveAction); + } + + void slotMoveItemTo(QAction *action) + { + pasteTo(itemSelectionModel, action, Qt::MoveAction); + } + + AgentInstance::List selectedAgentInstances() const + { + AgentInstance::List instances; + + Q_ASSERT(collectionSelectionModel); + if (collectionSelectionModel->selection().indexes().isEmpty()) { + return instances; + } + const QModelIndexList lstIndexes = collectionSelectionModel->selection().indexes(); + for (const QModelIndex &index : lstIndexes) { + Q_ASSERT(index.isValid()); + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + Q_ASSERT(collection.isValid()); + + if (collection.isValid()) { + const QString identifier = collection.resource(); + instances << AgentManager::self()->instance(identifier); + } + } + + return instances; + } + + AgentInstance selectedAgentInstance() const + { + const AgentInstance::List instances = selectedAgentInstances(); + + if (instances.isEmpty()) { + return AgentInstance(); + } + + return instances.first(); + } + + void slotCreateResource() + { + QPointer dlg(new Akonadi::AgentTypeDialog(parentWidget)); + dlg->setWindowTitle(contextText(StandardActionManager::CreateResource, StandardActionManager::DialogTitle)); + + for (const QString &mimeType : std::as_const(mMimeTypeFilter)) { + dlg->agentFilterProxyModel()->addMimeTypeFilter(mimeType); + } + + for (const QString &capability : std::as_const(mCapabilityFilter)) { + dlg->agentFilterProxyModel()->addCapabilityFilter(capability); + } + + if (dlg->exec() == QDialog::Accepted) { + const AgentType agentType = dlg->agentType(); + + if (agentType.isValid()) { + auto job = new AgentInstanceCreateJob(agentType, q); + q->connect(job, &KJob::result, q, [this](KJob *job) { + resourceCreationResult(job); + }); + job->configure(parentWidget); + job->start(); + } + } + delete dlg; + } + + void slotDeleteResource() const + { + const AgentInstance::List instances = selectedAgentInstances(); + if (instances.isEmpty()) { + return; + } + + if (KMessageBox::questionYesNo( + parentWidget, + contextText(StandardActionManager::DeleteResources, StandardActionManager::MessageBoxText, instances.count(), instances.first().name()), + contextText(StandardActionManager::DeleteResources, StandardActionManager::MessageBoxTitle, instances.count(), instances.first().name()), + KStandardGuiItem::del(), + KStandardGuiItem::cancel(), + QString(), + KMessageBox::Dangerous) + != KMessageBox::Yes) { + return; + } + + for (const AgentInstance &instance : instances) { + AgentManager::self()->removeInstance(instance); + } + } + + void slotSynchronizeResource() const + { + AgentInstance::List instances = selectedAgentInstances(); + for (AgentInstance &instance : instances) { + instance.synchronize(); + } + } + + void slotSynchronizeCollectionTree() const + { + AgentInstance::List instances = selectedAgentInstances(); + for (AgentInstance &instance : instances) { + instance.synchronizeCollectionTree(); + } + } + + void slotResourceProperties() const + { + AgentInstance instance = selectedAgentInstance(); + if (!instance.isValid()) { + return; + } + + instance.configure(parentWidget); + } + + void slotToggleWorkOffline(bool offline) + { + setWorkOffline(offline); + + AgentInstance::List instances = AgentManager::self()->instances(); + for (AgentInstance &instance : instances) { + instance.setIsOnline(!offline); + } + } + + void pasteTo(QItemSelectionModel *selectionModel, const QAbstractItemModel *model, StandardActionManager::Type type, Qt::DropAction dropAction) + { + const QSet mimeTypes = mimeTypesOfSelection(type); + + QPointer dlg(new CollectionDialog(const_cast(model))); + dlg->setMimeTypeFilter(mimeTypes.values()); + + if (type == CopyItemToMenu || type == MoveItemToMenu) { + dlg->setAccessRightsFilter(Collection::CanCreateItem); + } else if (type == CopyCollectionToMenu || type == MoveCollectionToMenu) { + dlg->setAccessRightsFilter(Collection::CanCreateCollection); + } + + if (dlg->exec() == QDialog::Accepted && dlg != nullptr) { + const QModelIndex index = EntityTreeModel::modelIndexForCollection(collectionSelectionModel->model(), dlg->selectedCollection()); + if (!index.isValid()) { + delete dlg; + return; + } + + const QMimeData *mimeData = selectionModel->model()->mimeData(safeSelectedRows(selectionModel)); + + auto model = const_cast(index.model()); + model->dropMimeData(mimeData, dropAction, -1, -1, index); + delete mimeData; + } + delete dlg; + } + + void pasteTo(QItemSelectionModel *selectionModel, QAction *action, Qt::DropAction dropAction) + { + Q_ASSERT(selectionModel); + Q_ASSERT(action); + + if (safeSelectedRows(selectionModel).count() <= 0) { + return; + } + + const QMimeData *mimeData = selectionModel->model()->mimeData(selectionModel->selectedRows()); + + const QModelIndex index = action->data().toModelIndex(); + Q_ASSERT(index.isValid()); + + auto model = const_cast(index.model()); + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + addRecentCollection(collection.id()); + model->dropMimeData(mimeData, dropAction, -1, -1, index); + delete mimeData; + } + + void addRecentCollection(Akonadi::Collection::Id id) const + { + QMapIterator> item(mRecentCollectionsMenu); + while (item.hasNext()) { + item.next(); + if (item.value().data()) { + item.value().data()->addRecentCollection(item.key(), id); + } + } + } + + void collectionCreationResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::CreateCollection, StandardActionManager::ErrorMessageTitle)); + } + } + + void collectionDeletionResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::DeleteCollections, StandardActionManager::ErrorMessageTitle)); + } + } + + void moveCollectionToTrashResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::MoveCollectionsToTrash, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::MoveCollectionsToTrash, StandardActionManager::ErrorMessageTitle)); + } + } + + void moveItemToTrashResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::MoveItemsToTrash, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::MoveItemsToTrash, StandardActionManager::ErrorMessageTitle)); + } + } + + void itemDeletionResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::DeleteItems, StandardActionManager::ErrorMessageTitle)); + } + } + + void resourceCreationResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::CreateResource, StandardActionManager::ErrorMessageTitle)); + } + } + + void pasteResult(KJob *job) const + { + if (job->error()) { + KMessageBox::error(parentWidget, + contextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageText, job->errorString()), + contextText(StandardActionManager::Paste, StandardActionManager::ErrorMessageTitle)); + } + } + + /** + * Returns a set of mime types of the entities that are currently selected. + */ + QSet mimeTypesOfSelection(StandardActionManager::Type type) const + { + QModelIndexList list; + QSet mimeTypes; + + const bool isItemAction = (type == CopyItemToMenu || type == MoveItemToMenu); + const bool isCollectionAction = (type == CopyCollectionToMenu || type == MoveCollectionToMenu); + + if (isItemAction) { + list = safeSelectedRows(itemSelectionModel); + mimeTypes.reserve(list.count()); + for (const QModelIndex &index : std::as_const(list)) { + mimeTypes << index.data(EntityTreeModel::MimeTypeRole).toString(); + } + } + + if (isCollectionAction) { + list = safeSelectedRows(collectionSelectionModel); + for (const QModelIndex &index : std::as_const(list)) { + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + + // The mimetypes that the selected collection can possibly contain + const auto mimeTypesResult = AgentManager::self()->instance(collection.resource()).type().mimeTypes(); + mimeTypes = QSet(mimeTypesResult.begin(), mimeTypesResult.end()); + } + } + + return mimeTypes; + } + + /** + * Returns whether items with the given @p mimeTypes can be written to the given @p collection. + */ + bool isWritableTargetCollectionForMimeTypes(const Collection &collection, const QSet &mimeTypes, StandardActionManager::Type type) const + { + if (collection.isVirtual()) { + return false; + } + + const bool isItemAction = (type == CopyItemToMenu || type == MoveItemToMenu); + const bool isCollectionAction = (type == CopyCollectionToMenu || type == MoveCollectionToMenu); + + const bool canContainRequiredMimeTypes = collection.contentMimeTypes().toSet().intersects(mimeTypes); + const bool canCreateNewItems = (collection.rights() & Collection::CanCreateItem); + + const bool canCreateNewCollections = (collection.rights() & Collection::CanCreateCollection); + const bool canContainCollections = + collection.contentMimeTypes().contains(Collection::mimeType()) || collection.contentMimeTypes().contains(Collection::virtualMimeType()); + + const bool resourceAllowsRequiredMimeTypes = AgentManager::self()->instance(collection.resource()).type().mimeTypes().toSet().contains(mimeTypes); + const bool isReadOnlyForItems = (isItemAction && (!canCreateNewItems || !canContainRequiredMimeTypes)); + const bool isReadOnlyForCollections = (isCollectionAction && (!canCreateNewCollections || !canContainCollections || !resourceAllowsRequiredMimeTypes)); + + return !(CollectionUtils::isStructural(collection) || isReadOnlyForItems || isReadOnlyForCollections); + } + + void fillFoldersMenu(const Akonadi::Collection::List &selectedCollectionsList, + const QSet &mimeTypes, + StandardActionManager::Type type, + QMenu *menu, + const QAbstractItemModel *model, + const QModelIndex &parentIndex) + { + const int rowCount = model->rowCount(parentIndex); + + for (int row = 0; row < rowCount; ++row) { + const QModelIndex index = model->index(row, 0, parentIndex); + const auto collection = model->data(index, EntityTreeModel::CollectionRole).value(); + + if (collection.isVirtual()) { + continue; + } + + const bool readOnly = !isWritableTargetCollectionForMimeTypes(collection, mimeTypes, type); + const bool collectionIsSelected = selectedCollectionsList.contains(collection); + if (type == MoveCollectionToMenu && collectionIsSelected) { + continue; + } + + QString label = model->data(index).toString(); + label.replace(QLatin1Char('&'), QStringLiteral("&&")); + + const auto icon = model->data(index, Qt::DecorationRole).value(); + + if (model->rowCount(index) > 0) { + // new level + auto popup = new QMenu(menu); + const bool moveAction = (type == MoveCollectionToMenu || type == MoveItemToMenu); + popup->setObjectName(QStringLiteral("subMenu")); + popup->setTitle(label); + popup->setIcon(icon); + + fillFoldersMenu(selectedCollectionsList, mimeTypes, type, popup, model, index); + if (!(type == CopyCollectionToMenu && collectionIsSelected)) { + if (!readOnly) { + popup->addSeparator(); + + QAction *action = popup->addAction(moveAction ? i18n("Move to This Folder") : i18n("Copy to This Folder")); + action->setData(QVariant::fromValue(index)); + } + } + + if (!popup->isEmpty()) { + menu->addMenu(popup); + } + + } else { + // insert an item + QAction *action = menu->addAction(icon, label); + action->setData(QVariant::fromValue(index)); + action->setEnabled(!readOnly && !collectionIsSelected); + } + } + } + + void checkModelsConsistency() const + { + if (favoritesModel == nullptr || favoriteSelectionModel == nullptr) { + // No need to check when the favorite collections feature is not used + return; + } + + // find the base ETM of the favourites view + const QAbstractItemModel *favModel = favoritesModel; + while (const auto proxy = qobject_cast(favModel)) { + favModel = proxy->sourceModel(); + } + + // Check that the collection selection model maps to the same + // EntityTreeModel than favoritesModel + if (collectionSelectionModel != nullptr) { + const QAbstractItemModel *model = collectionSelectionModel->model(); + while (const auto proxy = qobject_cast(model)) { + model = proxy->sourceModel(); + } + + Q_ASSERT(model == favModel); + } + + // Check that the favorite selection model maps to favoritesModel + const QAbstractItemModel *model = favoriteSelectionModel->model(); + while (const auto proxy = qobject_cast(model)) { + model = proxy->sourceModel(); + } + Q_ASSERT(model == favModel); + } + + void markCutAction(QMimeData *mimeData, bool cut) const + { + if (!cut) { + return; + } + + const QByteArray cutSelectionData = "1"; // krazy:exclude=doublequote_chars + mimeData->setData(QStringLiteral("application/x-kde.akonadi-cutselection"), cutSelectionData); + } + + bool isCutAction(const QMimeData *mimeData) const + { + const QByteArray data = mimeData->data(QStringLiteral("application/x-kde.akonadi-cutselection")); + if (data.isEmpty()) { + return false; + } else { + return (data.at(0) == '1'); // true if 1 + } + } + + void setContextText(StandardActionManager::Type type, StandardActionManager::TextContext context, const QString &data) + { + ContextTextEntry entry; + entry.text = data; + + contextTexts[type].insert(context, entry); + } + + void setContextText(StandardActionManager::Type type, StandardActionManager::TextContext context, const KLocalizedString &data) + { + ContextTextEntry entry; + entry.localizedText = data; + + contextTexts[type].insert(context, entry); + } + + QString contextText(StandardActionManager::Type type, StandardActionManager::TextContext context) const + { + return contextTexts[type].value(context).text; + } + + QString contextText(StandardActionManager::Type type, StandardActionManager::TextContext context, const QString &value) const + { + KLocalizedString text = contextTexts[type].value(context).localizedText; + if (text.isEmpty()) { + return contextTexts[type].value(context).text; + } + + return text.subs(value).toString(); + } + + QString contextText(StandardActionManager::Type type, StandardActionManager::TextContext context, int count, const QString &value) const + { + KLocalizedString text = contextTexts[type].value(context).localizedText; + if (text.isEmpty()) { + return contextTexts[type].value(context).text; + } + + const QString str = text.subs(count).toString(); + const int argCount = str.count(QRegularExpression(QStringLiteral("%[0-9]"))); + if (argCount > 0) { + return text.subs(count).subs(value).toString(); + } else { + return text.subs(count).toString(); + } + } + + StandardActionManager *const q; + KActionCollection *actionCollection; + QWidget *parentWidget; + QItemSelectionModel *collectionSelectionModel; + QItemSelectionModel *itemSelectionModel; + FavoriteCollectionsModel *favoritesModel; + QItemSelectionModel *favoriteSelectionModel; + bool insideSelectionSlot; + QVector actions; + QHash pluralLabels; + QHash pluralIconLabels; + QTimer mDelayedUpdateTimer; + + struct ContextTextEntry { + QString text; + KLocalizedString localizedText; + bool isLocalized; + }; + + using ContextTexts = QHash; + QHash contextTexts; + + ActionStateManager mActionStateManager; + + QStringList mMimeTypeFilter; + QStringList mCapabilityFilter; + QStringList mCollectionPropertiesPageNames; + QMap> mRecentCollectionsMenu; +}; + +/// @endcond + +StandardActionManager::StandardActionManager(KActionCollection *actionCollection, QWidget *parent) + : QObject(parent) + , d(new Private(this)) +{ + d->parentWidget = parent; + d->actionCollection = actionCollection; + d->mActionStateManager.setReceiver(this); +#ifndef QT_NO_CLIPBOARD + connect(QApplication::clipboard(), &QClipboard::changed, this, [this](auto mode) { + d->clipboardChanged(mode); + }); +#endif +} + +StandardActionManager::~StandardActionManager() +{ + delete d; +} + +void StandardActionManager::setCollectionSelectionModel(QItemSelectionModel *selectionModel) +{ + d->collectionSelectionModel = selectionModel; + connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() { + d->collectionSelectionChanged(); + }); + + d->checkModelsConsistency(); +} + +void StandardActionManager::setItemSelectionModel(QItemSelectionModel *selectionModel) +{ + d->itemSelectionModel = selectionModel; + connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() { + d->delayedUpdateActions(); + }); +} + +void StandardActionManager::setFavoriteCollectionsModel(FavoriteCollectionsModel *favoritesModel) +{ + d->favoritesModel = favoritesModel; + d->checkModelsConsistency(); +} + +void StandardActionManager::setFavoriteSelectionModel(QItemSelectionModel *selectionModel) +{ + d->favoriteSelectionModel = selectionModel; + connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this]() { + d->favoriteSelectionChanged(); + }); + d->checkModelsConsistency(); +} + +QAction *StandardActionManager::createAction(Type type) +{ + Q_ASSERT(type < LastType); + if (d->actions[type]) { + return d->actions[type]; + } + QAction *action = nullptr; + switch (standardActionData[type].actionType) { + case NormalAction: + case ActionWithAlternative: + action = new QAction(d->parentWidget); + break; + case ActionAlternative: + d->actions[type] = d->actions[type - 1]; + Q_ASSERT(d->actions[type]); + if ((LastType > type + 1) && (standardActionData[type + 1].actionType == ActionAlternative)) { + createAction(static_cast(type + 1)); // ensure that alternative actions are initialized when not created by createAllActions + } + return d->actions[type]; + case MenuAction: + action = new KActionMenu(d->parentWidget); + break; + case ToggleAction: + action = new KToggleAction(d->parentWidget); + break; + } + + if (d->pluralLabels.contains(type) && !d->pluralLabels.value(type).isEmpty()) { + action->setText(d->pluralLabels.value(type).subs(1).toString()); + } else if (standardActionData[type].label) { + action->setText(i18n(standardActionData[type].label)); + } + + if (d->pluralIconLabels.contains(type) && !d->pluralIconLabels.value(type).isEmpty()) { + action->setIconText(d->pluralIconLabels.value(type).subs(1).toString()); + } else if (standardActionData[type].iconLabel) { + action->setIconText(i18n(standardActionData[type].iconLabel)); + } + + if (standardActionData[type].icon) { + action->setIcon(standardActionDataIcon(standardActionData[type])); + } + if (d->actionCollection) { + d->actionCollection->setDefaultShortcut(action, QKeySequence(standardActionData[type].shortcut)); + } else { + action->setShortcut(standardActionData[type].shortcut); + } + + if (standardActionData[type].slot) { + switch (standardActionData[type].actionType) { + case NormalAction: + case ActionWithAlternative: + connect(action, SIGNAL(triggered()), standardActionData[type].slot); // clazy:exclude=old-style-connect + break; + case MenuAction: { + auto actionMenu = qobject_cast(action); + connect(actionMenu->menu(), SIGNAL(triggered(QAction *)), standardActionData[type].slot); // clazy:exclude=old-style-connect + break; + } + case ToggleAction: { + connect(action, SIGNAL(triggered(bool)), standardActionData[type].slot); // clazy:exclude=old-style-connect + break; + } + case ActionAlternative: + Q_ASSERT(0); + } + } + + if (type == ToggleWorkOffline) { + // inititalize the action state with information from config file + disconnect(action, SIGNAL(triggered(bool)), this, standardActionData[type].slot); // clazy:exclude=old-style-connect + action->setChecked(workOffline()); + connect(action, SIGNAL(triggered(bool)), this, standardActionData[type].slot); // clazy:exclude=old-style-connect + + // TODO: find a way to check for updates to the config file + } + + Q_ASSERT(standardActionData[type].name); + Q_ASSERT(d->actionCollection); + d->actionCollection->addAction(QString::fromLatin1(standardActionData[type].name), action); + d->actions[type] = action; + if ((standardActionData[type].actionType == ActionWithAlternative) && (standardActionData[type + 1].actionType == ActionAlternative)) { + createAction(static_cast(type + 1)); // ensure that alternative actions are initialized when not created by createAllActions + } + d->updateActions(); + return action; +} + +void StandardActionManager::createAllActions() +{ + for (uint i = 0; i < LastType; ++i) { + createAction(static_cast(i)); + } +} + +QAction *StandardActionManager::action(Type type) const +{ + Q_ASSERT(type < LastType); + return d->actions[type]; +} + +void StandardActionManager::setActionText(Type type, const KLocalizedString &text) +{ + Q_ASSERT(type < LastType); + d->pluralLabels.insert(type, text); + d->updateActions(); +} + +void StandardActionManager::interceptAction(Type type, bool intercept) +{ + Q_ASSERT(type < LastType); + + const QAction *action = d->actions[type]; + + if (!action) { + return; + } + + if (intercept) { + disconnect(action, SIGNAL(triggered()), this, standardActionData[type].slot); // clazy:exclude=old-style-connect + } else { + connect(action, SIGNAL(triggered()), standardActionData[type].slot); // clazy:exclude=old-style-connect + } +} + +Akonadi::Collection::List StandardActionManager::selectedCollections() const +{ + Collection::List collections; + + if (!d->collectionSelectionModel) { + return collections; + } + + const QModelIndexList lst = safeSelectedRows(d->collectionSelectionModel); + for (const QModelIndex &index : lst) { + const auto collection = index.data(EntityTreeModel::CollectionRole).value(); + if (collection.isValid()) { + collections << collection; + } + } + + return collections; +} + +Item::List StandardActionManager::selectedItems() const +{ + Item::List items; + + if (!d->itemSelectionModel) { + return items; + } + const QModelIndexList lst = safeSelectedRows(d->itemSelectionModel); + for (const QModelIndex &index : lst) { + const Item item = index.data(EntityTreeModel::ItemRole).value(); + if (item.isValid()) { + items << item; + } + } + + return items; +} + +void StandardActionManager::setContextText(Type type, TextContext context, const QString &text) +{ + d->setContextText(type, context, text); +} + +void StandardActionManager::setContextText(Type type, TextContext context, const KLocalizedString &text) +{ + d->setContextText(type, context, text); +} + +void StandardActionManager::setMimeTypeFilter(const QStringList &mimeTypes) +{ + d->mMimeTypeFilter = mimeTypes; +} + +void StandardActionManager::setCapabilityFilter(const QStringList &capabilities) +{ + d->mCapabilityFilter = capabilities; +} + +void StandardActionManager::setCollectionPropertiesPageNames(const QStringList &names) +{ + d->mCollectionPropertiesPageNames = names; +} + +void StandardActionManager::createActionFolderMenu(QMenu *menu, Type type) +{ + d->createActionFolderMenu(menu, type); +} + +void StandardActionManager::addRecentCollection(Akonadi::Collection::Id id) const +{ + RecentCollectionAction::addRecentCollection(id); +} + +#include "moc_standardactionmanager.cpp" diff --git a/src/widgets/standardactionmanager.h b/src/widgets/standardactionmanager.h new file mode 100644 index 0000000..7115b68 --- /dev/null +++ b/src/widgets/standardactionmanager.h @@ -0,0 +1,428 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +#include "collection.h" +#include "item.h" + +class QAction; +class KActionCollection; +class KLocalizedString; +class QItemSelectionModel; +class QWidget; +class QMenu; + +namespace Akonadi +{ +class FavoriteCollectionsModel; + +/** + * @short Manages generic actions for collection and item views. + * + * Manages generic Akonadi actions common for all types. This covers + * creating of the actions with appropriate labels, icons, shortcuts + * etc., updating the action state depending on the current selection + * as well as default implementations for the actual operations. + * + * If the default implementation is not appropriate for your application + * you can still use the state tracking by disconnecting the triggered() + * signal and re-connecting it to your implementation. The actual KAction + * objects can be retrieved by calling createAction() or action() for that. + * + * If the default look and feel (labels, icons, shortcuts) of the actions + * is not appropriate for your application, you can access them as noted + * above and customize them to your needs. Additionally, you can set a + * KLocalizedString which should be used as a action label with correct + * plural handling for actions operating on multiple objects with + * setActionText(). + * + * Finally, if you have special needs for the action states, connect to + * the actionStateUpdated() signal and adjust the state accordingly. + * + * The following actions are provided (KAction name in parenthesis): + * - Creation of a new collection (@c akonadi_collection_create) + * - Copying of selected collections (@c akonadi_collection_copy) + * - Deletion of selected collections (@c akonadi_collection_delete) + * - Synchronization of selected collections (@c akonadi_collection_sync) + * - Showing the collection properties dialog for the current collection (@c akonadi_collection_properties) + * - Copying of selected items (@c akonadi_itemcopy) + * - Pasting collections, items or raw data (@c akonadi_paste) + * - Deleting of selected items (@c akonadi_item_delete) + * - Managing local subscriptions (@c akonadi_manage_local_subscriptions) + * + * The following example shows how to use standard actions in your application: + * + * @code + * + * Akonadi::StandardActionManager *actMgr = new Akonadi::StandardActionManager( actionCollection(), this ); + * actMgr->setCollectionSelectionModel( collectionView->collectionSelectionModel() ); + * actMgr->createAllActions(); + * + * @endcode + * + * Additionally you have to add the actions to the KXMLGUI file of your application, + * using the names listed above. + * + * If you only need a subset of the actions provided, you can call createAction() + * instead of createAllActions() for the action types you want. + * + * If you want to use your own implementation of the actual action operation and + * not the default implementation, you can call interceptAction() on the action type + * you want to handle yourself and connect the slot with your own implementation + * to the triggered() signal of the action: + * + * @code + * + * using namespace Akonadi; + * + * StandardActionManager *manager = new StandardActionManager( actionCollection(), this ); + * manager->setCollectionSelectionModel( collectionView->collectionSelectionModel() ); + * manager->createAllActions(); + * + * // disable default implementation + * manager->interceptAction( StandardActionManager::CopyCollections ); + * + * // connect your own implementation + * connect( manager->action( StandardActionManager::CopyCollections ), SIGNAL(triggered(bool)), + * this, SLOT(myCopyImplementation()) ); + * ... + * + * void MyClass::myCopyImplementation() + * { + * const Collection::List collections = manager->selectedCollections(); + * for ( const Collection &collection : collections ) { + * // copy the collection manually... + * } + * } + * + * @endcode + * + * @todo collection deleting and sync do not support multi-selection yet + * + * @author Volker Krause + */ +class AKONADIWIDGETS_EXPORT StandardActionManager : public QObject +{ + Q_OBJECT +public: + /** + * Describes the supported actions. + */ + enum Type { + CreateCollection, ///< Creates an collection + CopyCollections, ///< Copies the selected collections + DeleteCollections, ///< Deletes the selected collections + SynchronizeCollections, ///< Synchronizes collections + CollectionProperties, ///< Provides collection properties + CopyItems, ///< Copies the selected items + Paste, ///< Paste collections or items + DeleteItems, ///< Deletes the selected items + ManageLocalSubscriptions, ///< Manages local subscriptions + AddToFavoriteCollections, ///< Add the collection to the favorite collections model @since 4.4 + RemoveFromFavoriteCollections, ///< Remove the collection from the favorite collections model @since 4.4 + RenameFavoriteCollection, ///< Rename the collection of the favorite collections model @since 4.4 + CopyCollectionToMenu, ///< Menu allowing to quickly copy a collection into another collection @since 4.4 + CopyItemToMenu, ///< Menu allowing to quickly copy an item into a collection @since 4.4 + MoveItemToMenu, ///< Menu allowing to move item into a collection @since 4.4 + MoveCollectionToMenu, ///< Menu allowing to move a collection into another collection @since 4.4 + CutItems, ///< Cuts the selected items @since 4.4 + CutCollections, ///< Cuts the selected collections @since 4.4 + CreateResource, ///< Creates a new resource @since 4.6 + DeleteResources, ///< Deletes the selected resources @since 4.6 + ResourceProperties, ///< Provides the resource properties @since 4.6 + SynchronizeResources, ///< Synchronizes the selected resources @since 4.6 + ToggleWorkOffline, ///< Toggles the work offline state of all resources @since 4.6 + CopyCollectionToDialog, ///< Copy a collection into another collection, select the target in a dialog @since 4.6 + MoveCollectionToDialog, ///< Move a collection into another collection, select the target in a dialog @since 4.6 + CopyItemToDialog, ///< Copy an item into a collection, select the target in a dialog @since 4.6 + MoveItemToDialog, ///< Move an item into a collection, select the target in a dialog @since 4.6 + SynchronizeCollectionsRecursive, ///< Synchronizes collections in a recursive way @since 4.6 + MoveCollectionsToTrash, ///< Moves the selected collection to trash and marks it as deleted, needs EntityDeletedAttribute @since 4.8 + MoveItemsToTrash, ///< Moves the selected items to trash and marks them as deleted, needs EntityDeletedAttribute @since 4.8 + RestoreCollectionsFromTrash, ///< Restores the selected collection from trash, needs EntityDeletedAttribute @since 4.8 + RestoreItemsFromTrash, ///< Restores the selected items from trash, needs EntityDeletedAttribute @since 4.8 + MoveToTrashRestoreCollection, ///< Move Collection to Trash or Restore it from Trash, needs EntityDeletedAttribute @since 4.8 + MoveToTrashRestoreCollectionAlternative, ///< Helper type for MoveToTrashRestoreCollection, do not create directly. Use this to override texts of the + ///< restore action. @since 4.8 + MoveToTrashRestoreItem, ///< Move Item to Trash or Restore it from Trash, needs EntityDeletedAttribute @since 4.8 + MoveToTrashRestoreItemAlternative, ///< Helper type for MoveToTrashRestoreItem, do not create directly. Use this to override texts of the restore + ///< action. @since 4.8 + SynchronizeFavoriteCollections, ///< Synchronize favorite collections @since 4.8 + SynchronizeCollectionTree, ///< Synchronize collection tree @since 4.15 + LastType ///< Marks last action + }; + + /** + * Describes the text context that can be customized. + */ + enum TextContext { + DialogTitle, ///< The window title of a dialog + DialogText, ///< The text of a dialog + MessageBoxTitle, ///< The window title of a message box + MessageBoxText, ///< The text of a message box + MessageBoxAlternativeText, ///< An alternative text of a message box + ErrorMessageTitle, ///< The window title of an error message + ErrorMessageText ///< The text of an error message + }; + + /** + * Creates a new standard action manager. + * + * @param actionCollection The action collection to operate on. + * @param parent The parent widget. + */ + explicit StandardActionManager(KActionCollection *actionCollection, QWidget *parent = nullptr); + + /** + * Destroys the standard action manager. + */ + ~StandardActionManager(); + + /** + * Sets the collection selection model based on which the collection + * related actions should operate. If none is set, all collection actions + * will be disabled. + * + * @param selectionModel model to be set for collection + */ + void setCollectionSelectionModel(QItemSelectionModel *selectionModel); + + /** + * Sets the item selection model based on which the item related actions + * should operate. If none is set, all item actions will be disabled. + * + * @param selectionModel selection model for items + */ + void setItemSelectionModel(QItemSelectionModel *selectionModel); + + /** + * Sets the favorite collections model based on which the collection + * relatedactions should operate. If none is set, the "Add to Favorite Folders" action + * will be disabled. + * + * @param favoritesModel model for the user's favorite collections + * @since 4.4 + */ + void setFavoriteCollectionsModel(FavoriteCollectionsModel *favoritesModel); + + /** + * Sets the favorite collection selection model based on which the favorite + * collection related actions should operate. If none is set, all favorite modifications + * actions will be disabled. + * + * @param selectionModel selection model for favorite collections + * @since 4.4 + */ + void setFavoriteSelectionModel(QItemSelectionModel *selectionModel); + + /** + * Creates the action of the given type and adds it to the action collection + * specified in the constructor if it does not exist yet. The action is + * connected to its default implementation provided by this class. + * + * @param type action to be created + */ + QAction *createAction(Type type); + + /** + * Convenience method to create all standard actions. + * @see createAction() + */ + void createAllActions(); + + /** + * Returns the action of the given type, 0 if it has not been created (yet). + * @param type action type + */ + QAction *action(Type type) const; + + /** + * Sets the label of the action @p type to @p text, which is used during + * updating the action state and substituted according to the number of + * selected objects. This is mainly useful to customize the label of actions + * that can operate on multiple objects. + * @param type the action to set a text for + * @param text the text to display for the given action + * Example: + * @code + * acctMgr->setActionText( Akonadi::StandardActionManager::CopyItems, + * ki18np( "Copy Mail", "Copy %1 Mails" ) ); + * @endcode + */ + void setActionText(Type type, const KLocalizedString &text); + + /** + * Sets whether the default implementation for the given action @p type + * shall be executed when the action is triggered. + * + * @param type action type + * @param intercept If @c false, the default implementation will be executed, + * if @c true no action is taken. + * + * @since 4.6 + */ + void interceptAction(Type type, bool intercept = true); + + /** + * Returns the list of collections that are currently selected. + * The list is empty if no collection is currently selected. + * + * @since 4.6 + */ + Akonadi::Collection::List selectedCollections() const; + + /** + * Returns the list of items that are currently selected. + * The list is empty if no item is currently selected. + * + * @since 4.6 + */ + Akonadi::Item::List selectedItems() const; + + /** + * Sets the @p text of the action @p type for the given @p context. + * + * @param type action type + * @param context context for action + * @param text content to set for the action + * @since 4.6 + */ + void setContextText(Type type, TextContext context, const QString &text); + + /** + * Sets the @p text of the action @p type for the given @p context. + * + * @param type action type + * @param context context for action + * @param text content to set for the action + * @since 4.6 + */ + void setContextText(Type type, TextContext context, const KLocalizedString &text); + + /** + * Sets the mime type filter that will be used when creating new resources. + * + * @param mimeTypes filter for creating new resources + * @since 4.6 + */ + void setMimeTypeFilter(const QStringList &mimeTypes); + + /** + * Sets the capability filter that will be used when creating new resources. + * + * @param capabilities filter for creating new resources + * @since 4.6 + */ + void setCapabilityFilter(const QStringList &capabilities); + + /** + * Sets the page @p names of the config pages that will be used by the + * built-in collection properties dialog. + * + * @param names list of names which will be used + * @since 4.6 + */ + void setCollectionPropertiesPageNames(const QStringList &names); + + /** + * Create a popup menu. + * + * @param menu parent menu for a popup + * @param type action type + * @since 4.8 + */ + void createActionFolderMenu(QMenu *menu, Type type); + + /** + * Add a collection to the global recent collection list. + * + * @param id the collection ID + * @since 5.18 + */ + void addRecentCollection(Akonadi::Collection::Id id) const; + +Q_SIGNALS: + + /** + * This signal is emitted whenever one of the selections has changed + * (selected collections, selected favorites collections, selected items) + * This allows other action managers to update their actions accordingly + * (see e.g. StandardMailActionManager) + */ + void selectionsChanged(const Collection::List &selectedCollectionsList, + const Collection::List &selectedFavoriteCollectionsList, + const Item::List &selectedItems); + + /** + * This signal is emitted whenever the action state has been updated. + * In case you have special needs for changing the state of some actions, + * connect to this signal and adjust the action state. + */ + void actionStateUpdated(); + +private: + /// @cond PRIVATE + class Private; + Private *const d; + + Q_PRIVATE_SLOT(d, void updateActions()) + + Q_PRIVATE_SLOT(d, void slotCreateCollection()) + Q_PRIVATE_SLOT(d, void slotCopyCollections()) + Q_PRIVATE_SLOT(d, void slotCutCollections()) + Q_PRIVATE_SLOT(d, void slotDeleteCollection()) + Q_PRIVATE_SLOT(d, void slotMoveCollectionToTrash()) + Q_PRIVATE_SLOT(d, void slotMoveItemToTrash()) + Q_PRIVATE_SLOT(d, void slotRestoreCollectionFromTrash()) + Q_PRIVATE_SLOT(d, void slotRestoreItemFromTrash()) + Q_PRIVATE_SLOT(d, void slotTrashRestoreCollection()) + Q_PRIVATE_SLOT(d, void slotTrashRestoreItem()) + Q_PRIVATE_SLOT(d, void slotSynchronizeCollection()) + Q_PRIVATE_SLOT(d, void slotSynchronizeCollectionRecursive()) + Q_PRIVATE_SLOT(d, void slotSynchronizeFavoriteCollections()) + Q_PRIVATE_SLOT(d, void slotCollectionProperties()) + Q_PRIVATE_SLOT(d, void slotCopyItems()) + Q_PRIVATE_SLOT(d, void slotCutItems()) + Q_PRIVATE_SLOT(d, void slotPaste()) + Q_PRIVATE_SLOT(d, void slotDeleteItems()) + Q_PRIVATE_SLOT(d, void slotDeleteItemsDeferred(const Akonadi::Item::List &)) + Q_PRIVATE_SLOT(d, void slotLocalSubscription()) + Q_PRIVATE_SLOT(d, void slotAddToFavorites()) + Q_PRIVATE_SLOT(d, void slotRemoveFromFavorites()) + Q_PRIVATE_SLOT(d, void slotRenameFavorite()) + Q_PRIVATE_SLOT(d, void slotCopyCollectionTo()) + Q_PRIVATE_SLOT(d, void slotMoveCollectionTo()) + Q_PRIVATE_SLOT(d, void slotCopyItemTo()) + Q_PRIVATE_SLOT(d, void slotMoveItemTo()) + Q_PRIVATE_SLOT(d, void slotCopyCollectionTo(QAction *)) + Q_PRIVATE_SLOT(d, void slotMoveCollectionTo(QAction *)) + Q_PRIVATE_SLOT(d, void slotCopyItemTo(QAction *)) + Q_PRIVATE_SLOT(d, void slotMoveItemTo(QAction *)) + Q_PRIVATE_SLOT(d, void slotCreateResource()) + Q_PRIVATE_SLOT(d, void slotDeleteResource()) + Q_PRIVATE_SLOT(d, void slotResourceProperties()) + Q_PRIVATE_SLOT(d, void slotSynchronizeResource()) + Q_PRIVATE_SLOT(d, void slotToggleWorkOffline(bool)) + Q_PRIVATE_SLOT(d, void slotSynchronizeCollectionTree()) + Q_PRIVATE_SLOT(d, void collectionCreationResult(KJob *)) + Q_PRIVATE_SLOT(d, void moveItemToTrashResult(KJob *)) + Q_PRIVATE_SLOT(d, void resourceCreationResult(KJob *)) + Q_PRIVATE_SLOT(d, void pasteResult(KJob *)) + + Q_PRIVATE_SLOT(d, void enableAction(int, bool)) + Q_PRIVATE_SLOT(d, void updatePluralLabel(int, int)) + Q_PRIVATE_SLOT(d, void updateAlternatingAction(int)) + Q_PRIVATE_SLOT(d, bool isFavoriteCollection(const Akonadi::Collection &)) + /// @endcond +}; + +} + diff --git a/src/widgets/subscriptiondialog.cpp b/src/widgets/subscriptiondialog.cpp new file mode 100644 index 0000000..6603961 --- /dev/null +++ b/src/widgets/subscriptiondialog.cpp @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "subscriptiondialog.h" +#include "ui_subscriptiondialog.h" + +#include "controlgui.h" +#include "monitor.h" +#include "recursivecollectionfilterproxymodel.h" +#include "subscriptionjob_p.h" +#include "subscriptionmodel_p.h" + +#include "akonadiwidgets_debug.h" +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Akonadi; + +/** + * @internal + */ +class Q_DECL_HIDDEN SubscriptionDialog::Private +{ +public: + explicit Private(SubscriptionDialog *parent) + : q(parent) + , model(&monitor, parent) + { + ui.setupUi(q); + + connect(&model, &SubscriptionModel::modelLoaded, q, [this]() { + filterRecursiveCollectionFilter.sort(0, Qt::AscendingOrder); + ui.collectionView->setEnabled(true); + ui.collectionView->expandAll(); + ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + }); + + filterRecursiveCollectionFilter.setSourceModel(&model); + filterRecursiveCollectionFilter.setFilterCaseSensitivity(Qt::CaseInsensitive); + filterRecursiveCollectionFilter.setSortRole(Qt::DisplayRole); + filterRecursiveCollectionFilter.setSortCaseSensitivity(Qt::CaseSensitive); + filterRecursiveCollectionFilter.setSortLocaleAware(true); + filterRecursiveCollectionFilter.setExcludeUnifiedMailBox(true); + + ui.collectionView->setModel(&filterRecursiveCollectionFilter); + ui.searchLineEdit->setFocus(); + q->connect(ui.searchLineEdit, &QLineEdit::textChanged, q, [this](const QString &str) { + filterRecursiveCollectionFilter.setSearchPattern(str); + ui.collectionView->expandAll(); + }); + q->connect(ui.subscribedOnlyCheckBox, &QCheckBox::toggled, q, [this](bool state) { + filterRecursiveCollectionFilter.setIncludeCheckedOnly(state); + }); + q->connect(ui.subscribeButton, &QPushButton::clicked, q, [this]() { + toggleSubscribed(Qt::Checked); + }); + q->connect(ui.unsubscribeButton, &QPushButton::clicked, q, [this]() { + toggleSubscribed(Qt::Unchecked); + }); + + auto okButton = ui.buttonBox->button(QDialogButtonBox::Ok); + okButton->setEnabled(false); + connect(okButton, &QPushButton::clicked, q, [this]() { + done(); + }); + } + + void done() + { + auto job = new SubscriptionJob(q); + job->subscribe(model.subscribed()); + job->unsubscribe(model.unsubscribed()); + connect(job, &SubscriptionJob::result, q, [this](KJob *job) { + if (job->error()) { + qCWarning(AKONADIWIDGETS_LOG) << job->errorString(); + KMessageBox::sorry(q, i18n("Failed to update subscription: %1", job->errorString()), i18nc("@title", "Subscription Error")); + q->reject(); + } + q->accept(); + }); + } + + void writeConfig() const + { + KConfigGroup group(KSharedConfig::openStateConfig(), "SubscriptionDialog"); + group.writeEntry("Size", q->size()); + } + + void readConfig() const + { + KConfigGroup group(KSharedConfig::openStateConfig(), "SubscriptionDialog"); + const QSize sizeDialog = group.readEntry("Size", QSize(500, 400)); + if (sizeDialog.isValid()) { + q->resize(sizeDialog); + } + } + + void toggleSubscribed(Qt::CheckState state) + { + const QModelIndexList list = ui.collectionView->selectionModel()->selectedIndexes(); + for (const QModelIndex &index : list) { + model.setData(index, state, Qt::CheckStateRole); + } + ui.collectionView->setFocus(); + } + + SubscriptionDialog *const q; + Ui::SubscriptionDialog ui; + + Monitor monitor; + SubscriptionModel model; + RecursiveCollectionFilterProxyModel filterRecursiveCollectionFilter; +}; + +SubscriptionDialog::SubscriptionDialog(QWidget *parent) + : SubscriptionDialog({}, parent) +{ +} + +SubscriptionDialog::SubscriptionDialog(const QStringList &mimetypes, QWidget *parent) + : QDialog(parent) + , d(new Private(this)) +{ + setAttribute(Qt::WA_DeleteOnClose); + + if (!mimetypes.isEmpty()) { + d->filterRecursiveCollectionFilter.addContentMimeTypeInclusionFilters(mimetypes); + } + ControlGui::widgetNeedsAkonadi(this); + d->readConfig(); +} + +SubscriptionDialog::~SubscriptionDialog() +{ + d->writeConfig(); +} + +void SubscriptionDialog::showHiddenCollection(bool showHidden) +{ + d->model.setShowHiddenCollections(showHidden); +} + +#include "moc_subscriptiondialog.cpp" diff --git a/src/widgets/subscriptiondialog.h b/src/widgets/subscriptiondialog.h new file mode 100644 index 0000000..c798366 --- /dev/null +++ b/src/widgets/subscriptiondialog.h @@ -0,0 +1,59 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +namespace Akonadi +{ +/** + * Local subscription dialog. + */ +class AKONADIWIDGETS_EXPORT SubscriptionDialog : public QDialog +{ + Q_OBJECT +public: + /** + * Creates a new subscription dialog. + * + * @param parent The parent widget. + */ + explicit SubscriptionDialog(QWidget *parent = nullptr); + + /** + * Creates a new subscription dialog. + * + * @param parent The parent widget. + * @param mimetypes The specific mimetypes + * @since 4.6 + */ + explicit SubscriptionDialog(const QStringList &mimetypes, QWidget *parent = nullptr); + + /** + * Destroys the subscription dialog. + * + * @note Don't call the destructor manually, the dialog will + * be destructed automatically as soon as all changes + * are written back to the server. + */ + ~SubscriptionDialog(); + + /** + * @param showHidden shows hidden collections if set as @c true + * @since 4.9 + */ + void showHiddenCollection(bool showHidden); + +private: + class Private; + QScopedPointer const d; +}; + +} + diff --git a/src/widgets/subscriptiondialog.ui b/src/widgets/subscriptiondialog.ui new file mode 100644 index 0000000..c422beb --- /dev/null +++ b/src/widgets/subscriptiondialog.ui @@ -0,0 +1,118 @@ + + + SubscriptionDialog + + + + 0 + 0 + 456 + 443 + + + + Local Subscriptions + + + + + + + + Search: + + + + + + + true + + + + + + + &Subscribed only + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ExtendedSelection + + + false + + + + + + + + + Su&bscribe + + + + + + + &Unsubscribe + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + rejected() + SubscriptionDialog + reject() + + + 423 + 421 + + + 456 + 287 + + + + + diff --git a/src/widgets/tageditwidget.cpp b/src/widgets/tageditwidget.cpp new file mode 100644 index 0000000..068ab0e --- /dev/null +++ b/src/widgets/tageditwidget.cpp @@ -0,0 +1,283 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "tageditwidget.h" +#include "changerecorder.h" +#include "tagattribute.h" +#include "tagcreatejob.h" +#include "tagdeletejob.h" +#include "tagfetchscope.h" +#include "tagmodel.h" +#include "ui_tageditwidget.h" + +#include +#include +#include + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN TagEditWidget::Private : public QObject +{ + Q_OBJECT +public: + explicit Private(QWidget *parent); + +public Q_SLOTS: + void slotTextEdited(const QString &text); + void slotItemEntered(const QModelIndex &index); + void deleteTag(); + void slotCreateTag(); + void slotCreateTagFinished(KJob *job); + void onRowsInserted(const QModelIndex &parent, int start, int end); + void onModelPopulated(); + +public: + void initCheckableProxy(Akonadi::TagModel *model) + { + Q_ASSERT(m_checkableProxy); + + auto selectionModel = new QItemSelectionModel(model, m_checkableProxy.get()); + m_checkableProxy->setSourceModel(model); + m_checkableProxy->setSelectionModel(selectionModel); + } + + void select(const QModelIndex &parent, int start, int end, QItemSelectionModel::SelectionFlag selectionFlag) const; + enum ItemType { + UrlTag = Qt::UserRole + 1, + }; + + QWidget *const d; + Ui::TagEditWidget ui; + + Akonadi::Tag::List m_tags; + Akonadi::TagModel *m_model = nullptr; + QScopedPointer m_checkableProxy; + QModelIndex m_deleteCandidate; + + QPushButton *m_deleteButton = nullptr; +}; + +TagEditWidget::Private::Private(QWidget *parent) + : d(parent) +{ +} + +void TagEditWidget::Private::select(const QModelIndex &parent, int start, int end, QItemSelectionModel::SelectionFlag selectionFlag) const +{ + if (!m_model) { + return; + } + + QItemSelection selection; + for (int i = start; i <= end; i++) { + const QModelIndex index = m_model->index(i, 0, parent); + const auto insertedTag = index.data(Akonadi::TagModel::TagRole).value(); + if (m_tags.contains(insertedTag)) { + selection.select(index, index); + } + } + if (m_checkableProxy) { + m_checkableProxy->selectionModel()->select(selection, selectionFlag); + } +} + +void TagEditWidget::Private::onModelPopulated() +{ + select(QModelIndex(), 0, m_model->rowCount() - 1, QItemSelectionModel::ClearAndSelect); +} + +void TagEditWidget::Private::onRowsInserted(const QModelIndex &parent, int start, int end) +{ + select(parent, start, end, QItemSelectionModel::Select); +} + +void TagEditWidget::Private::slotCreateTag() +{ + if (ui.newTagButton->isEnabled()) { + auto createJob = new TagCreateJob(Akonadi::Tag(ui.newTagEdit->text()), this); + connect(createJob, &TagCreateJob::finished, this, &TagEditWidget::Private::slotCreateTagFinished); + + ui.newTagEdit->clear(); + ui.newTagEdit->setEnabled(false); + ui.newTagButton->setEnabled(false); + } +} + +void TagEditWidget::Private::slotCreateTagFinished(KJob *job) +{ + if (job->error()) { + KMessageBox::error(d, i18n("Failed to create a new tag"), i18n("An error occurred while creating a new tag")); + } + + ui.newTagEdit->setEnabled(true); +} + +void TagEditWidget::Private::slotTextEdited(const QString &text) +{ + // Remove unnecessary spaces from a new tag is + // mandatory, as the user cannot see the difference + // between a tag "Test" and "Test ". + const QString tagText = text.simplified(); + if (tagText.isEmpty()) { + ui.newTagButton->setEnabled(false); + return; + } + + // Check whether the new tag already exists + bool exists = false; + for (int i = 0, count = m_model->rowCount(); i < count; ++i) { + const QModelIndex index = m_model->index(i, 0, QModelIndex()); + if (index.data(Qt::DisplayRole).toString() == tagText) { + exists = true; + break; + } + } + ui.newTagButton->setEnabled(!exists); +} + +void TagEditWidget::Private::slotItemEntered(const QModelIndex &index) +{ + // align the delete-button to stay on the right border + // of the item + const QRect rect = ui.tagsView->visualRect(index); + const int size = rect.height(); + const int x = rect.right() - size; + const int y = rect.top(); + m_deleteButton->move(x, y); + m_deleteButton->resize(size, size); + + m_deleteCandidate = index; + m_deleteButton->show(); +} + +void TagEditWidget::Private::deleteTag() +{ + Q_ASSERT(m_deleteCandidate.isValid()); + const auto tag = m_deleteCandidate.data(Akonadi::TagModel::TagRole).value(); + const QString text = xi18nc("@info", "Do you really want to remove the tag %1?", tag.name()); + const QString caption = i18nc("@title", "Delete tag"); + if (KMessageBox::questionYesNo(d, text, caption, KStandardGuiItem::del(), KStandardGuiItem::cancel()) == KMessageBox::Yes) { + new TagDeleteJob(tag, this); + } +} + +TagEditWidget::TagEditWidget(QWidget *parent) + : QWidget(parent) + , d(new Private(this)) +{ + d->ui.setupUi(this); + + d->ui.tagsView->installEventFilter(this); + connect(d->ui.tagsView, &QAbstractItemView::entered, d.get(), &Private::slotItemEntered); + + connect(d->ui.newTagEdit, &QLineEdit::textEdited, d.get(), &Private::slotTextEdited); + connect(d->ui.newTagEdit, &QLineEdit::returnPressed, d.get(), &Private::slotCreateTag); + connect(d->ui.newTagButton, &QAbstractButton::clicked, d.get(), &Private::slotCreateTag); + + // create the delete button, which is shown when + // hovering the items + d->m_deleteButton = new QPushButton(d->ui.tagsView->viewport()); + d->m_deleteButton->setObjectName(QStringLiteral("tagDeleteButton")); + d->m_deleteButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); + d->m_deleteButton->setToolTip(i18nc("@info", "Delete tag")); + d->m_deleteButton->hide(); + connect(d->m_deleteButton, &QAbstractButton::clicked, d.data(), &Private::deleteTag); +} + +TagEditWidget::TagEditWidget(Akonadi::TagModel *model, QWidget *parent, bool enableSelection) + : TagEditWidget(parent) +{ + setModel(model); + setSelectionEnabled(enableSelection); +} + +TagEditWidget::~TagEditWidget() = default; + +void TagEditWidget::setSelectionEnabled(bool enabled) +{ + if (enabled == (d->m_checkableProxy != nullptr)) { + return; + } + + if (enabled) { + d->m_checkableProxy.reset(new KCheckableProxyModel(this)); + if (d->m_model) { + d->initCheckableProxy(d->m_model); + } + d->ui.tagsView->setModel(d->m_checkableProxy.get()); + } else { + d->m_checkableProxy.reset(); + d->ui.tagsView->setModel(d->m_model); + } + d->ui.selectLabel->setVisible(enabled); +} + +void TagEditWidget::setModel(TagModel *model) +{ + if (d->m_model) { + disconnect(d->m_model, &QAbstractItemModel::rowsInserted, d.get(), &Private::onRowsInserted); + disconnect(d->m_model, &TagModel::populated, d.get(), &Private::onModelPopulated); + } + + d->m_model = model; + if (d->m_model) { + connect(d->m_model, &QAbstractItemModel::rowsInserted, d.get(), &Private::onRowsInserted); + if (d->m_checkableProxy) { + d->initCheckableProxy(d->m_model); + d->ui.tagsView->setModel(d->m_checkableProxy.get()); + } else { + d->ui.tagsView->setModel(d->m_model); + } + connect(d->m_model, &TagModel::populated, d.get(), &Private::onModelPopulated); + } +} + +TagModel *TagEditWidget::model() const +{ + return d->m_model; +} + +bool TagEditWidget::selectionEnabled() const +{ + return d->m_checkableProxy != nullptr; +} + +void TagEditWidget::setSelection(const Akonadi::Tag::List &tags) +{ + d->m_tags = tags; + d->select(QModelIndex(), 0, d->m_model->rowCount() - 1, QItemSelectionModel::ClearAndSelect); +} + +Akonadi::Tag::List TagEditWidget::selection() const +{ + if (!d->m_checkableProxy) { + return {}; + } + + Akonadi::Tag::List list; + for (int i = 0; i < d->m_checkableProxy->rowCount(); ++i) { + if (d->m_checkableProxy->selectionModel()->isRowSelected(i, QModelIndex())) { + const auto index = d->m_checkableProxy->index(i, 0, QModelIndex()); + const auto tag = index.data(TagModel::TagRole).value(); + list.push_back(tag); + } + } + return list; +} + +bool TagEditWidget::eventFilter(QObject *watched, QEvent *event) +{ + if ((watched == d->ui.tagsView) && (event->type() == QEvent::Leave)) { + d->m_deleteButton->hide(); + } + return QWidget::eventFilter(watched, event); +} + +#include "tageditwidget.moc" diff --git a/src/widgets/tageditwidget.h b/src/widgets/tageditwidget.h new file mode 100644 index 0000000..0311b03 --- /dev/null +++ b/src/widgets/tageditwidget.h @@ -0,0 +1,49 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "tag.h" +#include + +namespace Akonadi +{ +class TagModel; +/** + * A widget that offers facilities to add/remove tags and optionally provides a way to select tags. + * + * @since 4.13 + */ +class AKONADIWIDGETS_EXPORT TagEditWidget : public QWidget +{ + Q_OBJECT +public: + explicit TagEditWidget(QWidget *parent = nullptr); + explicit TagEditWidget(Akonadi::TagModel *model, QWidget *parent = nullptr, bool enableSelection = false); + ~TagEditWidget() override; + + void setModel(Akonadi::TagModel *model); + Akonadi::TagModel *model() const; + + void setSelectionEnabled(bool enabled); + bool selectionEnabled() const; + + void setSelection(const Akonadi::Tag::List &tags); + Q_REQUIRED_RESULT Akonadi::Tag::List selection() const; + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + class Private; + QScopedPointer d; +}; + +} + diff --git a/src/widgets/tageditwidget.ui b/src/widgets/tageditwidget.ui new file mode 100644 index 0000000..2c7e360 --- /dev/null +++ b/src/widgets/tageditwidget.ui @@ -0,0 +1,64 @@ + + + TagEditWidget + + + + 0 + 0 + 400 + 300 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Select tags that should be applied. + + + + + + + true + + + QAbstractItemView::NoSelection + + + + + + + + + + + + false + + + Create new tag + + + + + + + + + + diff --git a/src/widgets/tagmanagementdialog.cpp b/src/widgets/tagmanagementdialog.cpp new file mode 100644 index 0000000..a2123cb --- /dev/null +++ b/src/widgets/tagmanagementdialog.cpp @@ -0,0 +1,76 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagmanagementdialog.h" +#include "ui_tagmanagementdialog.h" + +#include "controlgui.h" +#include "monitor.h" +#include "tagmodel.h" + +#include +#include +#include + +using namespace Akonadi; + +struct Q_DECL_HIDDEN TagManagementDialog::Private { + explicit Private(QDialog *parent) + : q(parent) + { + } + + void writeConfig() const; + void readConfig() const; + + Ui::TagManagementDialog ui; + QDialog *const q; + QDialogButtonBox *buttonBox = nullptr; +}; + +void TagManagementDialog::Private::writeConfig() const +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "TagManagementDialog"); + group.writeEntry("Size", q->size()); +} + +void TagManagementDialog::Private::readConfig() const +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "TagManagementDialog"); + const QSize sizeDialog = group.readEntry("Size", QSize(500, 400)); + if (sizeDialog.isValid()) { + q->resize(sizeDialog); + } +} + +TagManagementDialog::TagManagementDialog(QWidget *parent) + : QDialog(parent) + , d(new Private(this)) +{ + auto monitor = new Monitor(this); + monitor->setObjectName(QStringLiteral("TagManagementDialogMonitor")); + monitor->setTypeMonitored(Monitor::Tags); + + d->ui.setupUi(this); + d->ui.tagEditWidget->setModel(new TagModel(monitor, this)); + d->ui.tagEditWidget->setSelectionEnabled(false); + + d->readConfig(); + + ControlGui::widgetNeedsAkonadi(this); +} + +TagManagementDialog::~TagManagementDialog() +{ + d->writeConfig(); +} + +QDialogButtonBox *TagManagementDialog::buttons() const +{ + return d->buttonBox; +} diff --git a/src/widgets/tagmanagementdialog.h b/src/widgets/tagmanagementdialog.h new file mode 100644 index 0000000..a712116 --- /dev/null +++ b/src/widgets/tagmanagementdialog.h @@ -0,0 +1,38 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include "tag.h" +#include +class QDialogButtonBox; +namespace Akonadi +{ +/** + * A dialog to manage tags. + * + * @since 4.13 + */ +class AKONADIWIDGETS_EXPORT TagManagementDialog : public QDialog +{ + Q_OBJECT +public: + explicit TagManagementDialog(QWidget *parent = nullptr); + ~TagManagementDialog() override; + + Q_REQUIRED_RESULT QDialogButtonBox *buttons() const; + +private: + struct Private; + QScopedPointer d; +}; + +} + diff --git a/src/widgets/tagmanagementdialog.ui b/src/widgets/tagmanagementdialog.ui new file mode 100644 index 0000000..2adde2e --- /dev/null +++ b/src/widgets/tagmanagementdialog.ui @@ -0,0 +1,72 @@ + + + TagManagementDialog + + + + 0 + 0 + 400 + 300 + + + + Manage Tags + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Akonadi::TagEditWidget + QWidget +
tageditwidget.h
+ 1 +
+
+ + + + buttonBox + accepted() + TagManagementDialog + accept() + + + 199 + 276 + + + 199 + 149 + + + + + buttonBox + rejected() + TagManagementDialog + reject() + + + 199 + 276 + + + 199 + 149 + + + + +
diff --git a/src/widgets/tagselectioncombobox.cpp b/src/widgets/tagselectioncombobox.cpp new file mode 100644 index 0000000..d4bb26e --- /dev/null +++ b/src/widgets/tagselectioncombobox.cpp @@ -0,0 +1,299 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "tagselectioncombobox.h" + +#include "monitor.h" +#include "tagmodel.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace Akonadi; + +namespace +{ +template List tagsFromSelection(const QItemSelection &selection, int role) +{ + List tags; + for (int i = 0; i < selection.size(); ++i) { + const auto indexes = selection.at(i).indexes(); + std::transform(indexes.cbegin(), indexes.cend(), std::back_inserter(tags), [role](const auto &idx) { + return idx.model()->data(idx, role).template value(); + }); + } + return tags; +} + +QString getEditText(const QItemSelection &selection) +{ + const auto tags = tagsFromSelection(selection, TagModel::TagRole); + QStringList names; + names.reserve(tags.size()); + std::transform(tags.cbegin(), tags.cend(), std::back_inserter(names), std::bind(&Tag::name, std::placeholders::_1)); + return QLocale{}.createSeparatedList(names); +} + +} // namespace + +class TagSelectionComboBox::Private +{ +public: + explicit Private(TagSelectionComboBox *parent) + : q(parent) + { + } + + enum LoopControl { + Break, + Continue, + }; + + template void setSelection(const Selection &entries, Comp &&cmp) + { + if (!mModelReady) { + mPendingSelection = entries; + return; + } + + const auto forEachIndex = [this, entries, cmp](auto &&func) { + for (int i = 0, cnt = tagModel->rowCount(); i < cnt; ++i) { + const auto index = tagModel->index(i, 0); + const auto tag = tagModel->data(index, TagModel::TagRole).value(); + if (std::any_of(entries.cbegin(), entries.cend(), std::bind(cmp, tag, std::placeholders::_1))) { + if (func(index) == Break) { + break; + } + } + } + }; + + if (mCheckable) { + QItemSelection selection; + forEachIndex([&selection](const QModelIndex &index) { + selection.push_back(QItemSelectionRange{index}); + return Continue; + }); + selectionModel->select(selection, QItemSelectionModel::ClearAndSelect); + } else { + forEachIndex([this](const QModelIndex &index) { + q->setCurrentIndex(index.row()); + return Break; + }); + } + } + + void toggleItem(const QModelIndex &tagModelIndex) const + { + selectionModel->select(tagModelIndex, QItemSelectionModel::Toggle); + } + + void setItemChecked(const QModelIndex &tagModelIndex, Qt::CheckState state) const + { + selectionModel->select(tagModelIndex, state == Qt::Checked ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); + } + + void setCheckable(bool checkable) + { + if (checkable) { + selectionModel = std::make_unique(tagModel.get(), q); + checkableProxy = std::make_unique(q); + checkableProxy->setSourceModel(tagModel.get()); + checkableProxy->setSelectionModel(selectionModel.get()); + + tagModel->setParent(nullptr); + q->setModel(checkableProxy.get()); + tagModel->setParent(q); + + q->setEditable(true); + q->lineEdit()->setReadOnly(true); + q->lineEdit()->setPlaceholderText(i18nc("@label Placeholder text in tag selection combobox", "Select tags...")); + q->lineEdit()->setAlignment(Qt::AlignLeft); + + q->lineEdit()->installEventFilter(q); + q->view()->installEventFilter(q); + q->view()->viewport()->installEventFilter(q); + + q->connect(selectionModel.get(), &QItemSelectionModel::selectionChanged, q, [this]() { + const auto selection = selectionModel->selection(); + q->setEditText(getEditText(selection)); + Q_EMIT q->selectionChanged(tagsFromSelection(selection, TagModel::TagRole)); + }); + q->connect(q, qOverload(&QComboBox::activated), selectionModel.get(), [this](int i) { + if (q->view()->isVisible()) { + const auto index = tagModel->index(i, 0); + toggleItem(index); + } + }); + } else { + // QComboBox automatically deletes models that it is a parent of + // which breaks our stuff + tagModel->setParent(nullptr); + q->setModel(tagModel.get()); + tagModel->setParent(q); + + if (q->lineEdit()) { + q->lineEdit()->removeEventFilter(q); + } + if (q->view()) { + q->view()->removeEventFilter(q); + q->view()->viewport()->removeEventFilter(q); + } + + q->setEditable(false); + + selectionModel.reset(); + checkableProxy.reset(); + } + } + + std::unique_ptr selectionModel; + std::unique_ptr tagModel; + std::unique_ptr checkableProxy; + + bool mCheckable = false; + bool mAllowHide = true; + bool mModelReady = false; + + std::variant mPendingSelection; + +private: + TagSelectionComboBox *const q; +}; + +TagSelectionComboBox::TagSelectionComboBox(QWidget *parent) + : QComboBox(parent) + , d(new Private(this)) +{ + auto monitor = new Monitor(this); + monitor->setObjectName(QStringLiteral("TagSelectionComboBoxMonitor")); + monitor->setTypeMonitored(Monitor::Tags); + + d->tagModel = std::make_unique(monitor, this); + connect(d->tagModel.get(), &TagModel::populated, this, [this]() { + d->mModelReady = true; + if (auto list = std::get_if(&d->mPendingSelection)) { + setSelection(*list); + } else if (auto slist = std::get_if(&d->mPendingSelection)) { + setSelection(*slist); + } + d->mPendingSelection = std::monostate{}; + }); + + d->setCheckable(d->mCheckable); +} + +TagSelectionComboBox::~TagSelectionComboBox() = default; + +void TagSelectionComboBox::setCheckable(bool checkable) +{ + if (d->mCheckable != checkable) { + d->mCheckable = checkable; + d->setCheckable(d->mCheckable); + } +} + +bool TagSelectionComboBox::checkable() const +{ + return d->mCheckable; +} + +void TagSelectionComboBox::setSelection(const Tag::List &tags) +{ + d->setSelection(tags, [](const Tag &a, const Tag &b) { + return a.name() == b.name(); + }); +} + +void TagSelectionComboBox::setSelection(const QStringList &tagNames) +{ + d->setSelection(tagNames, [](const Tag &a, const QString &b) { + return a.name() == b; + }); +} + +Tag::List TagSelectionComboBox::selection() const +{ + if (!d->selectionModel) { + return {currentData(TagModel::TagRole).value()}; + } + return tagsFromSelection(d->selectionModel->selection(), TagModel::TagRole); +} + +QStringList TagSelectionComboBox::selectionNames() const +{ + if (!d->selectionModel) { + return {currentText()}; + } + return tagsFromSelection(d->selectionModel->selection(), TagModel::NameRole); +} + +void TagSelectionComboBox::hidePopup() +{ + if (d->mAllowHide) { + QComboBox::hidePopup(); + } + d->mAllowHide = true; +} + +void TagSelectionComboBox::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Up: + case Qt::Key_Down: + showPopup(); + event->accept(); + break; + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Escape: + hidePopup(); + event->accept(); + break; + default: + break; + } +} + +bool TagSelectionComboBox::eventFilter(QObject *receiver, QEvent *event) +{ + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::ShortcutOverride: + switch (static_cast(event)->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Escape: + hidePopup(); + return true; + } + break; + case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + d->mAllowHide = false; + if (receiver == lineEdit()) { + showPopup(); + return true; + } + break; + default: + break; + } + return QComboBox::eventFilter(receiver, event); +} diff --git a/src/widgets/tagselectioncombobox.h b/src/widgets/tagselectioncombobox.h new file mode 100644 index 0000000..302f36f --- /dev/null +++ b/src/widgets/tagselectioncombobox.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2014 Christian Mollekopf + SPDX-FileCopyrightText: 2020 Daniel Vrátil + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include "tag.h" + +#include + +#include + +namespace Akonadi +{ +/** + * @brief The TagSelectionCombo class + */ +class AKONADIWIDGETS_EXPORT TagSelectionComboBox : public QComboBox +{ + Q_OBJECT +public: + explicit TagSelectionComboBox(QWidget *parent = nullptr); + ~TagSelectionComboBox() override; + + void setCheckable(bool checkable); + bool checkable() const; + + Tag::List selection() const; + QStringList selectionNames() const; + void setSelection(const Tag::List &selection); + void setSelection(const QStringList &selection); + + void hidePopup() override; + +protected: + void keyPressEvent(QKeyEvent *event) override; + bool eventFilter(QObject *receiver, QEvent *event) override; + +Q_SIGNALS: + void selectionChanged(const Akonadi::Tag::List &selection); + +private: + class Private; + std::unique_ptr const d; +}; + +} // namespace + diff --git a/src/widgets/tagselectiondialog.cpp b/src/widgets/tagselectiondialog.cpp new file mode 100644 index 0000000..8d92e97 --- /dev/null +++ b/src/widgets/tagselectiondialog.cpp @@ -0,0 +1,100 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagselectiondialog.h" +#include "controlgui.h" +#include "monitor.h" +#include "tagmodel.h" +#include "ui_tagselectiondialog.h" + +#include +#include + +using namespace Akonadi; + +class Q_DECL_HIDDEN TagSelectionDialog::Private +{ +public: + explicit Private(QDialog *parent) + : q(parent) + { + } + + void writeConfig() const; + void readConfig() const; + + QDialog *const q; + Ui::TagSelectionDialog ui; +}; + +void TagSelectionDialog::Private::writeConfig() const +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "TagSelectionDialog"); + group.writeEntry("Size", q->size()); +} + +void TagSelectionDialog::Private::readConfig() const +{ + KConfigGroup group(KSharedConfig::openStateConfig(), "TagSelectionDialog"); + const QSize sizeDialog = group.readEntry("Size", QSize(500, 400)); + if (sizeDialog.isValid()) { + q->resize(sizeDialog); + } +} + +TagSelectionDialog::TagSelectionDialog(QWidget *parent) + : QDialog(parent) + , d(new Private(this)) +{ + d->ui.setupUi(this); + + auto monitor = new Monitor(this); + monitor->setObjectName(QStringLiteral("TagSelectionDialogMonitor")); + monitor->setTypeMonitored(Monitor::Tags); + + d->ui.tagWidget->setModel(new TagModel(monitor, this)); + d->ui.tagWidget->setSelectionEnabled(true); + + d->readConfig(); + + ControlGui::widgetNeedsAkonadi(this); +} + +TagSelectionDialog::TagSelectionDialog(TagModel *model, QWidget *parent) + : QDialog(parent) + , d(new Private(this)) +{ + d->ui.setupUi(this); + + d->ui.tagWidget->setModel(model); + d->ui.tagWidget->setSelectionEnabled(true); + + d->readConfig(); + + ControlGui::widgetNeedsAkonadi(this); +} + +TagSelectionDialog::~TagSelectionDialog() +{ + d->writeConfig(); +} + +QDialogButtonBox *TagSelectionDialog::buttons() const +{ + return d->ui.buttonBox; +} + +Tag::List TagSelectionDialog::selection() const +{ + return d->ui.tagWidget->selection(); +} + +void TagSelectionDialog::setSelection(const Tag::List &tags) +{ + d->ui.tagWidget->setSelection(tags); +} diff --git a/src/widgets/tagselectiondialog.h b/src/widgets/tagselectiondialog.h new file mode 100644 index 0000000..94d6b67 --- /dev/null +++ b/src/widgets/tagselectiondialog.h @@ -0,0 +1,50 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +#include "tag.h" + +class QDialogButtonBox; +namespace Akonadi +{ +class TagModel; + +/** + * A widget that shows a tag selection and provides means to edit that selection. + * + * TODO A standalone dialog version that takes an item and takes care of writing back the changes would be useful. + * @since 4.13 + */ +class AKONADIWIDGETS_EXPORT TagSelectionDialog : public QDialog +{ + Q_OBJECT +public: + explicit TagSelectionDialog(QWidget *parent = nullptr); + TagSelectionDialog(TagModel *model, QWidget *parent = nullptr); + ~TagSelectionDialog() override; + + void setSelection(const Akonadi::Tag::List &tags); + Q_REQUIRED_RESULT Akonadi::Tag::List selection() const; + + Q_REQUIRED_RESULT QDialogButtonBox *buttons() const; + +Q_SIGNALS: + void selectionChanged(const Akonadi::Tag::List &tags); + +private: + class Private; + QScopedPointer d; +}; + +} + diff --git a/src/widgets/tagselectiondialog.ui b/src/widgets/tagselectiondialog.ui new file mode 100644 index 0000000..83fa4ab --- /dev/null +++ b/src/widgets/tagselectiondialog.ui @@ -0,0 +1,72 @@ + + + TagSelectionDialog + + + + 0 + 0 + 400 + 300 + + + + Select Tags + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Akonadi::TagEditWidget + QWidget +
tageditwidget.h
+ 1 +
+
+ + + + buttonBox + accepted() + TagSelectionDialog + accept() + + + 243 + 280 + + + 0 + 48 + + + + + buttonBox + rejected() + TagSelectionDialog + reject() + + + 282 + 276 + + + 1 + 120 + + + + +
diff --git a/src/widgets/tagselectwidget.cpp b/src/widgets/tagselectwidget.cpp new file mode 100644 index 0000000..5342eff --- /dev/null +++ b/src/widgets/tagselectwidget.cpp @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2015-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "tagselectwidget.h" +#include "monitor.h" +#include "tageditwidget.h" +#include "tagmodel.h" + +#include "shared/akranges.h" + +#include + +using namespace Akonadi; +using namespace AkRanges; + +class Q_DECL_HIDDEN TagSelectWidget::Private +{ +public: + QScopedPointer mTagEditWidget; +}; + +TagSelectWidget::TagSelectWidget(QWidget *parent) + : QWidget(parent) + , d(new Private()) +{ + auto mainLayout = new QHBoxLayout(this); + + auto monitor = new Monitor(this); + monitor->setObjectName(QStringLiteral("TagSelectWidgetMonitor")); + monitor->setTypeMonitored(Monitor::Tags); + + auto model = new TagModel(monitor, this); + d->mTagEditWidget.reset(new TagEditWidget()); + d->mTagEditWidget->setModel(model); + d->mTagEditWidget->setSelectionEnabled(true); + d->mTagEditWidget->setObjectName(QStringLiteral("tageditwidget")); + + mainLayout->addWidget(d->mTagEditWidget.get()); +} + +TagSelectWidget::~TagSelectWidget() = default; + +void TagSelectWidget::setSelection(const Tag::List &tags) +{ + d->mTagEditWidget->setSelection(tags); +} + +Tag::List TagSelectWidget::selection() const +{ + return d->mTagEditWidget->selection(); +} + +QStringList TagSelectWidget::tagToStringList() const +{ + return selection() | Views::transform([](const auto &tag) { + return tag.url().url(); + }) + | Actions::toQList; +} + +void TagSelectWidget::setSelectionFromStringList(const QStringList &lst) +{ + setSelection(lst | Views::transform([](const auto &cat) { + return Tag::fromUrl(QUrl{cat}); + }) + | Actions::toQVector); +} diff --git a/src/widgets/tagselectwidget.h b/src/widgets/tagselectwidget.h new file mode 100644 index 0000000..c2040d6 --- /dev/null +++ b/src/widgets/tagselectwidget.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2015-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#pragma once + +#include "akonadiwidgets_export.h" +#include "tag.h" +#include + +namespace Akonadi +{ +/** + * A widget that offers facilities to add/remove tags and provides a way to select tags. + * + * @since 4.14.6 + */ + +class AKONADIWIDGETS_EXPORT TagSelectWidget : public QWidget +{ + Q_OBJECT +public: + explicit TagSelectWidget(QWidget *parent = nullptr); + ~TagSelectWidget(); + + void setSelection(const Akonadi::Tag::List &tags); + Q_REQUIRED_RESULT Akonadi::Tag::List selection() const; + + /** + * @brief tagToStringList + * @return QStringList from selected tag (List of Url) + */ + Q_REQUIRED_RESULT QStringList tagToStringList() const; + /** + * @brief setSelectionFromStringList, convert a QStringList to Tag (converted from url) + */ + void setSelectionFromStringList(const QStringList &lst); + +private: + /// @cond PRIVATE + class Private; + QScopedPointer const d; + /// @endcond +}; +} + diff --git a/src/widgets/tagwidget.cpp b/src/widgets/tagwidget.cpp new file mode 100644 index 0000000..2f1677d --- /dev/null +++ b/src/widgets/tagwidget.cpp @@ -0,0 +1,139 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2010 Tobias Koenig + SPDX-FileCopyrightText: 2014 Christian Mollekopf + SPDX-FileCopyrightText: 2016-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "tagwidget.h" + +#include "changerecorder.h" +#include "tagmodel.h" +#include "tagselectiondialog.h" + +#include + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +class Q_DECL_HIDDEN TagView : public QLineEdit +{ + Q_OBJECT +public: + explicit TagView(QWidget *parent) + : QLineEdit(parent) + { + } + + void contextMenuEvent(QContextMenuEvent *event) override + { + if (text().isEmpty()) { + return; + } + + QMenu menu; + menu.addAction(i18n("Clear"), this, &TagView::clearTags); + menu.exec(event->globalPos()); + } + +Q_SIGNALS: + void clearTags(); +}; + +} // namespace Akonadi + +// include after defining TagView +#include "ui_tagwidget.h" + +class Q_DECL_HIDDEN TagWidget::Private +{ +public: + Ui::TagWidget ui; + Akonadi::Tag::List mTags; + Akonadi::TagModel *mModel = nullptr; +}; + +TagWidget::TagWidget(QWidget *parent) + : QWidget(parent) + , d(new Private) +{ + auto monitor = new Monitor(this); + monitor->setObjectName(QStringLiteral("TagWidgetMonitor")); + monitor->setTypeMonitored(Monitor::Tags); + d->mModel = new Akonadi::TagModel(monitor, this); + connect(monitor, &Monitor::tagAdded, this, &TagWidget::updateView); + + d->ui.setupUi(this); + connect(d->ui.tagView, &TagView::clearTags, this, &TagWidget::clearTags); + + connect(d->ui.editButton, &QToolButton::clicked, this, &TagWidget::editTags); + connect(d->mModel, &Akonadi::TagModel::populated, this, &TagWidget::updateView); +} + +TagWidget::~TagWidget() = default; + +void TagWidget::clearTags() +{ + if (!d->mTags.isEmpty()) { + d->mTags.clear(); + d->ui.tagView->clear(); + Q_EMIT selectionChanged(d->mTags); + } +} + +void TagWidget::setSelection(const Akonadi::Tag::List &tags) +{ + if (d->mTags != tags) { + d->mTags = tags; + updateView(); + Q_EMIT selectionChanged(d->mTags); + } +} + +Akonadi::Tag::List TagWidget::selection() const +{ + return d->mTags; +} + +void TagWidget::setReadOnly(bool readOnly) +{ + d->ui.editButton->setEnabled(!readOnly); + // d->mTagView is always readOnly => not change it. +} + +void TagWidget::editTags() +{ + QScopedPointer dlg(new TagSelectionDialog(d->mModel, this)); + dlg->setSelection(d->mTags); + if (dlg->exec() == QDialog::Accepted) { + d->mTags = dlg->selection(); + updateView(); + Q_EMIT selectionChanged(d->mTags); + } +} + +void TagWidget::updateView() +{ + QStringList tagsNames; + // Load the real tag names from the model + for (int i = 0; i < d->mModel->rowCount(); ++i) { + const QModelIndex index = d->mModel->index(i, 0); + const auto tag = d->mModel->data(index, Akonadi::TagModel::TagRole).value(); + if (d->mTags.contains(tag)) { + tagsNames.push_back(tag.name()); + } + } + d->ui.tagView->setText(QLocale::system().createSeparatedList(tagsNames)); +} + +#include "tagwidget.moc" diff --git a/src/widgets/tagwidget.h b/src/widgets/tagwidget.h new file mode 100644 index 0000000..bf685bd --- /dev/null +++ b/src/widgets/tagwidget.h @@ -0,0 +1,51 @@ +/* + This file is part of Akonadi + + SPDX-FileCopyrightText: 2010 Tobias Koenig + SPDX-FileCopyrightText: 2014 Christian Mollekopf + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadiwidgets_export.h" + +#include + +#include "tag.h" + +namespace Akonadi +{ +/** + * A widget that shows a tag selection and provides means to edit that selection. + * + * TODO A standalone dialog version that takes an item and takes care of writing back the changes would be useful. + * @since 4.13 + */ +class AKONADIWIDGETS_EXPORT TagWidget : public QWidget +{ + Q_OBJECT +public: + explicit TagWidget(QWidget *parent = nullptr); + ~TagWidget(); + + void setSelection(const Akonadi::Tag::List &tags); + Q_REQUIRED_RESULT Akonadi::Tag::List selection() const; + + void clearTags(); + void setReadOnly(bool readOnly); +Q_SIGNALS: + void selectionChanged(const Akonadi::Tag::List &tags); + +private Q_SLOTS: + void editTags(); + void updateView(); + +private: + class Private; + QScopedPointer d; +}; + +} + diff --git a/src/widgets/tagwidget.ui b/src/widgets/tagwidget.ui new file mode 100644 index 0000000..d725ff5 --- /dev/null +++ b/src/widgets/tagwidget.ui @@ -0,0 +1,42 @@ + + + TagWidget + + + + 0 + 0 + 400 + 46 + + + + + + + true + + + Click to add tags + + + + + + + ... + + + + + + + + Akonadi::TagView + QLineEdit +
tagwidget.h
+
+
+ + +
diff --git a/src/xml/CMakeLists.txt b/src/xml/CMakeLists.txt new file mode 100644 index 0000000..632a2bf --- /dev/null +++ b/src/xml/CMakeLists.txt @@ -0,0 +1,99 @@ + +find_package(LibXml2) +set_package_properties(LibXml2 PROPERTIES + DESCRIPTION "Required for XML schema validation in akonadixml" + URL "http://xmlsoft.org" + TYPE OPTIONAL +) + +if (LIBXML2_FOUND) + add_definitions(-DHAVE_LIBXML2) + # TODO: Use LibXml2::LibXml2 when we'll require CMake >= 3.12 + include_directories(${LIBXML2_INCLUDE_DIR}) +endif () + +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() +set(akonadixml_SRCS + xmldocument.cpp + xmlreader.cpp + xmlwritejob.cpp + xmlwriter.cpp +) + +ecm_generate_headers(AkonadiXml_HEADERS + HEADER_NAMES + XmlDocument + XmlReader + XmlWriteJob + XmlWriter + REQUIRED_HEADERS AkonadiXml_HEADERS +) + +add_executable(akonadi2xml akonadi2xml.cpp) +if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) + set_target_properties(akonadi2xml PROPERTIES UNITY_BUILD ON) +endif() + +set_target_properties(akonadi2xml PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(akonadi2xml + KF5::AkonadiXml + KF5::I18n + Qt::Widgets # for QApplication +) + +add_library(KF5AkonadiXml ${akonadixml_SRCS}) + +generate_export_header(KF5AkonadiXml BASE_NAME akonadi-xml) + +add_library(KF5::AkonadiXml ALIAS KF5AkonadiXml) + +target_include_directories(KF5AkonadiXml INTERFACE "$") +target_include_directories(KF5AkonadiXml PUBLIC "$") + +target_link_libraries(KF5AkonadiXml +PUBLIC + KF5::AkonadiCore + Qt::Xml +PRIVATE + KF5::I18n + ${LIBXML2_LIBRARIES} +) + +set_target_properties(KF5AkonadiXml PROPERTIES + VERSION ${AKONADI_VERSION} + SOVERSION ${AKONADI_SOVERSION} + EXPORT_NAME AkonadiXml +) + +ecm_generate_pri_file(BASE_NAME AkonadiXml + LIB_NAME KF5AkonadiXml + DEPS "AkonadiCore QtXml" FILENAME_VAR PRI_FILENAME +) + +install(TARGETS + KF5AkonadiXml + EXPORT KF5AkonadiTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(TARGETS + akonadi2xml ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/akonadi-xml_export.h + ${AkonadiXml_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/AkonadiXml COMPONENT Devel +) + +install(FILES + ${PRI_FILENAME} + DESTINATION ${ECM_MKSPECS_INSTALL_DIR} +) + +install(FILES + akonadi-xml.xsd + DESTINATION ${KDE_INSTALL_DATADIR_KF5}/akonadi/ +) diff --git a/src/xml/akonadi-xml.xsd b/src/xml/akonadi-xml.xsd new file mode 100644 index 0000000..5b1922b --- /dev/null +++ b/src/xml/akonadi-xml.xsd @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/xml/akonadi2xml.cpp b/src/xml/akonadi2xml.cpp new file mode 100644 index 0000000..4c41365 --- /dev/null +++ b/src/xml/akonadi2xml.cpp @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmlwritejob.h" + +#include "collection.h" +#include "collectionpathresolver.h" + +#include +#include +#include +#include +#include + +using namespace Akonadi; + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + KAboutData aboutData(QStringLiteral("akonadi2xml"), + i18n("Akonadi To XML converter"), + QStringLiteral("1.0"), + i18n("Converts an Akonadi collection subtree into a XML file."), + KAboutLicense::GPL, + i18n("(c) 2009 Volker Krause ")); + + QCommandLineParser parser; + KAboutData::setApplicationData(aboutData); + + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + Collection root; + if (parser.isSet(QStringLiteral("collection"))) { + const QString path = parser.value(QStringLiteral("collection")); + CollectionPathResolver resolver(path); + if (!resolver.exec()) { + qCritical() << resolver.errorString(); + return -1; + } + root = Collection(resolver.collection()); + } else { + return -1; + } + + XmlWriteJob writer(root, parser.value(QStringLiteral("output"))); + if (!writer.exec()) { + qCritical() << writer.exec(); + return -1; + } +} diff --git a/src/xml/autotests/CMakeLists.txt b/src/xml/autotests/CMakeLists.txt new file mode 100644 index 0000000..d1611e2 --- /dev/null +++ b/src/xml/autotests/CMakeLists.txt @@ -0,0 +1,15 @@ + +find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED COMPONENTS Test) +include(ECMMarkAsTest) + +macro(add_libakonadixml_test _source) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + ecm_add_test(TEST_NAME ${_name} NAME_PREFIX "akonadixml-" ${_test}) + set_tests_properties(akonadixml-${_name} PROPERTIES ENVIRONMENT "QT_HASH_SEED=1;QT_NO_CPU_FEATURE=sse4.2") + set_target_properties(${_name} PROPERTIES COMPILE_FLAGS -DKDESRCDIR="\\"${CMAKE_CURRENT_SOURCE_DIR}/\\"") + target_link_libraries(${_name} KF5AkonadiCore KF5::AkonadiXml Qt::Test Qt::Xml) +endmacro() + +add_libakonadixml_test(collectiontest.cpp) +add_libakonadixml_test(xmldocumenttest.cpp) diff --git a/src/xml/autotests/collectiontest.cpp b/src/xml/autotests/collectiontest.cpp new file mode 100644 index 0000000..bbe9119 --- /dev/null +++ b/src/xml/autotests/collectiontest.cpp @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: Igor Trindade Oliveira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiontest.h" +#include +#include + +#include "entitydisplayattribute.h" + +#include +#include +using namespace Akonadi; + +QTEST_MAIN(CollectionTest) + +// NOTE: XML element attributes are stored in QHash, which means that there are +// always in random order when converting to string. This test has QT_HASH_SEED +// always set to 1, but it appears that it has different effect on my computer +// and on Jenkins. This order of attributes is the order that passes on Jenkis, +// so if it fails for you locally because of different order of arguments, +// please make sure that your fix won't break the test on Jenkins. + +QByteArray collection1( + "\n" + " \n" + " (\"Posteingang\" \"mail-folder-inbox\" \"\" ())\n" + " \n" + "\n"); + +QByteArray collection2( + " \ + \ + (\"Posteingang\" \"mail-folder-inbox\" false) \ + \ + \ + \ + wcW \ + \ + \ +"); + +void CollectionTest::testBuildCollection() +{ + QDomDocument mDocument; + + mDocument.setContent(collection1, true, nullptr); + Collection::List colist = XmlReader::readCollections(mDocument.documentElement()); + + const QStringList mimeType{QStringLiteral("inode/directory"), QStringLiteral("message/rfc822")}; + QCOMPARE(colist.size(), 1); + verifyCollection(colist, 0, QStringLiteral("c11"), QStringLiteral("Inbox"), mimeType); + + mDocument.setContent(collection2, true, nullptr); + colist = XmlReader::readCollections(mDocument.documentElement()); + + QCOMPARE(colist.size(), 3); + verifyCollection(colist, 0, QStringLiteral("c11"), QStringLiteral("Inbox"), mimeType); + verifyCollection(colist, 1, QStringLiteral("c111"), QStringLiteral("KDE PIM"), mimeType); + verifyCollection(colist, 2, QStringLiteral("c112"), QStringLiteral("Akonadi"), mimeType); + + QVERIFY(colist.at(0).hasAttribute()); + const auto attr = colist.at(0).attribute(); + QCOMPARE(attr->displayName(), QStringLiteral("Posteingang")); +} + +void CollectionTest::serializeCollection() +{ + Collection c; + c.setRemoteId(QStringLiteral("c11")); + c.setName(QStringLiteral("Inbox")); + c.setContentMimeTypes(QStringList() << Collection::mimeType() << QStringLiteral("message/rfc822")); + c.attribute(Collection::AddIfMissing)->setDisplayName(QStringLiteral("Posteingang")); + c.attribute()->setIconName(QStringLiteral("mail-folder-inbox")); + + QDomDocument doc; + QDomElement root = doc.createElement(QStringLiteral("test")); + doc.appendChild(root); + XmlWriter::writeCollection(c, root); + + QCOMPARE(doc.toString(), QString::fromUtf8(collection1)); +} + +void CollectionTest::verifyCollection(const Collection::List &colist, + int listPosition, + const QString &remoteId, + const QString &name, + const QStringList &mimeType) +{ + QVERIFY(colist.at(listPosition).name() == name); + QVERIFY(colist.at(listPosition).remoteId() == remoteId); + QVERIFY(colist.at(listPosition).contentMimeTypes() == mimeType); +} diff --git a/src/xml/autotests/collectiontest.h b/src/xml/autotests/collectiontest.h new file mode 100644 index 0000000..4a46f70 --- /dev/null +++ b/src/xml/autotests/collectiontest.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include "collection.h" +#include + +class CollectionTest : public QObject +{ + Q_OBJECT +private: + void verifyCollection(const Akonadi::Collection::List &colist, int listPosition, const QString &remoteId, const QString &name, const QStringList &mimeType); +private Q_SLOTS: + void serializeCollection(); + void testBuildCollection(); +}; + diff --git a/src/xml/autotests/knutdemo.xml b/src/xml/autotests/knutdemo.xml new file mode 100644 index 0000000..88b75ca --- /dev/null +++ b/src/xml/autotests/knutdemo.xml @@ -0,0 +1,72 @@ + + + + ("Posteingang" "mail-folder-inbox") + + + + wcW + + + Subject: Welcome to the Knut resource +To: new-user@this-computer.local +From: knut@your.computer.local +MIME-Version: 1.0 +Content-Type: text/plain +Date: Thu, 01 Jan 2009 15:08:50 +0000 + +This is a mail body + + \SEEN + + + + + + + + + +BEGIN:VCARD +EMAIL:vkrause@kde.org +FN:Volker Krause +GEO:52.500000;13.366667 +N:Krause;Volker;;; +NAME:Volker Krause +ORG:KDE +REV:2003-02-27T20:08:42Z +ROLE:Author of this file +TZ:+02:00 +UID:bb2slGmqxb +URL:http://www.akonadi-project.org +VERSION:3.0 +END:VCARD + + + + + + +BEGIN:VCALENDAR +PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN +VERSION:2.0 +BEGIN:VTODO +DTSTAMP:20090101T154017Z +ORGANIZER:MAILTO:vkrause@kde.org +CREATED:20040505T094143Z +UID:libkcal-1506191911.958 +LAST-MODIFIED:20040512T133925Z +SUMMARY:Add a demo task to this file +PRIORITY:3 +DUE;VALUE=DATE:20090101 +COMPLETED:20090101T133925Z +PERCENT-COMPLETE:100 +END:VTODO +END:VCALENDAR + + + + + + + diff --git a/src/xml/autotests/xmldocumenttest.cpp b/src/xml/autotests/xmldocumenttest.cpp new file mode 100644 index 0000000..85a2fb5 --- /dev/null +++ b/src/xml/autotests/xmldocumenttest.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmldocument.h" + +#include + +#include + +using namespace Akonadi; + +class XmlDocumentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDocumentLoad() + { + XmlDocument doc(QStringLiteral(KDESRCDIR "/knutdemo.xml")); + QVERIFY(doc.isValid()); + QVERIFY(doc.lastError().isEmpty()); + QCOMPARE(doc.collections().count(), 9); + + Collection col = doc.collectionByRemoteId(QStringLiteral("c11")); + QCOMPARE(col.name(), QStringLiteral("Inbox")); + QCOMPARE(col.attributes().count(), 1); + QCOMPARE(col.parentCollection().remoteId(), QStringLiteral("c1")); + + QCOMPARE(doc.childCollections(col).count(), 2); + + Item item = doc.itemByRemoteId(QStringLiteral("contact1")); + QCOMPARE(item.mimeType(), QStringLiteral("text/directory")); + QVERIFY(item.hasPayload()); + + Item::List items = doc.items(col); + QCOMPARE(items.count(), 1); + item = items.first(); + QVERIFY(item.hasPayload()); + QCOMPARE(item.flags().count(), 1); + QVERIFY(item.hasFlag("\\SEEN")); + } +}; + +QTEST_MAIN(XmlDocumentTest) + +#include "xmldocumenttest.moc" diff --git a/src/xml/format_p.h b/src/xml/format_p.h new file mode 100644 index 0000000..2da2a4a --- /dev/null +++ b/src/xml/format_p.h @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +namespace Format +{ +namespace Tag +{ +inline QString root() +{ + return QStringLiteral("knut"); +} +inline QString collection() +{ + return QStringLiteral("collection"); +} +inline QString item() +{ + return QStringLiteral("item"); +} +inline QString attribute() +{ + return QStringLiteral("attribute"); +} +inline QString flag() +{ + return QStringLiteral("flag"); +} +inline QString tag() +{ + return QStringLiteral("tag"); +} +inline QString payload() +{ + return QStringLiteral("payload"); +} + +} + +namespace Attr +{ +inline QString remoteId() +{ + return QStringLiteral("rid"); +} +inline QString attributeType() +{ + return QStringLiteral("type"); +} +inline QString collectionName() +{ + return QStringLiteral("name"); +} +inline QString collectionContentTypes() +{ + return QStringLiteral("content"); +} +inline QString itemMimeType() +{ + return QStringLiteral("mimetype"); +} +inline QString name() +{ + return QStringLiteral("name"); +} +inline QString gid() +{ + return QStringLiteral("gid"); +} +inline QString type() +{ + return QStringLiteral("type"); +} + +} + +} + +} + diff --git a/src/xml/xmldocument.cpp b/src/xml/xmldocument.cpp new file mode 100644 index 0000000..5cea2bb --- /dev/null +++ b/src/xml/xmldocument.cpp @@ -0,0 +1,321 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmldocument.h" +#include "format_p.h" +#include "xmlreader.h" + +#include + +#include +#include + +#ifdef HAVE_LIBXML2 +#include +#include +#include +#include +#endif + +using namespace Akonadi; + +// helper class for dealing with libxml resource management +template class XmlPtr +{ +public: + explicit XmlPtr(const T &t) + : p(t) + { + } + + ~XmlPtr() + { + FreeFunc(p); + } + + operator T() const // NOLINT(google-explicit-constructor) + { + return p; + } + + explicit operator bool() const + { + return p != nullptr; + } + +private: + Q_DISABLE_COPY(XmlPtr) + T p; +}; + +static QDomElement findElementByRidHelper(const QDomElement &elem, const QString &rid, const QString &elemName) +{ + if (elem.isNull()) { + return QDomElement(); + } + if (elem.tagName() == elemName && elem.attribute(Format::Attr::remoteId()) == rid) { + return elem; + } + const QDomNodeList children = elem.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement child = children.at(i).toElement(); + if (child.isNull()) { + continue; + } + const QDomElement rv = findElementByRidHelper(child, rid, elemName); + if (!rv.isNull()) { + return rv; + } + } + return QDomElement(); +} + +namespace Akonadi +{ +class XmlDocumentPrivate +{ +public: + XmlDocumentPrivate() + : valid(false) + { + lastError = i18n("No data loaded."); + } + + QDomElement findElementByRid(const QString &rid, const QString &elemName) const + { + return findElementByRidHelper(document.documentElement(), rid, elemName); + } + + QDomDocument document; + QString lastError; + bool valid; +}; + +} // namespace Akonadi + +XmlDocument::XmlDocument() + : d(new XmlDocumentPrivate) +{ + const QDomElement rootElem = d->document.createElement(Format::Tag::root()); + d->document.appendChild(rootElem); +} + +XmlDocument::XmlDocument(const QString &fileName) + : d(new XmlDocumentPrivate) +{ + loadFile(fileName); +} + +XmlDocument::~XmlDocument() +{ + delete d; +} + +bool Akonadi::XmlDocument::loadFile(const QString &fileName) +{ + d->valid = false; + d->document = QDomDocument(); + + if (fileName.isEmpty()) { + d->lastError = i18n("No filename specified"); + return false; + } + + QFile file(fileName); + QByteArray data; + if (file.exists()) { + if (!file.open(QIODevice::ReadOnly)) { + d->lastError = i18n("Unable to open data file '%1'.", fileName); + return false; + } + data = file.readAll(); + } else { + d->lastError = i18n("File %1 does not exist.", fileName); + return false; + } + +#ifdef HAVE_LIBXML2 + // schema validation + XmlPtr sourceDoc(xmlParseMemory(data.constData(), data.length())); + if (!sourceDoc) { + d->lastError = i18n("Unable to parse data file '%1'.", fileName); + return false; + } + + const QString &schemaFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kf5/akonadi/akonadi-xml.xsd")); + XmlPtr schemaDoc(xmlReadFile(schemaFileName.toLocal8Bit().constData(), nullptr, XML_PARSE_NONET)); + if (!schemaDoc) { + d->lastError = i18n("Schema definition could not be loaded and parsed."); + return false; + } + XmlPtr parserContext(xmlSchemaNewDocParserCtxt(schemaDoc)); + if (!parserContext) { + d->lastError = i18n("Unable to create schema parser context."); + return false; + } + XmlPtr schema(xmlSchemaParse(parserContext)); + if (!schema) { + d->lastError = i18n("Unable to create schema."); + return false; + } + XmlPtr validationContext(xmlSchemaNewValidCtxt(schema)); + if (!validationContext) { + d->lastError = i18n("Unable to create schema validation context."); + return false; + } + + if (xmlSchemaValidateDoc(validationContext, sourceDoc) != 0) { + d->lastError = i18n("Invalid file format."); + return false; + } +#endif + + // DOM loading + QString errMsg; + if (!d->document.setContent(data, true, &errMsg)) { + d->lastError = i18n("Unable to parse data file: %1", errMsg); + return false; + } + + d->valid = true; + d->lastError.clear(); + return true; +} + +bool XmlDocument::writeToFile(const QString &fileName) const +{ + QFile f(fileName); + if (!f.open(QFile::WriteOnly)) { + d->lastError = f.errorString(); + return false; + } + + f.write(d->document.toByteArray(2)); + + d->lastError.clear(); + return true; +} + +bool XmlDocument::isValid() const +{ + return d->valid; +} + +QString XmlDocument::lastError() const +{ + return d->lastError; +} + +QDomDocument &XmlDocument::document() const +{ + return d->document; +} + +QDomElement XmlDocument::collectionElement(const Collection &collection) const +{ + if (collection == Collection::root()) { + return d->document.documentElement(); + } + if (collection.remoteId().isEmpty()) { + return QDomElement(); + } + if (collection.parentCollection().remoteId().isEmpty() && collection.parentCollection() != Collection::root()) { + return d->findElementByRid(collection.remoteId(), Format::Tag::collection()); + } + QDomElement parent = collectionElement(collection.parentCollection()); + if (parent.isNull()) { + return QDomElement(); + } + const QDomNodeList children = parent.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement child = children.at(i).toElement(); + if (child.isNull()) { + continue; + } + if (child.tagName() == Format::Tag::collection() && child.attribute(Format::Attr::remoteId()) == collection.remoteId()) { + return child; + } + } + return QDomElement(); +} + +QDomElement XmlDocument::itemElementByRemoteId(const QString &rid) const +{ + return d->findElementByRid(rid, Format::Tag::item()); +} + +QDomElement XmlDocument::collectionElementByRemoteId(const QString &rid) const +{ + return d->findElementByRid(rid, Format::Tag::collection()); +} + +Collection XmlDocument::collectionByRemoteId(const QString &rid) const +{ + const QDomElement elem = d->findElementByRid(rid, Format::Tag::collection()); + return XmlReader::elementToCollection(elem); +} + +Item XmlDocument::itemByRemoteId(const QString &rid, bool includePayload) const +{ + return XmlReader::elementToItem(itemElementByRemoteId(rid), includePayload); +} + +Collection::List XmlDocument::collections() const +{ + return XmlReader::readCollections(d->document.documentElement()); +} + +Tag::List XmlDocument::tags() const +{ + return XmlReader::readTags(d->document.documentElement()); +} + +Collection::List XmlDocument::childCollections(const Collection &parentCollection) const +{ + QDomElement parentElem = collectionElement(parentCollection); + + if (parentElem.isNull()) { + d->lastError = QStringLiteral("Parent node not found."); + return Collection::List(); + } + + Collection::List rv; + const QDomNodeList children = parentElem.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement childElem = children.at(i).toElement(); + if (childElem.isNull() || childElem.tagName() != Format::Tag::collection()) { + continue; + } + Collection c = XmlReader::elementToCollection(childElem); + c.setParentCollection(parentCollection); + rv.append(c); + } + + return rv; +} + +Item::List XmlDocument::items(const Akonadi::Collection &collection, bool includePayload) const +{ + const QDomElement colElem = collectionElement(collection); + if (colElem.isNull()) { + d->lastError = i18n("Unable to find collection %1", collection.name()); + return Item::List(); + } else { + d->lastError.clear(); + } + + Item::List items; + const QDomNodeList children = colElem.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement itemElem = children.at(i).toElement(); + if (itemElem.isNull() || itemElem.tagName() != Format::Tag::item()) { + continue; + } + items += XmlReader::elementToItem(itemElem, includePayload); + } + + return items; +} diff --git a/src/xml/xmldocument.h b/src/xml/xmldocument.h new file mode 100644 index 0000000..0c53baa --- /dev/null +++ b/src/xml/xmldocument.h @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadi-xml_export.h" + +#include "collection.h" +#include + +#include + +namespace Akonadi +{ +class XmlDocumentPrivate; + +/** + Represents a document of the KNUT XML serialization format for Akonadi objects. +*/ +class AKONADI_XML_EXPORT XmlDocument +{ +public: + /** + Creates an empty document. + */ + XmlDocument(); + + /** + Creates a new XmlDocument object and calls loadFile(). + + @see loadFile() + */ + explicit XmlDocument(const QString &fileName); + ~XmlDocument(); + + /** + Parses the given XML file and validates it. + In case of an error, isValid() will return @c false and + lastError() will return an error message. + + @see isValid(), lastError() + */ + bool loadFile(const QString &fileName); + + /** + Writes the current document into the given file. + */ + bool writeToFile(const QString &fileName) const; + + /** + Returns true if the document could be parsed successfully. + @see lastError() + */ + Q_REQUIRED_RESULT bool isValid() const; + + /** + Returns the last error occurred during file loading/parsing. + Empty if isValid() returns @c true. + @see isValid() + */ + Q_REQUIRED_RESULT QString lastError() const; + + /** + Returns the DOM document for this XML document. + */ + QDomDocument &document() const; + + /** + Returns the DOM element representing @p collection. + */ + Q_REQUIRED_RESULT QDomElement collectionElement(const Collection &collection) const; + + /** + Returns the DOM element representing the item with the given remote id + */ + Q_REQUIRED_RESULT QDomElement itemElementByRemoteId(const QString &rid) const; + + /** + * Returns the DOM element representing the collection with the given remote id + */ + Q_REQUIRED_RESULT QDomElement collectionElementByRemoteId(const QString &rid) const; + + /** + Returns the collection with the given remote id. + */ + Q_REQUIRED_RESULT Collection collectionByRemoteId(const QString &rid) const; + + /** + Returns the item with the given remote id. + */ + Q_REQUIRED_RESULT Item itemByRemoteId(const QString &rid, bool includePayload = true) const; + + /** + Returns the collections defined in this document. + */ + Q_REQUIRED_RESULT Collection::List collections() const; + + /** + Returns the tags defined in this document. + */ + Q_REQUIRED_RESULT Tag::List tags() const; + + /** + Returns the immediate child collections of @p parentCollection. + */ + Q_REQUIRED_RESULT Collection::List childCollections(const Collection &parentCollection) const; + + /** + Returns the items in the given collection. + */ + Q_REQUIRED_RESULT Item::List items(const Collection &collection, bool includePayload = true) const; + +private: + Q_DISABLE_COPY(XmlDocument) + XmlDocumentPrivate *const d; +}; + +} + diff --git a/src/xml/xmlreader.cpp b/src/xml/xmlreader.cpp new file mode 100644 index 0000000..94088a0 --- /dev/null +++ b/src/xml/xmlreader.cpp @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + SPDX-FileCopyrightText: 2009 Igor Trindade Oliveira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmlreader.h" +#include "format_p.h" + +#include "attributefactory.h" +#include + +using namespace Akonadi; + +Attribute *XmlReader::elementToAttribute(const QDomElement &elem) +{ + if (elem.isNull() || elem.tagName() != Format::Tag::attribute()) { + return nullptr; + } + Attribute *attr = AttributeFactory::createAttribute(elem.attribute(Format::Attr::attributeType()).toUtf8()); + Q_ASSERT(attr); + attr->deserialize(elem.text().toUtf8()); + return attr; +} + +template static void readAttributesImpl(const QDomElement &elem, T &entity) +{ + if (elem.isNull()) { + return; + } + const QDomNodeList children = elem.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement attrElem = children.at(i).toElement(); + Attribute *attr = XmlReader::elementToAttribute(attrElem); + if (attr) { + entity.addAttribute(attr); + } + } +} + +void XmlReader::readAttributes(const QDomElement &elem, Item &item) +{ + readAttributesImpl(elem, item); +} + +void XmlReader::readAttributes(const QDomElement &elem, Collection &collection) +{ + readAttributesImpl(elem, collection); +} + +Collection XmlReader::elementToCollection(const QDomElement &elem) +{ + if (elem.isNull() || elem.tagName() != Format::Tag::collection()) { + return Collection(); + } + + Collection c; + c.setRemoteId(elem.attribute(Format::Attr::remoteId())); + c.setName(elem.attribute(Format::Attr::collectionName())); + c.setContentMimeTypes(elem.attribute(Format::Attr::collectionContentTypes()).split(QLatin1Char(','))); + XmlReader::readAttributes(elem, c); + + const QDomElement parentElem = elem.parentNode().toElement(); + if (!parentElem.isNull() && parentElem.tagName() == Format::Tag::collection()) { + c.parentCollection().setRemoteId(parentElem.attribute(Format::Attr::remoteId())); + } + + return c; +} + +Collection::List XmlReader::readCollections(const QDomElement &elem) +{ + Collection::List rv; + if (elem.isNull()) { + return rv; + } + if (elem.tagName() == Format::Tag::collection()) { + rv += elementToCollection(elem); + } + const QDomNodeList children = elem.childNodes(); + for (int i = 0; i < children.count(); i++) { + const QDomElement child = children.at(i).toElement(); + if (child.isNull() || child.tagName() != Format::Tag::collection()) { + continue; + } + rv += readCollections(child); + } + return rv; +} + +Tag XmlReader::elementToTag(const QDomElement &elem) +{ + if (elem.isNull() || elem.tagName() != Format::Tag::tag()) { + return Tag(); + } + + Tag t; + t.setRemoteId(elem.attribute(Format::Attr::remoteId()).toUtf8()); + t.setName(elem.attribute(Format::Attr::name())); + t.setGid(elem.attribute(Format::Attr::gid()).toUtf8()); + t.setType(elem.attribute(Format::Attr::type()).toUtf8()); + + // TODO Implement rid parent support in TagCreateJob first + // const QDomElement parentElem = elem.parentNode().toElement(); + // if ( !parentElem.isNull() && parentElem.tagName() == Format::Tag::tag() ) { + // Tag parent; + // parent.setRemoteId( parentElem.attribute( Format::Attr::remoteId() ).toLatin1() ); + // t.setParent( parent ); + // } + + return t; +} + +Tag::List XmlReader::readTags(const QDomElement &elem) +{ + Tag::List rv; + if (elem.isNull()) { + return rv; + } + if (elem.tagName() == Format::Tag::tag()) { + rv += elementToTag(elem); + } + const QDomNodeList children = elem.childNodes(); + for (int i = 0; i < children.count(); i++) { + const QDomElement child = children.at(i).toElement(); + if (child.isNull() || child.tagName() != Format::Tag::tag()) { + continue; + } + rv += readTags(child); + } + return rv; +} + +Item XmlReader::elementToItem(const QDomElement &elem, bool includePayload) +{ + Item item(elem.attribute(Format::Attr::itemMimeType(), QStringLiteral("application/octet-stream"))); + item.setRemoteId(elem.attribute(Format::Attr::remoteId())); + XmlReader::readAttributes(elem, item); + + const QDomNodeList children = elem.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement subElem = children.at(i).toElement(); + if (subElem.isNull()) { + continue; + } + if (subElem.tagName() == Format::Tag::flag()) { + item.setFlag(subElem.text().toUtf8()); + } else if (subElem.tagName() == Format::Tag::tag()) { + Tag tag; + tag.setRemoteId(subElem.text().toUtf8()); + item.setTag(tag); + } else if (includePayload && subElem.tagName() == Format::Tag::payload()) { + const QByteArray payloadData = subElem.text().toUtf8(); + item.setPayloadFromData(payloadData); + } + } + + return item; +} diff --git a/src/xml/xmlreader.h b/src/xml/xmlreader.h new file mode 100644 index 0000000..6b05325 --- /dev/null +++ b/src/xml/xmlreader.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2009 Igor Trindade Oliveira + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadi-xml_export.h" + +#include "collection.h" +#include "item.h" + +#include + +namespace Akonadi +{ +class Attribute; + +/** + Low-level methods to transform DOM elements into the corresponding Akonadi objects. + @see Akonadi::XmlDocument +*/ +namespace XmlReader +{ +/** + Converts an attribute element. +*/ +AKONADI_XML_EXPORT Attribute *elementToAttribute(const QDomElement &elem); + +/** + Reads all attributes that are immediate children of @p elem and adds them + to @p item. +*/ +AKONADI_XML_EXPORT void readAttributes(const QDomElement &elem, Item &item); + +/** + Reads all attributes that are immediate children of @p elem and adds them + to @p collection. +*/ +AKONADI_XML_EXPORT void readAttributes(const QDomElement &elem, Collection &collection); + +/** + Converts a collection element. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT Collection elementToCollection(const QDomElement &elem); + +/** + Reads recursively all collections starting from the given DOM element. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT Collection::List readCollections(const QDomElement &elem); + +/** + Converts a tag element. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT Tag elementToTag(const QDomElement &elem); + +/** + Reads recursively all tags starting from the given DOM element. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT Tag::List readTags(const QDomElement &elem); + +/** + Converts an item element. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT Item elementToItem(const QDomElement &elem, bool includePayload = true); +} + +} + diff --git a/src/xml/xmlwritejob.cpp b/src/xml/xmlwritejob.cpp new file mode 100644 index 0000000..4d94cb4 --- /dev/null +++ b/src/xml/xmlwritejob.cpp @@ -0,0 +1,155 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmlwritejob.h" +#include "xmldocument.h" +#include "xmlwriter.h" + +#include "collectionfetchjob.h" +#include "item.h" +#include "itemfetchjob.h" +#include "itemfetchscope.h" + +#include + +#include +#include + +using namespace Akonadi; + +namespace Akonadi +{ +class XmlWriteJobPrivate +{ +public: + explicit XmlWriteJobPrivate(XmlWriteJob *parent) + : q(parent) + { + } + + XmlWriteJob *const q; + Collection::List roots; + QStack pendingSiblings; + QStack elementStack; + QString fileName; + XmlDocument document; + + void collectionFetchResult(KJob *job); + void processCollection(); + void itemFetchResult(KJob *job); + void processItems(); +}; + +} // namespace Akonadi + +void XmlWriteJobPrivate::collectionFetchResult(KJob *job) +{ + if (job->error()) { + return; + } + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + if (fetch->collections().isEmpty()) { + processItems(); + } else { + pendingSiblings.push(fetch->collections()); + processCollection(); + } +} + +void XmlWriteJobPrivate::processCollection() +{ + if (!pendingSiblings.isEmpty() && pendingSiblings.top().isEmpty()) { + pendingSiblings.pop(); + if (pendingSiblings.isEmpty()) { + q->done(); + return; + } + processItems(); + return; + } + + if (pendingSiblings.isEmpty()) { + q->done(); + return; + } + + const Collection current = pendingSiblings.top().first(); + qDebug() << "Writing " << current.name() << "into" << elementStack.top().attribute(QStringLiteral("name")); + elementStack.push(XmlWriter::writeCollection(current, elementStack.top())); + auto subfetch = new CollectionFetchJob(current, CollectionFetchJob::FirstLevel, q); + q->connect(subfetch, &CollectionFetchJob::result, q, [this](KJob *job) { + collectionFetchResult(job); + }); +} + +void XmlWriteJobPrivate::processItems() +{ + const Collection collection = pendingSiblings.top().first(); + auto fetch = new ItemFetchJob(collection, q); + fetch->fetchScope().fetchAllAttributes(); + fetch->fetchScope().fetchFullPayload(); + q->connect(fetch, &ItemFetchJob::result, q, [this](KJob *job) { + itemFetchResult(job); + }); +} + +void XmlWriteJobPrivate::itemFetchResult(KJob *job) +{ + if (job->error()) { + return; + } + auto fetch = qobject_cast(job); + Q_ASSERT(fetch); + const Akonadi::Item::List lstItems = fetch->items(); + for (const Item &item : lstItems) { + XmlWriter::writeItem(item, elementStack.top()); + } + pendingSiblings.top().removeFirst(); + elementStack.pop(); + processCollection(); +} + +XmlWriteJob::XmlWriteJob(const Collection &root, const QString &fileName, QObject *parent) + : Job(parent) + , d(new XmlWriteJobPrivate(this)) +{ + d->roots.append(root); + d->fileName = fileName; +} + +XmlWriteJob::XmlWriteJob(const Collection::List &roots, const QString &fileName, QObject *parent) + : Job(parent) + , d(new XmlWriteJobPrivate(this)) +{ + d->roots = roots; + d->fileName = fileName; +} + +XmlWriteJob::~XmlWriteJob() +{ + delete d; +} + +void XmlWriteJob::doStart() +{ + d->elementStack.push(d->document.document().documentElement()); + auto job = new CollectionFetchJob(d->roots, this); + connect(job, &CollectionFetchJob::result, this, [this](KJob *job) { + d->collectionFetchResult(job); + }); +} + +void XmlWriteJob::done() // cannot be in the private class due to emitResult() +{ + if (!d->document.writeToFile(d->fileName)) { + setError(Unknown); + setErrorText(d->document.lastError()); + } + emitResult(); +} + +#include "moc_xmlwritejob.cpp" diff --git a/src/xml/xmlwritejob.h b/src/xml/xmlwritejob.h new file mode 100644 index 0000000..cd98c92 --- /dev/null +++ b/src/xml/xmlwritejob.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadi-xml_export.h" +#include "collection.h" +#include "job.h" +namespace Akonadi +{ +class Collection; +class XmlWriteJobPrivate; + +/** + Serializes a given Akonadi collection into a XML file. +*/ +class AKONADI_XML_EXPORT XmlWriteJob : public Job +{ + Q_OBJECT +public: + XmlWriteJob(const Collection &root, const QString &fileName, QObject *parent = nullptr); + XmlWriteJob(const Collection::List &roots, const QString &fileName, QObject *parent = nullptr); + ~XmlWriteJob() override; + +protected: + /* reimpl. */ void doStart() override; + +private: + friend class XmlWriteJobPrivate; + XmlWriteJobPrivate *const d; + void done(); +}; + +} + diff --git a/src/xml/xmlwriter.cpp b/src/xml/xmlwriter.cpp new file mode 100644 index 0000000..3b395e4 --- /dev/null +++ b/src/xml/xmlwriter.cpp @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + SPDX-FileCopyrightText: 2009 Igor Trindade Oliveira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "xmlwriter.h" +#include "format_p.h" + +#include "attribute.h" +#include "collection.h" +#include "item.h" + +using namespace Akonadi; + +QDomElement XmlWriter::attributeToElement(Attribute *attr, QDomDocument &document) +{ + if (document.isNull()) { + return QDomElement(); + } + + QDomElement top = document.createElement(Format::Tag::attribute()); + top.setAttribute(Format::Attr::attributeType(), QString::fromUtf8(attr->type())); + QDomText attrText = document.createTextNode(QString::fromUtf8(attr->serialized())); + top.appendChild(attrText); + + return top; +} + +template static void writeAttributesImpl(const T &entity, QDomElement &parentElem) +{ + if (parentElem.isNull()) { + return; + } + + QDomDocument doc = parentElem.ownerDocument(); + const auto attributes = entity.attributes(); + for (Attribute *attr : attributes) { + parentElem.appendChild(XmlWriter::attributeToElement(attr, doc)); + } +} + +void XmlWriter::writeAttributes(const Item &item, QDomElement &parentElem) +{ + writeAttributesImpl(item, parentElem); +} + +void XmlWriter::writeAttributes(const Collection &collection, QDomElement &parentElem) +{ + writeAttributesImpl(collection, parentElem); +} + +QDomElement XmlWriter::collectionToElement(const Akonadi::Collection &collection, QDomDocument &document) +{ + if (document.isNull()) { + return QDomElement(); + } + + QDomElement top = document.createElement(Format::Tag::collection()); + top.setAttribute(Format::Attr::remoteId(), collection.remoteId()); + top.setAttribute(Format::Attr::collectionName(), collection.name()); + top.setAttribute(Format::Attr::collectionContentTypes(), collection.contentMimeTypes().join(QLatin1Char(','))); + writeAttributes(collection, top); + + return top; +} + +QDomElement XmlWriter::writeCollection(const Akonadi::Collection &collection, QDomElement &parentElem) +{ + if (parentElem.isNull()) { + return QDomElement(); + } + + QDomDocument doc = parentElem.ownerDocument(); + const QDomElement elem = collectionToElement(collection, doc); + parentElem.insertBefore(elem, QDomNode()); // collection need to be before items to pass schema validation + return elem; +} + +QDomElement XmlWriter::itemToElement(const Akonadi::Item &item, QDomDocument &document) +{ + if (document.isNull()) { + return QDomElement(); + } + + QDomElement top = document.createElement(Format::Tag::item()); + top.setAttribute(Format::Attr::remoteId(), item.remoteId()); + top.setAttribute(Format::Attr::itemMimeType(), item.mimeType()); + + if (item.hasPayload()) { + QDomElement payloadElem = document.createElement(Format::Tag::payload()); + QDomText payloadText = document.createTextNode(QString::fromUtf8(item.payloadData())); + payloadElem.appendChild(payloadText); + top.appendChild(payloadElem); + } + + writeAttributes(item, top); + + const auto flags = item.flags(); + for (const Item::Flag &flag : flags) { + QDomElement flagElem = document.createElement(Format::Tag::flag()); + QDomText flagText = document.createTextNode(QString::fromUtf8(flag)); + flagElem.appendChild(flagText); + top.appendChild(flagElem); + } + + return top; +} + +QDomElement XmlWriter::writeItem(const Item &item, QDomElement &parentElem) +{ + if (parentElem.isNull()) { + return QDomElement(); + } + + QDomDocument doc = parentElem.ownerDocument(); + const QDomElement elem = itemToElement(item, doc); + parentElem.appendChild(elem); + return elem; +} diff --git a/src/xml/xmlwriter.h b/src/xml/xmlwriter.h new file mode 100644 index 0000000..67aa0d5 --- /dev/null +++ b/src/xml/xmlwriter.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2009 Volker Krause + SPDX-FileCopyrightText: 2009 Igor Trindade Oliveira + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "akonadi-xml_export.h" + +#include + +namespace Akonadi +{ +class Attribute; +class Collection; +class Item; + +/** + Low-level methods to serialize Akonadi objects into XML. + @see Akonadi::XmlDocument +*/ +namespace XmlWriter +{ +/** + Creates an attribute element for the given document. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT QDomElement attributeToElement(Attribute *attr, QDomDocument &document); + +/** + Serializes all attributes of the given Akonadi object into the given parent element. +*/ +AKONADI_XML_EXPORT void writeAttributes(const Item &entity, QDomElement &parentElem); + +/** + Serializes all attributes of the given Akonadi object into the given parent element. +*/ +AKONADI_XML_EXPORT void writeAttributes(const Collection &entity, QDomElement &parentElem); + +/** + Creates a collection element for the given document, not yet attached to the DOM tree. +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT QDomElement collectionToElement(const Collection &collection, QDomDocument &document); + +/** + Serializes the given collection into a DOM element with the given parent. +*/ +AKONADI_XML_EXPORT QDomElement writeCollection(const Collection &collection, QDomElement &parentElem); + +/** + Creates an item element for the given document, not yet attached to the DOM tree +*/ +Q_REQUIRED_RESULT AKONADI_XML_EXPORT QDomElement itemToElement(const Item &item, QDomDocument &document); + +/** + Serializes the given item into a DOM element and attaches it to the given item. +*/ +AKONADI_XML_EXPORT QDomElement writeItem(const Item &item, QDomElement &parentElem); +} + +} + diff --git a/templates/.clang-format b/templates/.clang-format new file mode 100644 index 0000000..9d15924 --- /dev/null +++ b/templates/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: false diff --git a/templates/CMakeLists.txt b/templates/CMakeLists.txt new file mode 100644 index 0000000..a22199d --- /dev/null +++ b/templates/CMakeLists.txt @@ -0,0 +1,6 @@ +set(apptemplate_DIRS + akonadiresource + akonadiserializer +) + +kde_package_app_templates(TEMPLATES ${apptemplate_DIRS} INSTALL_DIR ${KDE_INSTALL_KTEMPLATESDIR}) diff --git a/templates/akonadiresource/CMakeLists.txt b/templates/akonadiresource/CMakeLists.txt new file mode 100644 index 0000000..eccc0bc --- /dev/null +++ b/templates/akonadiresource/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.16) + +project(%{APPNAMELC}) + +set(KF5_MIN_VERSION "5.82.0") + +set(ECM_MIN_VERSION ${KF5_MIN_VERSION}) +find_package(ECM ${ECM_MIN_VERSION} CONFIG REQUIRED) + +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH} ${CMAKE_MODULE_PATH}) + +include(FeatureSummary) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) +include(ECMQtDeclareLoggingCategory) + +set(QT_MIN_VERSION "5.15.0") +find_package(Qt5 ${QT_MIN_VERSION} REQUIRED Core DBus Gui) + +find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) + +set(AKONADI_MIN_VERSION "5.16.0") +find_package(KF5Akonadi ${AKONADI_MIN_VERSION} CONFIG REQUIRED) + +find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable") +if (NOT XSLTPROC_EXECUTABLE) + message(FATAL_ERROR "\nThe command line XSLT processor program 'xsltproc' could not be found.\nPlease install xsltproc.\n") +endif() + +add_subdirectory(src) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/templates/akonadiresource/README b/templates/akonadiresource/README new file mode 100644 index 0000000..cdc41c6 --- /dev/null +++ b/templates/akonadiresource/README @@ -0,0 +1,89 @@ +How To Build This Template +-=-=-=-=-=-=-=-=-=-=-=-=-= + +--- On Linux & similar: + +cd +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=$MY_PREFIX -DCMAKE_BUILD_TYPE=Debug +make +make install or su -c 'make install' + +(MY_PREFIX is where you install your Akonadi setup, replace it accordingly) + +to uninstall the project: +make uninstall or su -c 'make uninstall' + +Note: you can use another build path. Then cd in your build dir and: +export MY_SRC=path_to_your_src +cmake $MY_SRC -DCMAKE_INSTALL_PREFIX=$MY_PREFIX -DCMAKE_BUILD_TYPE=Debug + +--- On Windows: + +cd +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=%MY_PREFIX% -DCMAKE_BUILD_TYPE=Debug +[n]make +[n]make install + +(MY_PREFIX is where you install your Akonadi setup, replace it accordingly) + +to uninstall the project: +[n]make uninstall + +Note: use nmake if you're building with the Visual Studio compiler, or make +if you're using the minGW compiler + + +Implementation hints +-=-=-=-=-=-=-=-=-=-= + +The code generated by the template can be compiled without any further +changes, so you can start with your own code right away. + +However, there are a couple of things you will need to change outside the +resource's code, i.e. in the resource's .desktop file: + +- Name field: the name of the resource with which it will be displayed in + system settings and applications which can add resources on their own. + E.g. MyBackend Resource + +- Comment field: short description of the resource, also used to be + displayed, e.g. For calendars and contacts stored in MyBackend + +- Icon field: if you are not writing a contact (addressbook) resource, you have + to change this to either an icon for the respective MIME type you are going + to provide or use a resource specific icon which you provide yourself + +- X-Akonadi-MimeTypes field: if you are not writing a contact (addressbook) + resource, you have to change this to either a known MIME type or one you + install together with the resource. + If your resource can provide data of more than one MIME type, you can + specific all possible ones as a comma separate list. + + Common MIME types are: + * text/directory: for contact data + * text/calendar: for calendar data (there are Akonadi defined subtypes + available, e.g. application/x-vnd.akonadi.calendar.event) + * message/rfc822: for e-mails and usenet news + +The template comes with an input file for KDE's KConfigXT framework +for improved configuration file handling. The generated class is called +"Settings", so access to its data is provided through its singleton +instance method Settings::self(). +See https://techbase.kde.org/Development/Tutorials/Using_KConfig_XT + + +Documentation +-=-=-=-=-=-=- + +The Akonadi-KDE API documentation can be found here: +https://api.kde.org/kdepim/akonadi/html/index.html + +General developer information, e.g. tutorials can be found here: +https://techbase.kde.org/KDE_PIM/Akonadi + +The contact site can be found here: +https://community.kde.org/KDE_PIM/Contact diff --git a/templates/akonadiresource/akonadiresource.kdevtemplate b/templates/akonadiresource/akonadiresource.kdevtemplate new file mode 100644 index 0000000..15892d7 --- /dev/null +++ b/templates/akonadiresource/akonadiresource.kdevtemplate @@ -0,0 +1,76 @@ +[General] +Name=C++ +Name[ar]=سي++ +Name[az]=C++ +Name[ca]=C++ +Name[ca@valencia]=C++ +Name[cs]=C++ +Name[da]=C++ +Name[de]=C++ +Name[el]=C++ +Name[en_GB]=C++ +Name[es]=C++ +Name[et]=C++ +Name[eu]=C++ +Name[fi]=C++ +Name[fr]=C++ +Name[gl]=C++ +Name[hu]=C++ +Name[ia]=C++ +Name[it]=C++ +Name[ko]=C++ +Name[nl]=C++ +Name[pl]=C++ +Name[pt]=C++ +Name[pt_BR]=C++ +Name[ru]=C++ +Name[sk]=C++ +Name[sl]=C++ +Name[sr]=Ц++ +Name[sr@ijekavian]=Ц++ +Name[sr@ijekavianlatin]=C++ +Name[sr@latin]=C++ +Name[sv]=C++ +Name[uk]=C++ +Name[x-test]=xxC++xx +Name[zh_CN]=C++ +Name[zh_TW]=C++ +Comment=Akonadi Resource Template. A template for an Akonadi PIM data resource +Comment[ar]=قالب موارد «أكونادي». قالب لمورد بيانات «أكونادي PIM» +Comment[az]=Akonadi Məlumatlar Mənbəyi Şablonu. Akonadi PİM verilənlər mənbəyi üçün şablon. +Comment[ca]=Una plantilla de recurs de l'Akonadi. Una plantilla per a un recurs de dades PIM de l'Akonadi +Comment[ca@valencia]=Una plantilla de recurs de l'Akonadi. Una plantilla per a un recurs de dades PIM de l'Akonadi +Comment[cs]=Šablona zdroje Akonadi. Šablona pro zdroj dat Akonadi PIM +Comment[da]=Skabelon til Akonadi-ressource. En skabelon til en Akonadi PIM-dataressource +Comment[de]=Vorlage für Akonadi-Ressource. Vorlage für eine Datenressource für das Akonadi-PIM-Framework +Comment[el]=Πρότυπο πόρου akonadi. Ένα πρότυπο για έναν πόρο δεδομένων PIM του Akonadi +Comment[en_GB]=Akonadi Resource Template. A template for an Akonadi PIM data resource +Comment[es]=Plantilla de recursos de Akonadi. Una plantilla para un recurso de datos PIM de Akonadi +Comment[et]=Akonadi ressursi mall. Akonadi PIM-andmete ressursi mall +Comment[eu]=Akonadi baliabide txantiloia. Akonadi PIM datu-baliabide baterako txantiloia +Comment[fi]=Akonadin resurssimalli Akonadin PIM-tietoresurssille +Comment[fr]=Modèle de ressource Akonadi. Un modèle pour une ressource de données « PIM Akonadi » +Comment[gl]=Modelo de recurso de Akonadi. Un modelo para un recurso de datos de xestión de información persoal de Akonadi. +Comment[hu]=Akonadi erőforrássablon. Sablon Akonadi PIM adatforrásokhoz +Comment[ia]=Akonadi Resource Template (Patrono de ressource de Akonadi). Un patrono per un ressource de datos de Akonadi PIM +Comment[it]=Modello di risorsa Akonadi. Un modello per una risorsa di dati di PIM Akonadi +Comment[ko]=Akonadi 자원 템플릿. Akonadi PIM 데이터 자원용 템플릿 +Comment[nl]=Akonadi sjabloon voor hulpbron. Een sjabloon voor een Akonadi PIM gegevenshulpbron +Comment[pl]=Szablon zasobu Akonadi. Szablon dla zasobu danych PIM Akonadi +Comment[pt]=Modelo de Recursos do Akonadi. Um modelo para um recurso de dados PIM do Akonadi +Comment[pt_BR]=Modelo de recurso do Akonadi. Um modelo para um recurso de dados PIM do Akonadi +Comment[ru]=Шаблон источника данных Akonadi. Шаблон для источника данных PIM Akonadi. +Comment[sk]=Šablóna Akonadi zdroja. Šablóna pre zdroj dát Akonadi PIM +Comment[sl]=Predloga vira za Akonadi. Predloga za podatkovni vir Akonadi +Comment[sr]=Шаблон за Аконадијев ресурс ПИМ података +Comment[sr@ijekavian]=Шаблон за Аконадијев ресурс ПИМ података +Comment[sr@ijekavianlatin]=Šablon za Akonadijev resurs PIM podataka +Comment[sr@latin]=Šablon za Akonadijev resurs PIM podataka +Comment[sv]=Akonadi-resursmall. En mall för en Akonadi PIM-dataresurs +Comment[uk]=Шаблон ресурсу Akonadi. Шаблон для ресурсу даних PIM Akonadi +Comment[x-test]=xxAkonadi Resource Template. A template for an Akonadi PIM data resourcexx +Comment[zh_CN]=Akonadi 资源模板。Akonadi 个人信息管理数据资源的模板 +Comment[zh_TW]=Akonadi 資源模板。用於 Akonadi PIM 資料資源的模板 +Category=Akonadi/Resource +Icon=akonadiresource.png +ShowFilesAfterGeneration=src/akonadi_serializer_%{APPNAMELC}.cpp diff --git a/templates/akonadiresource/akonadiresource.png b/templates/akonadiresource/akonadiresource.png new file mode 100644 index 0000000000000000000000000000000000000000..8513893d7ec8eb0f0a98327d3d985a7dba16eef7 GIT binary patch literal 69059 zcmV)QK(xP!P)00009a7bBm0000; z0000;07l7cJ^%m!33hNnX8-^I`2YX_r2zCly>tKp4PZ$`K~#9!?3@WWRqev}SA-@- z5{Z%wiAE_UO&XMFKuLp2qCsg=8Z;?UQWPme=E626q*A0pDiShJdu?;3412HpzTdlh zcizi+J>PfEbI$Xe_k7>+`(OY6b?>#;UVBZ~e=S*}nuj#1)l**sis{NpcYR~a^GCTa zj~iO3?I9H>TM+7=6E~=TqGzdj@hK z|B37-iTG!7zq1aIgnhIB8T&iV=l{HBXpR#ti1sjuyIH7oYkBms{PNd^In#<$Tf`b$ z>Z;gZX+`=G-AFgke@)d-afE(~-9rnqd)Y(T;%pJR1Wzxf>*+fDiCUsQP+xD9I8+YX zy~+Jn=Bbk_#+vgBPmHzljN*E=ksuqyU6@M$k>`>KAFOw$D53wk?}h+wj*izpV1O0 zTKqQTl-lX|1?O<=EdAzvoM4jdT>oC|nZ|p~qSoe|M%%jQ?91$K`V!5j;-@%Me~NvU zZOcAa?@@14@78j-#iHdJ+k(B0eHhPCO_$OwbTc-ah(O(~jr*3bd`RAx`Xq=5a4d{P zRtTa21{h#~0scT39%zM4h!Cjz%DsYTt#P+1b7I%D)}V&C+M(4MXP;`6YMk0A#g44q&c3s( zc==1+Imz2Morw-k%QQX2ior@E$_ER(O~9o$CsV zxi`6$Xacu{+khr>*}Ms8DQ_sx4DIEq^RA)`ytTYIuL7}nS9nj67tfaG zjIQwvc{`9bZw7BN(&Y{3Hlr!rI&LWv;}&wgU@F%eo`tsX3_@rNcKI*&U_;`*K%zdt zEcglA1Kx)osEvrg>|hQtJ76+Qg~^CdD{903^Y~6gV0wWF(+fqQ7!*PJ$Pno>n#@W@ z6Y*h)_$)+=(L#6|dcoU>PZMg!7W^rA5L&?l_+4js4W*)&=oR9_BkKRX2#mna1tlOC z@o69;z=tpt+W;D4Ux4S~1>}luV)tFE0S5RF26)0H|6S|HGa((Zule4!kH$a7HR{*1 zYbC2FdV7sm#qJufuKjH-T?elxUW>mTE2JSTI`~U`P_%G-Y^!-=WvkZ8*DC^-Z&O+{ zWrmV5dr!3{+nm!@ql^oRd6|nI4R||oIK9l@>I@xdP}(uoy0aN3!taedX!CJd(;$^rq;IB$d{h3 zjjlE=p=(E0JuBH;6H^&cTu?Ksa(wZ%>U$N3i?yq_mzRI5sp3_b6wj^RRMAj;y!uw< zgW`-Dm8z%`gPO4FT_x{og=@r1_tu8iyf1xTTVLx`Hk5shtyktvPo(wAHR%M7dU-f! zB`3T>jg!Htsl3Z6Z;+^6*&x~erR{zD7le=tHk-H?wPXLE07U(#^rs%xVHXqiq8{u@ zqC%ohB0gnA`9uX+Ur{qDSw5{qc|;|s6uS#`V-xUPjaYdo7psG)|D~uu@Rt|_eoZ)x zwF4bS7Fc}nI%D;vnCL4}18T(ny)Z-;Xb(1@4%CTVNK}T(v3ZEW=2J_QkG^1)67lIG z`bJbn#Pm@<5g@82;t+9(z7XXT^%Av{3-ed3{9jAs^8P;Loe|Q<+>k zdSA=EfR84)*1d48%T%qQcd;K;#j(Shb?a9(uY2wB;^phz@i9@IF@-I=8Wy%(Ydps} z(|D?Ac)n$kebBIg6G0-ixA~_c#&sv+DQsv!AtYMQZ)^Z`OJTYQ;`QpRgz5 z*Au%WT1>knyo{ezdKoLRZ{;gh&whIM;p}%ar+DVf$e6R$`{*2#C6oNuEaa}u4U1Y4 zW^y1>RR8gg+!&vY*;a$%JvQ~&2qky#h(6((DrRAM;mn(ID|l7^o!()8*In69jw-sB ze!O+!%FWB!UyhnMZ+U;ycEp9AjOW(2c1vH`Sj5_Bq@%yP2<_ZUXN$FsaoGLP(5qt;p1y~vn;M~L0KHHo|(g| zM+Cf8@38yut42dnAU2`NqIpWwG0vAdf6h152h*ahA*Mxd z;kohRd47m+e3G5r@rj{F!rDTQd15Ffrw1m!gCCe60%OIT zWULUOPOZ+PPA%6n&n(xoB)-JGBpw2o2O%Or88{LSMOCOCZ?_KKKMP?JjD$%j8pWen z_yK0Y7l;o)5UUGyqAtAe)bHyjIJUlxEQX18&EI#1_J&}18RbXa2Z^PGEpANMmzDhn8Pd39$rCwx`_rD;E%3# z9;tO-QtQsR)@7@f(0e$NHR7DH?2fAM?DnRZy6~naEd>o~&) zODM`=6GaKmxRNcXOi?b;6h)=pOr)fQhk8JY8ino!3`&z3^l-IE-lTQGYN7&aRQQR} zA#*py?UZIMUY?{S`@$ghC8e~+#3Vya&0wc#md|uktA(GGG*|ALn>}KB{U(i!aJ3VL z`Y%0|N|%HtJ(e?@zbp<(=c{E$$Vq5TstdLeH5pg^aGS_xu{duT0qw!GM=)N8+?!66 z?4Zxrl?MTea+IVfg$>T96*hccot5=@HJ4)axD?{^486qW1Br$b5qfiagL(_*OD@== z^-b>jL^C-bC2hgWkIrw;=ORKgtGT?{zr(Aey5kV(P({9x6({6_P!0q4`a66cA#*X>Rr>apUcHhe(mx%up zKo@GqCUgaMVKdhlcjm^gbw1Lo)H|h9tJAD=De6PFUiXr2yvj^97R|?itvh%A7*dN%vRlRIpEw5^DZFfy(MMF&* zZC`zoK2En(H_+ed+UiQW_HWfXzW>2~h=Q;c@oU94$vW3I$ul1Un$LWos7uupC1mG3 zQ7BuGcI^_d92R)1T#zHyb4Pk;={UjrH%FY3-|1f`&Q+BPS}UnIJ3oAp%-IFXth@=~ z%M+hmRg5#xd^T%p*=Cj0tJAG^Sf)E^*zUgZ_J~IFj<4zY8s3{TQwyelF?jdPMNMu+ z$dhWNS=vES^Cv25c!oR41gNeK%9AWm+8>u$K8kcyd>fS5Q7*f3`fCXi17Mj8Y^w*>aDAS zR({n#pucL(^i)*ad~D)dljOXwj)FnqQl)i6-UdWncmrGffdu%^_->{`*)SBQ|iB5zObPP@`m z$LzSKC${LC+9f0<8YSc*g2Q_U=d;lpa*f8U&k@fXt1)`CtGe3|SEY)z72hh>E;C+n zVVN-^;0p6%t|5Y(?rkNvr8B0@j+rqZ5vI;oS5|?ZkzVeezvllT@ncUXpS+LI^n)`BrrRSz(3@b#z)2R@EY4fJFgv&HlG%~$nIBGOPv(BazbBD1 zS216(eaHUI`%YNNyL!95b?rffn3P!c*crwPH>w)XHhsRCYx**OM*jAE#g2eZ(T*>Z z7b$(1RM+{j^JwP+>*f1vt*tLvIi9<;o4&&tMc7?K9OrVwlGbks65%PpQ}^k|e+nW9S2CW+*QMiCt|v~--qJv3#t10LII2`Wo=t5r8Bt?}CE_0?-t=(5npP&@fhg}4d6h!FKCa#duCP(ov!{Ic@!{86@gT2(5kQ1;%hH~5fvb-lcU;c1 zUcax~nm1HuSfy|@+zbu^n>FqRg=;?8shm1&Cyxl+A>2vaU*9{^ZqDGD_A7ez99D2< z9hzM?E1R{GrOt{*1epSv#p4vWx|{FcTBs>F&vs7Nd6#qG{N8r8c7yiJVGl$+h4)+d zSe)AJQMIXdP}K`WkX0R@Cw(ME{rRO7moxR}dd^fEWUnkqeAR8^j?sGma)-fh%fC2vOS{JNRq1VtQ z)y;He-N>3qPGd&&8|$}*PkRzBJyn09m|T=%`#dvw{fmVuamlk%5|TG386|m^Bourp z=G8r@^{RVDpR2Z~PyD^L9^9wa{cx=(2IQVjq^R>paIG6U2?@@kD3@&%CA8TwcF@xH!v5>y<&3M3UPIxkm9I5^pK(bTx%@+;>}irP70=FHM?6X~0(KSJ`P z93~|`)E~7}*NgRe?uwN!Ss^;c>Y;&ern<>5@fj~;He$uCH^Xcx%8`R>UCR0KWWn5n znclevajmcVty+hP@EHt2gw_kKkuA@a@2G5<{ARnjx#RXlN7GMu921thEEO%ev~p+p zx-#A*ZKZ3ISURmsRCQizRn0%HSLjuAkhXjQeMQVkAkYbJRmLC7BaJP$zkJ@%O&Qtqt!rI~M7V7t`PFVYR zbufNwDjGjBL~QPenVI2PQ?kU=Z_ZRuKXvZgd35gE=gYb4K3^7qA`1i{A~1KEAm%P2 zT)XFT@7g{2SVc2=KSVHjv02A>$gYb!BX{cZf*ET@@!hufGcp_?8>E68C;d+yI{AI3 z>P(-R<5&lxl~{HZ0my;TKu{-sNv}@wLHR@82PYx|{->lTz4s77qFaiQsPdWO^W4YH zZiJoiu`cZy+HgLPC(RqgJE^>0<)Lz@QRKvg1OIo(YvvCQLho=^NnbL0siD#ui005mmS+Mgk4bIl3krC z{$XuwQ>~7t-^2HUxS+d>vsEp&VA4xZ$ZtLT2TwGKi(fx_aU`jNNPPB*ZP7hX9d+L%2}S08x(j0P?Sj3tyY0l(TjJc4H-J-_PwZ~ z`m*DE6~$B(_6N=p|EQ`Q(k4|hb7i=K^xC-&EGs#o`4v&M6BjQ|k5iYQv-BX&kxRzL zt&*R+U_rFv#LaWxvG&Uu;XX5vwpUFI)s`Ni@GsD9+)zTz;gG=JtueDCcaEcc$&P+)S_+IrT0rRZ@ky>Y4_9h9kxTu z{K^jTVw>W5CEHKkKe^6sg}L7LOPG6N^hIyxr7tow z*!IIr0TDb!?tJz5YVmAO?rv&zje+JG14oOi#~rsK!hL}UIrk548L@SnDR=3&6-rCX zSRbQRSszQcmUfnIU0b%UbZr?T0B;Zkyb&RCD(iOS)ZI(=l?s*Z;i!=`BcGz~LbzDE;ee%PJ>9*>&qBfgDR)sJjHxMC9EXy(r zSj6sfG0!+W&ZhOCY0Z_I=GyUya5==W`_h-~N6nvXPhHltB4imIlpAt1D33jo?Z6(n zT4AlyYK7i}-kx3`M2I>NnG~UF+HF>}dB(L4mjq{BMEFpVE&QQ<|6c19s|kA#?X%ju z)Yso%$2S}ifIV;o_Go|s{_I*`Re6$qy}#LeF_sEme$tc7r=1HpH0{SUX}?}IEpg#n zI^shq>f$_nx4yx7@Z=4+)<2P2U-Um}y+3uR9-9F4fDzDRuHrl3tAF(<9sx$ct1to; zp?vJW=W)Zn^Ze^+AbN=WvHqxM$@?$I37thwFde>w>4?t{qW>)*f&>2*#sL2>)OvMM zqwXcJpE@TDi#;Ec#|jHi2wNKz8|oW;HgNdkv0?5Z z+wa%hSM#R6j?X*u^>E`Aj&h^z@2PcJ{93jZ)D^UXGt|R^Gq}XhoX53Ze=%6Ff%97wAG&S9VoHVv+6@|>0e``#GTg{j+6y>mp zq7-L3A5)x}Yo7Tf*PLs_XmgEzqt?5y{!%?F(F(MPdBVJ4qL48%L7UKPtTg5w^O<=A z*;u7aF~ea>;9cks?*MP$haCbUKnVN{Kfye35bOt*;37B&j>9of3Q8gZ^NPt~UcnUj z8aoH(!5k)udCMdr0@ww112f=(m*tA5<}>f`5^jKthyYH3OW2;k7kDy`j0fWg4WS7% zM22VsGGvZ27n!4oPX`eJZi3t4CZ?QeWXgX{;bL_F8?XoLgUg@})B&$S8i+$mXfl$6 zFW@Wq5(okzrU$P_Fu2cbWOgwd_|*(n0~74;pa*>Ke+gt_nN%hg#=~bY0jA;QJYzzc z7$y*%M5oXxxCX9(Yw-6T02Xj8RKRwGF3=HGlLtLlLIb!090A9HHAnjDNG;qd}vC;v$CioQM%!p;qcL_;0ZBJteWu6e(C;C{B81l zJ`3eNX;S25G)?$N?$*11-mO1e`txqx?jWvpwR6V>)$n~Wg&E>#Mq#dvqXek)*Y{9^ z1Q*|&LQx{CZ(k93If8YkWAJ=2X|K0J%~E*}77z835q|J;xUuXBKcV6E6AnKV5KWP@ z_CF(9KVgeM6#XKb?YC_NB{TNHr{O!L${uVUnlswQt5jHHk&Atlx#AnYt7FjcVTQlsEEeBoJ6Q3@=_JqoPcgIOx%Zhh+it%*Pgnl8Xdohhub>=!MZ?fY>^|VN4aS}c=fRnO^_Qjp&h)Q_ zCny0u0f&G+IE47L6ZMDJ=TC<`(0Ozby+O(7S-;x)|B)6V0yqnz{SU5nUZom) zapNJ*+(u{C_K3X5hJ2J8o^Mz*DF61Cg7**KCS|n5d&lmMRf#f;+!?MDZyGZ+Iwwpd zXouhC{C%Il=CK=DoEuFNzo*vC@oV$%X?63yr(L)cbnn6)ftMFw2)rCs;W%tmh3NjP zZ$m4=GJ;N8?+B0nC@bRu;!ygQP?=oz7%*Y-m-;rEl zpUWY_J~ORvxXrZwd@J+X=Ud#1%xdn%ezpEvv=a3zQ&<2?u*X9gI3AROVo-`6pnKSS zx`+s%5p-e)fN&6iuApn!1b78H!b_kP)Pq`R1WlpIfB%%Q9<2iI;1+O&&)^dng9y+b zI$#SyLF_qj7Mz0){1kspKow{JRY(F!VH037e2Kjbu7*pY9scYol=$<`4%`E7;5v+e zLGWS!?dmS?p}Nb_d-8n!dZdr^@R~S-E3gT&VKw5DjFkkA0|#&%j)fE8SX7Csuz%jO z*TbuL%g!@fnQe?wPkhgpo&>}v9oK9c>csj1&%+DQp2=lCG4FfydiM3KK?G<6k3k!t z3k-oSnuD~^9LOLFGB6e5q_)Xz zd)g);0?-G`fEE%%BhV1E1sS7_KPwQNLW3X|b|V6`g(tBoD2S~C=VLd+M%avxpg#z} z%U*-suT0?%XbE=!OK=!iB5kx7yOW5(C@>m~Lf_|w;3y~p1;{pJ;p6fI#s8XKjE`n9 zFaw)_88SuNu?gTMc#rJ{yn!1c@a%avc=p^VZU#3B@p+FjvHLv54@=<}`0=l57w>&+ zK8i>MDdIh10G7e)@HV`T2;D6`V%;ss2H9dKVm(8I)`ZqCZKK&{>=*1yh|grC{9o+d zXH=Buy6|x|vBY3E8l$n7XrjiL#FAKJOEel|#fBxK^dcQWKrl#?rqTqFj?$4Tb!gJt z&bFyc>@6KL3?}xL_THD{ft}pU|(P#KGGv4j?Nbjjef&Y2^(2our>+@*=qh~VU~a{>=z0J z&B7Vt^8%i5lk5Ybhj5luC?E?*WiN%tgz>Upg%gCAqci|?Xp=yb6weXyhA*dA^C~pX=$GFNb$GQde$}!;_!6`+Opl_^6u}csqTP=J#SI0RcVu=lE9q zb0D4$$gp2YR3N{nYym2$(+>gZaZa3$O)Kh=q8F!8y8UbAVvC z%OT8em-e1ELVFMN{S2qzC{zJSH{;J#5MpUSs~ZCDsEf575GV^N8I+GbSKi<6xe^En zj?O}GoQsQbHW1ir*pJw2TraqVxn2MQdK$fno(4X#2pU^WkjA#|ZGCy&TOi=IcoSX= zN@D53qOr%A9}pTpdV8edSBrY{BGc3!Rj;sjHxMuce;-4j9|m=^1NQ4?+(9@B`=JS1 zpz&|K8H4`200}?@0{yRIe*d^VWp3qBWp0sQMR6m)0s`WRJViV~4l>>P84TeVG(!Q9 z;5O`nb1)7go-cBMryJtrGdhaO&`}^{Gjq3PGZUXDf0pA1iy6kAYC! zQ!=Z#Ck(>-!+?B_EGM4>0xrijxEu)h3Vc1j0toiA9BJ#XH3g;;$XfJEb`o0s(8#U5DI1yD+nXr z4hX`}g;#{1Z$I?id)p5^ob&L~!#P0U%;i{c=971l=a4ryDw@7*x&S+1CR~GY(eNB% zfbdq*IQq7?@nutbV?K&vx3EuuKs`Ziq#87DYC7LIN;^$UrriaCVuiv*@k9L=b=`Fd z{FOXg-gF=^4>KP!ziS#?%?r(JEglyYD>Pg4{+q#R{)`Je;=!FW7)Um#$}t(P;3@j&{^Ge zsI%sP&%sX)c()n0|JHT^C*jd}b^VHlHT6?lELv~0IOAh*9H!$I_z(CYAecv+C7Emu zK|;KONb>LH{qk(aL*`eEXZ3Y;l-knKBP{;#vt=L zqphW{b$Lr)e|G+fzVCqQg!{_*#EZUttDQCJLA`Yz_aE}vnb$L>#c|?)kK=-#H`~ijzS)7LV0`RF zdPnO0w9Otio~t|@icL#RicOMODch5nK;WF>ByfHxb$e4(YR+!v++z!XV6n$4-C~z~ zntY242SR2|W>rSu#SaYA&u$V4#cAT1<$?-+IZqTUB8vhn$80WGjsZdJB&Lg>*ZJ1X zsT~qkh}H;eVBAjl8oq>g{++BJq57~M@%FM_^Y-X=+)XNMlAe{dN;e?uH4WM=ti8+} z2B}h9h7|=CoGthwmr`t1Qd0afjh$AN8r^-X!>QYo|B8K$pE#a%4Y4fPL zHQbu_Yq*LW<%f!#vCc8}Sf|iKWFdSa{6)N6z!0L+X_Eb-3=vO?a(&r5IdrOqmPY$f zVj?Y)+!XyHx+^+5dU>>D^gOMCew$XRG*jj&Z}86Yp74&*Gnv-(L~g4<$*qwoRlmsu zoF1MTr(ZX*hx+{i=;qBl&BuaK z=0#;F5I7Imb2)-)esy0pAM?R#FrUAAw2mLb5C3-{N&-vVOM+wVVq0Tx2BAUM9_<5y z$6l`xk5#6dO<$VYJIC4wI5eg8rbeXA0Rl3H&=3|7BA!NyBR@H}-thf%Q&Lx@o=Qnj z?^9W;EiHa9lbE;O|K7{T^V6f%$6b%D1cLuT|6Jd~+m}tJ-RdzKx?FQP5POa#V@(&| zTyeWt?G@}9?`35E^mdBL(-u<84=wT6=KVI|+I-Vn=JlrMs0XR7)I&gUn|g1n+vMY! zCw-5HcXoA=yB0m0`YbG3Xd7&E-+G7LF}oeMJGnEs&$v54kJT!hdscIi4s;8;#+UW* z+V4&d)9t8t@?4I)+;y_kJl7x^isD1XR>e|xTaQ)lV`opC8#yEFz1Ab`eZModE2fiS zRcGaAdEd(3>Xe1ZG0GVn1)!(>R>$}4A=Vz&u2vRyh~48mKbxjmd}B5d2!-}VHia}J z`)eLX&%8xGhrF+l-XpQAv7M^jJ9Y|^m?@W%27wSt2|p5c_1tqq`*T+5;`HHkXV7Dq za_O}p*F4qgSBvF!ZuRMPCWnWAEIqO;m=T}|4DT}Pn$|hX|F}QX?_;~`cQ)E+U9(-a z&SOA0`Yb~SRj{P2P5_ahHXKu__Ml5dKq#9oNg#GYf9bJnp7%}}$KCJiw*{3P)DM}SSfBuKOIrbY6y4QBU?RGRPw|Hef z=eHR*4Q_aAleIgwUU#w_*54_Xy(ilyn*@ZYf*8-J+TG>*uk1Nmf3x9Cz2n^=_v*V3 zSs$|QvZmV4aCEi*4hUW?_aAv!-gn_)B0}oe<<^mO_X`EM>vz& zBf7&oAN3vWFB(|UU!+>15vrC9zZq^HenU1V&m^0VZsA#tZV{)*8pSF7u6lR zdE_WGSbY}_<}4iD!&ykC_3Z7LOPw^#8Zm$ksu=)7^NTY9IxSuuL{{Ch6`^Jciw=C)6ij98V$Vkmq;1hM#(3 z5+2r3+DYzUjYvmAMizRgJ^bALKu;aF!Kr>`aBuMBpe8%EU4-3F5Au8j-piV|ww!4W z2;BLoFK`VI%r;nP&5lYw6yFjrKe*wCFZWJ0qi0Rp&0m`?2ZF1E%cqWAgG&Yu_b24WdAj(NedW9gMmawwG5X`k6-}bXdvC?`Tt*jTgKLOerK>0C3BMdV9Q?NGRpqjZ zOTzmi578W#`FB@4vor^^6E%4cYJQ*OO$+CTPY=I-s^rwElb;GLgwKTgEG=wSS_T54 z%&cNgnSI#o(8VEhY<5^nt-bGgxoh2~fu3OR&~w2qO`DtWrVZi<$x?Bo-9`JWb{By_ zJ4I8|wwuqi>iP6a)Bt|8AB!8Y53hI%UYM5w@O3lo#DPX?N zQuyeS>v%8Ot``>sT20i6hE0FSQVMU>(A?4;(3pYXYG{~!=-i|jwLEgHQr zS}}Uk?Ni-zGAF)}H^%eb=eCFOeR9Ow=*&nTceVS8yH2V-%1UJ-jC%&r5C#A7h^(*E z%X%-7^?QG0owtjzi)z8@)$Lnf!7DEG&o3){#%|@=aIOerM0P^sK}P@Up6()A(XM>s zyzJb?8TG~eg+p1A)cYx)Cy=_s+oQ-?{CIX0zn;iCne<22dw66WxX zy5(p-D{3?!BDA#-A@Jk73H)efv_mwSXpNSm-U)|mUVn0hGS|Vke$e@x|2+4X5W~8QGJZWtxgQf4T}zKj9wSn z6FJkP%>(n`ksHZ{XLlY-wmYwQy&Fx?t_O>xMheug03N4D6`$kM@OjFH6=J#}<=|cNP>Cd!Q7w z5S88>xXHTN352-t=MHfxdA9jG^XN%wiPi~gyhH9wybel_O9!OkKqx!*=1nQpblq*b zF@LO6P$T%-lker|xh-KJq4P!3om#u!Y|8tT{R{fML632yvAgjj#TG@T;<}@SW2Zw< zT3T9b>K)5jmO*AJwWm5u?Po=@6k8hPoXxh&sx;YjE7Qa@6SDfU7SyaEy&+xj9`pI= zeuckDzI^bjfq>xX7ZGu#x{=s>nRd<%( z>AAB2^mrS3-|#jhZ6fU^ZQ}dz1NlCdWtQcZl|T^h7B`5E>>k+--$_hgmu{3Qv`VpF zW)ZF0rMjqEc2jjzVLSl{u_xo4V^5~;N}HFu3kWyde+#+c4tm}!E{!OCV*ZO+mB~;M zR=BE&e`50qs}tKmPo7zxW1d;`nn_WIsUR^%)nEY5b zpvN>$x8g;sMVm#-(*05orTT_PuF|FYNTpK-FYEgZzI?rWf_&?_&(6A>?s#j_ zFr^{=aPQ&uM<%nDvT|4pEESgBmI^6DIx1xVp=o2&wx*5tckJEm?*PF#!T5!70@I#p z!?d(0wxC#uG)bCyYO?8G!mV6VaijE75vBB*I!XOpoe$$kP^FvyctqBh>1BP+ zA6ajC^+(n@-i)uQmzgVgcSjAm2MP}6I_35-3t0w?jodX{7H2Bik6hj5S(s5UFLyBa zv+Tf(8AV3`{EAc&WIi zc&YN&OjP-+tdInim12>)OR-3KL`zd1(X2o+bknIdKsxbqO)p*!fS3S4C2|%j@dFwy zegG=8won0pm;%5iEdw@zopuM<0bq*(0EhVia0&~>%kd#J8jlA8H<1(0jn`$pfXI5& zKh63*+#a_A!r=9RoBdtAvAtJ%E(1ZkM(e9}YOZT?YRsB@&4Di!jLp$t#I# zeL+K5eG=%A_sS2-rp1iJ_{E$|`1XZ5!L(LY*H$|KguK|iu)O7FJTrHbg}=$K+g+aw z1XdUG0dudvAYi*+ZozQjkA(|>P)@1fl@9}<|F?d6|EZyPit$i_e5&k2S)0GF-`j^@ z^;!1++!qW4&02Mj#xVM1q&RXy`L`8U%a^Jgm3NiaK~n;3{R3*(kz}Nwfq;L8FT!U6 zp**=Fv-~j-=!Ud?RAxY^AL_Tb<5_!18;lz+9-cJh_;`0X_wm)#ZMtx58mgiM4m}4# zN_xt}WZ#7KFTRNnY6yM%tbq#z%odBnE(0MWHpA;xPJG7m_&7?9y@|)RjK8*S#swi@%?2wc%Nsn@iS`zg^;+4criQY{Qo3Az5 zf}WQ0)+a6HoHLvf&KdPJwYT~j$+yOfd!Ud>$tB+#~ywrvl-Pq72>) z0in;JFSXBLFnnmsU^oyeE>xbWI0WPTAr1QR6BrdmQ`=HaQqp3{v37CWO8324So#U* zp_$UgXr}F-wsYJ6>(8?zvSwtShzgHf5b>?bScxlPT#cLWNL~gWx0tc4VAfjQ zRm)U3M;v_$PsItR623^V??%X3-J{Ie%!kam-5+(gch3hs)$SzEYIlWDxltj6Uhs$B zJdfP;JdeP~K|cnDwPEd~_C-J-IaTwke4jc$o%d9n5tF$#!%BHiS*LU*#neQSVzrUl zENui3_y&AqzCnF+Ls@+@5Gu{8?pB(~X^Po$T4hvKY^5&{Ms`zp6uXBRev5rG%7qm_ zRxE~b5%35ifl93ZczPw%SU7Pburrj`e zWCEp-wS}>kKE3^IM`qjWJYk+FdvV6>4BpEv*`B$=oRFmNl0xGakUwg#ZKCkK*uf05 z@vM&!S-@IGD>!rH?T4Wor2e znVK<$@d0B>^Z6!r^LcI+--%l#*dw|u*fTtrLLQ#m5!ZRYBTl_Y%Th05saPMdRGm}W zzwMmDp3X{PPe+_p*AeHT<3q_q$GfIExn}V#doq{a#4sr_l+Mv%+=0F7i zRtP|UT2}_*ZqDmMg_FpGwM0V#0!PTM;#%mk9!q4s{r}3kJ`J(PJ%@OBjxZ2JgJ{ccH`v zM-D!W_x=yLU;=*sDyFYyL<1y$U5j{T}x#B29fn7#$4(FN-TfDXbs&yXHM4A ztjU8BgDnFuhP9MO!>lGs(@2wT9y!N7^Zg8)SKTl3vQOvO<}6N3Nb-CxB(HC8YuV0s zV+S%k##u7!k?Lv~ir1Qzs%z3Tsev4$_K$>8m-cZtMo`Ms`E*@Ol6WZUzyaqt5-LJMFZ0Yi*OEJjB`c@*~>-;duEVV^z3G|F`v`> zVJ(saFu0?z9RhmyW`7M|O7^h~@DG5%!Py1e7+u!m{zcZsx+MYv9)>@}@8c_pec9ul zLKH+`gIEhzgN0(zSTLM~Gw>_ifSYiGIE@(z`V8R|97d<0?;$Aag?gagSTdG{CBe8L z2-eLw39(6zfYbjt+lV9~sYoPt7Q2KQ0s-5B9l&+~0l$D>(~TGIhkLWlhgl~^NI33{+0jD-#1I84&bE69LXm^J2#S%V(j3crb8 zMf?$0*A4f`Aj|Mk&*4!9fc00e9|_7k=n^ff{)l%Y4#tLPc@I%q+(&~mf@H9$~A2F`Ftw+I*$V_`I81+oa4j>~Zj*AQ3j z0zyYdkOAGltt;w|uf;d%rVa1b&G^PdJ=k^30=o{O5Uv|7s6mV3s0zgqU*r+uOB_$z z0tDLTAEB>wSL7uGK?nq4*1BtA4K+}!n{kU_87#)4v1Baz&l3RWu%qw* zeBl9h3cG^+`sePT-*G=A7oOwimx!!CVZ2BEl48d{ z$6iVA&D#9x^YoDJWO99%OW(qQX+5TIFE(y|>z=303CSFK#m@ZVWn6Aco>8_>Dl6$y z;-v1-_Ka2;{{{OA9sT=Rf33@Ub4|A{>vpOgns3lA6d96KrIFA@Fetn#_(>2eI4Ykk zKO~>5Sg9~jtQ-p*8y*W3U6KTeE-Cx8k;*>tkbJ9nNJtXa2}$b5@)Gr9;3EdWm;EgM zR`#_5qK-Z$yT2s_oGE-tDnW?B#u29qgLHS7OsC*>xm5?M++BsT3O%dG2 zX8}mvSuH@8En&6*Ac;yf4&%5^9Di=&UuOM(hDf*mJC0NFS@;z6EP4#xiQD1E`0aO} z!t>5M@3KzEshcU5d^`3Rj1MygUdE-Sk|n(>dftvq8KO}J+77jbHBZa?Bu|;WEvGee z;AKE@#Ow3fO{q@F{s~dtksT*m3;563<_v?spY^xYZfaA_gW9zuhK*e?76#InO$Oe$LP zAs;RrNgFPdr^=b~)Pbgf`vXnQxs6WExgrnQERhHAG(UlNn!K{Bn7p#Zxw)*x833sO z7@0LK07F3~C*Wn1EX6^-lD}X$M4bQxPS^m7Ky|-p7I%BKyt<}Zj@`!^ zu>0?_{?0q^{IARUeCkO`IDhtNJpE>NboP_X`jG&djQW)QfNRJ8s87^u&{b4yR`eqO zLf*yPs2pkWh2pJ+3sR%fGSlAc9_~2XI)NX=zQx%7_p{za?a*c2RrhG!1>tCUh)8)w zQLVf}W79Aid+6$r<4nhaI(c>SnFoSPt*WSkA!e z0c2n``|#*)_F(|zAOJOxx|JF@7&quJ7$-NBACViXZv3$ZY6t2EYH3So`)Nxc4vHa; zyOg_=yL3P?`2OGuu87yeU7(^U-e_T%SKbK*!+T4n0SL8BBaI~{1whK#9gIBAY0@vG z$)sN}TdWkbeV6rj-g)PLUDh{JFHoNG(nlTWtMU)zG-o|yf5t>uFNAl5<^ul#tDz@@ z``^$@6N>y_`xG3`%P)FeYFLb9`oHW-W%pQi^ysp_j(z0+E9=CIFmAP<*0>=UQiWg& zjlw}UZ3>m5O=+lHs~e#@RVh^FYTJ~#ieQbqB3SK+%uqXm1M&qpz(kD%CgQo;|HIyS z05z4r3mXfrU9l{?h}~5edvDlvZGaW9psu=ty0Xez*IrQ9z9K4CY`7p0Y9OH{BwzzV z2^|7SPLh*CN$=;p_js6ZFZ1nu`}be(?)}d5&hyR~GvMINGd~WTWHg=#U}XS+23rjp z+!KF=dx9U#A#7nic*AE?q3)lqHr zy$K$)U6G11TggHq>kCo0e=O?)BI_q|OgSfP2JD5+K;-@l_jdn~(%UX@)eO)1MRP0>F#%-26|VmGg7V%NN^-Ce^j5toiH zS+8?a?=TFJ94ki6H!IHP>}cjnE(%>59~va)emXN!kD!tjIgT7*jw2R;9mWEFJFRC> zr*#Snia$x#mk56nu$ALvZA1x-PARkKGt|S>{hDW*Pg}e!J*5qjIg$_TO7>Uj{%Q2o zcFApxN06c@gK{2Es^zOITqp~Oe40-)@bxFD2yILXW@RK zGv8X$hi{ellfRbt)6Fr4>*iFxtXy9CvS3T0u3(FGoFUdaPF15^tg0y)UUIo)cvWJ} zu&P8L5rheX1Yw1b3eOfkYBV%YX*8IsOo67Vvc+Y}vc<*AOQMUH8@%-k4c@X2(jZv} z;_gdzWKEfsV=u5KOERUa zkag^b?BEO}gO)ax9>Lkc8pSA$x6!&r#*}v`8PDsYnvaf0ru?H>Kb~XFId0QoM{GLd z0)yJP04+jG&?3oYshi}oda~B6o~&b;j_O#_x6+Q%w~7+Ex1t0;Yf<87HPEEffb^)e zyYy&tWb?}ANFY({R(!A6ExRqF%Wj+fO{2{IW~MRJ%oGceM6nPR$R42rU9hgJE?9Oz z_MI%Kenw4w{m$C&1q*A=R`n_Kt$E5j&2*L|@tZTpsI-Dte1*h2`&-Ux7S4!B>%uUr z?lnzP-GdhLflH_c10e+k1;w8s>nnwqh^+UQvPBP>eHq>919Wo?+jWTmFbZoU-z4Wr z%D84WE%ScXjI`PG37j^pSC38et_vAO6g{&FOhY9UypS`Xr)0K=VV5!{D3CL21kPPy)KJV{7ty_rG zx*9uZQya8~>jtefQr1=)S#UFNbHUBh^72)s%LR9$31 zH3yr&)*LJ@;O{RkC~Cv+UepFikhRECWNnd4VP%mEx)D8sZZt2p(9DY)$2L?nj%|u) zYG{gR4r%<*98&y*|GfBF&6(PWn*Bhco2nh8yDbS5p%N_{;S9;%lGHx&UD62Q74bgd zIeP+r#GV8g0AUW45w?I+VW*&=_|JibwaR+r$E@!eOlJK&nRTA}1?3JS@B`JX~q5pjBE`agA+NaoYPvH|_nZ9aXmo zM^Yg85<{F<$^^BrF9Elu{w5prH7l0=x0wSx-zPPwTE9v#$I0W7b7H;a!0O zt&sj8)MS{`9;OFdwdTd-Tj~b`$d}7!$`-PZu?J=D&v?PyOAqI|WF2Fe61-w#qxzIq z7Mlw&#bI zXSkiX=b{s7lcEzVX%$^6X;_tgK2`;40Kzm#BE$frUVeyzg5rM;?1%r(dferYS>MV2 znDvHDAG2OFPIyyLEM!U_i@ef$FyEz$jNJ^|bl2^<_8#9zvgT*w~9I+xZteGS7g zxp$_2x({N1cmU5qoPi!eraN4(BC}rc_hp^Dwtm_M*ll|o18P`dK+U%- zd4w+5D~pTC--ej{Ee7}aP6;~kn;{|*)J6gP`^a>N`5j+oXd?gFh-MRmoO71j1h zmOb`KtzHx~jCz?N3JQup16WY2tiSn~b^kVG*2QGjLxdLuY*Ch!E=r;QkSb43)eP2m z*92Pk*m~GbOA{nM;-u^!vYb&73?c|ggaV;dD9-sMw`b0A`*7^J-O2ccA<*b*WSMe|S;kCjrZLmF$I3PC zvA9`n7B^_HUW5jiZ+!;yL1TRg8hDMpgV(SQ=!BQy0AV9&2zd|zJS@+KVR@#p=JBSn z`USd0`UOpS%`ckr8d4Pn4XNefWiI966&__rD?GBBIHB2138#{H38!kRYg1~fv3B-b zSUUiCMfe=Jgh0LLIRyp9p9O4xtE{J!r*#9F^$%p$PYTZpazwYK*`iYMn%*fnGx1AiTZU`W%E-hhZPWtZ=f!sl0GWtxK&Jog ztTz)rh8Kh+!YrXx7@e~r=WR}m)!+WDm8t(h|Bc?y5McVsu*Y!8I?-@RUu%id*P1w% z91{oV7DC))5#T0pv3P+CxL{v?+;EfJ-3Z%5Z^E;%4bEbIwvCvdA;C1lkf0l-?V%gh zxT|SE<1XbAG+w!+u6G?Fs~h_~s~i1M>QD4X!XDyZggvlt?bX<~&_XnnLNpb03JQw9 zSk?~-4+*kFiBh8|iry`yEB&BqY{MK?u;!j-pxU)|LiOdU9HxZvDrI8w!o=>hD(3Li zi}4#GA@WW{X#rj&pLSZ0gIGcmk(=;}5OPnw`Qpt&Ai?%odDtR68taKC z;W1Ey$KZDX@VmGjY=kXX|1h(5Xt9AX<)gS7tH#xqsTQSWs^OgRxZ#{OM9t8Ky#4b1 z;kRFAbj=9Q=!!Ws{Xb4d<1{s_x~PrGpvAQzs`KN(xL+#jZ(nPQD-CD{5fG_7{%}%tc8&6EYLM zC${~qtZTqPD1%r+lI9_e>CI!`D03IQq31HWIk~|cI%g$^%_`%pX7Slx+?i}I_Cl_N zy^!6R%VKwC?c=^-?PHzd=CDq&j&loH$BBJ`+}`BgO6-%|Ev%EQAGxboKeAG|A6O|Y z9@mM*V;$gRu?}#ZI4ihLIor6VoNezjKXiDX*;vqEXeeO>e+UxkcCZy;y?AnUDA0YXTE7vQ7t59+1r)oO2vpJa|?7s8k5 zks_o>f*{4nTgh|eEy9%?N4Q9#Bp4|~bdqz34rwNKGoqH9LDa;)MeaxBe(>AYN**Ix zL@qgt$dO!0JmD2dHsLgBC&@G!6D^i8P47(GP458UAt3_q2}?i-CDac^DJUrZ@>yRh zTp_qE-Xn_=`_R6NA0IbQuuL$fwp#SC{)T8JFND`2-;i`Uu`sS%tXpi`=rwV?gv$wM zUp$LAAHIzDrihn63+YH?o%S*7{(qeH*6)!>XaNJ^e;%B)g%4mNEP{GM3JMB}zkb$# zCbB+XI8AUy{7_~Uzo2!B^@u62++O*4+080>O^?dr_nkhBd|#XF8lN4T9fQO^jINHC zC354RMtVi;ei6hQT9m-MhKxsVBhH_e_10%$1=JB%Lo(qra&E%|!n^R0@D8!>{L6OB z#|(u>gi*jGd=5N910Z0a?i8hv^>M;6f<5ALbOO>nx?^-|q-W`lvY1lOs?pWb zs#PCS@~3^+oK%+7Db5_%pSB}#O7!8+i`JH+e#2I=TsI^D*l`kXdK{ zF6)1U9!!Kf&=US92L~$^aS95G{}oxcl39OBX8jJ4^+m#^g0tdlGK-iVYoX1ImetIw z;#8|eFN8xx;l;g*uNO{FDoAlo&?k?J=SNpF-=~g_za5E3(qB#Cbu8xa0+6YwA2N{0 zdaT#StpD2P|H?WA1qB7gzcK3{W_-;0z2hIVjxvd?FR!f=go<~HP2x>7I&E&;UAT%J&xQJkB4fYB#OoHQn}N8D8Ay;N%wH}d@JFQR4_RP)Xj;K%|b4w*q@ zJw{7ry(^V<3JMB}|6tY^ky&3xX1y1Yb?;hV!9B6NxVLzDTtxh28beZrlp#jdq=u)e zkyS<25tXmg4QUZ6`sC-y@+5t#Ev<=ZjkdhL5LsThmp6#lR$PO0An!7b?L=n1((wrle zh0l3Lo`X0KX+z#+8teGUS+|nAiBL*v1|ae97FLu#~npQd78#Cm^z3Ew+7X)>|hq7vzM1y8>Vr@nhIU z`y%|DeUbeto@2jkKaHo_58035Ec;>mA^a_|594p_N9~7*ecgTq=h|=BZ{i$c-@tkQ zhFw7JYGU8P3+;F8d+}fFdm#{?0D)k~jbH~U0IBW|a@h(@i`fY8V$ZO0@}f6gc0y7MBpy?5Znb?(1U@H1n1#=oQJt$!!TFi z!3W^s5?qN(u=Ch;>^zWQ^Dz%>J|6UOfCTfy{4qbg46nw^u+3Ni;Z$rs;Sy{Op(Eym zIpUM>nS`rxUwk#z9_xvQy!={=d%piQ1=v98tLF7ts^CZ%PmPYOSaCuy(Q4)8=j4EbS&?NxHM6 z*#k3OGR`o@&_g*hS%Hkk1RvVuNUZPxPex?DLTvw(tlJ2=P)0~%>ut`p^%fV2<>Df~ zNBMNV2Y+qZB>rmthSFaAZT!Wh3;DbGKBa^BJNN;79se+Y58uYWz`xBmm0=Xu90wB!H^3&s3aBJ(;T61u7cdIOf*ZVrcf^qVjdObIP-c^(A?NqM zx+)>(1LP5YdSrk9Oa^d(f!kpaVGAZO6aFz)uoIpnXAU_DkVM!*4&)N!1)i`1Ccq>Z z4_xBUIncs6I7>);H3{Q@M%cnZI70X{Tp?7EGoAd(w-5ve2uFY$;U&0C2;~3jUf2yg zVLVKPagYc3ge{yU$DN$l5KBlx-3mwXFFdWk5M+xYMW;ls>7S>JNX9jz4Ldap@e%lX z>rPo;v_BHcS;tPx2+iD-Hk`hfGn6%)VN2LXbC2p#_#;nNfRkCD@wc+Bfo4J`6cCc2 zm(k_u#R6Xb#ey1nnDUsMQ6GVfuJ;f*AeY6FLMNoBY`*v=;w#lk*CL4YvP6OyrAv@X zRgyx)EO(pCSrxq!UxD9Y>gZggm)nX!hodaer-}sCLb?z zDeVlcn4i5!Ge0|Bl-Vm?1SHr-Y$vu6e~LfGp8}U05J$fD>UPL_?(QjxE}D2S~ov?RfO9?taX}?e}BKcUQQU?;cq{`lpfg@H^zE*ENbt`vDW-!*5CV6B1TV)~csWGC zW5SKFnNUTJnH&-v!G$;ifv}g51XtiFu7EwTo3I5dIsY$g-ySK&6-4BZ61M(06cqnV)*sZy3HTyc(Nd95T35#2)UPZLOv5b;K0X;h zwOg?g_0H**y_5AeYe3qdr?HvV8JOCi zEbUE~v?B8@(^tBw7P0xFZh~Q`QKJpe4L1a6>MYx=lXY9o1I<$1Fnzdjzjli$-8x9C z)EzJuXpS2Pny(pyk^aa4q$6h?w~Dh48;!BB(Z8LJvj|(Axo{<}z?DE^oNshBt{-!C z?Do;tUcS8zy@n%>Vm{&oB;5z~8s2?SOlYhjCKO0y{<3Q_f2;`OVMY1}`bqi+4X&zF z4X)4%qrj*&Rw$pTHYqcdeU#%BH^2qY#It}z`$lWfa%#M58fv`NiE6Pr5n5q3ny;95 z;|KBY@wpS-raYZE?QrN((cuvDA@d#cp{vsC6<4J|Vw2k}HaU>oy&aZx_qGE`Nn-Km zC5caDPYq9GE@=aPa!CUcN4HKtIl6g_^KkGO2P7Szbp6or$*)5r!+#x`a3!%O;mUVI z{4>8B(kZT6L8rJ|okOIzI_G_nXUqG-Q?{zuQ#NJ%^qMI{pVMBRelDHXYwD0GJ>z_6 zU1-pnP=`D0P>1e5y&}5%Ty?*?;HtY@;OI4Of%Rd+8TDa6qA%88(-))JXbPJBTP_XI z2qaDY8zqgJ>Yu9{tLGWE815VP0f~9AsmMH7aYA0NIDy~C*WmYo#Q4f6GG>^f%)`y? z@t61$JWTsUTdjQtN@xJ3Dcw|VN>}KVmlYZ%T{Tdd2_(jFV>`pOx+8Tj>(&{0CP!lh zkeD(}KbkU$Lx&^U8`gj?VGC-|P;Ycj@h{2xvs#8gA+(Ez2<_?a%=PJWZR>5lY}0|H z(O-F4zJ&cMJDsi1n40F4T*leTn!p^8;6a-jsV)rUA-uNY%1^ln(+o|7PvHe2i6}@k zQ#72z;(ngfr=hqhu4#v?K>AoZp${U=&YqB%eYJVud*WjU%dp9J!|$H+O=qvVa>=@?VW=ADxFx@({6@b zPpX<#g;dQN<2*imj5CnfTz%FITX%cSu>4~M9rKS3su}ibP)$m&)D7fOM)e@#kV#*-Z*Z9qt9qxcb`@MqTL()D}m(t&>J_d6)bmL+GVlu)af&mPu2;1 z>OK=3RNhtISC($hrd9a|*B)wra5a$l1#QXLqzRfBRK4%8Y?(|X3+|!o zt?D5I63?_%^`2>i@S!&c>65M`CnRMQJjxrKU$ZoKiE-hXU8}zz8^9Ia7B3KYKu@CI zNgw+?+dOZR#5Z|;gs*5v=ba%t9IZuGzO@iqmf62>R+@!|(m2@z zk+u(aBuCcCE3XOXH)Ox;ofaw_%pCK6GrIcMy37ek{L>w&AA}Q`E*}_*lUM6r&#S*0 ztLJQ%dRK(j_ipeDmoN?puhox1CN|H<*IR$J4HIn_|19p$J;7baJ%R1SVzHgS&w2}X zi`{HD0Lk_>+ed!e6tFL#KH%42$KVaYGZ!fqWi7l8Bok&%woEwJxw-4xPRfmkd;@%s z0*RN0XOHEM2UZu7_!ZFP>+dA25+S<-)``zqWqgY&c?48w-t3+S?s%ZC+ZctsAZPmhbgk zvwT7D`ipylH+C9f9gy6Z_e;+k^JW#yJvOT#?uUdoaX)NZ zxa01&h12%UESs;UOtaHthxMeGrWP2iB8@=3tuf6%d$CrgjLcK>FU+I=D(iFbfb zL6`T{?Qb(kHz1;aO!&BGFT%@#Yp~Q6ygI%Lz|tJ4D<;mGSg7W2Nw5-jT9f=v(fHqLDJ>eoFEe!k@x6B=oGmlqAl5 zFIkb3S}a9fU&zxvMW<`_if=dgy&RDdP~XU1UwlzfvynSfdi!x@qXk1=isk|`)0RJ5CAQXvjiha5MTz9TNwg;nOdw!pF zd^i3*z79y__vF9It_=ER@Z154z^?mN2d+IIbJ6{LW#8p}gZs=bpI++6|8+p^;B5mv zR?l18clC%V4pV1MR%fis9F=kDAm?DmALD$yeMfrVx!?c6=V1rNIE^_xW-yRAk8tVa zJmTf52>+L>B89IHxxAI^L^ zYvboxxwU!4d2`IU`t{>*PP-()y0{^gs4i|3=phzH8O8FKT^_2Va|9;Y46j+qth z9b-Cy1f(A{*1Y*a!(*}MOOJ&>x_VU4=&H%744vmE(z*_Gcy{^&DMBM+dW8Ne$?Em1 z5*RqMlo6~jR&G#upcIseR>K$(bP)OElmDdFeb{lF5`nwmk{~|kmmKS?$ue7|hU^#h zBDJYHwexC6Qs<(Q(?!hO1Npr<$20a8m=?I@WhXYob7C9n_3KgU6p_AY>mRB0dxCTP z2Ngx7^%dy@Ekg?i_lPG*c+x2{;eft8L8{UBRDNt=NVZ+RcJPWULGeZJ&i=*nWrIJ- z?B#X6J^fO-#)!+v&x*@^yJWM5ng*vUf0WntHOXt_ru{6rY-HL%wS4V}i7G?&JK8ha zs%%Ct$SJaRb;WRv@+b9Una_~5dM5I@dJ(!$SvSH|t7J(EHBI)OwRZN5x1x74-X zWO+f>DASS+Ks9^?6z5*oP?rTCCl`NRblb7SS?b85EonYVTa+%&K+~mW0p^ir<{i5` zW^{i0Fz11ZpS#5aGk=qy=AF%!&98xEV{G@q#<*s7jl5BX_zI5e$G+ojfMVA1nu9cEq*J`%bBEoDfjPuaV-}0ebWJ2`z$OIs% z2h_NF0I$dE@OsQ1^TzCf^ueIe_rpzluG40FF^~e?f*J!#1I`ES2)F>G7oE=@JfoPK zTZEXV*etbU+uTF!kW0vk=qv9|Mwd8tI%(N^2JH!=1ZvA8)qOd}FlSWQoxdlPP8aCewiAp6A)&o|n;=wLC)vq<(I% zxUbkJ+;@?;lVQBkkYR6oBkOH@I*?xHg_|?z;GMui@Fr7|ZJl*($Y{u6Xp2fb`t|5OQHDrQkWxIq@M>;T*7B^B zbf?_5ybrk{aW~>baTfKX)cy4m(KgX4k=DOo>(2!H_%RiZWveQ_mobJkl>2%kBn|y@ zSh~y>mU)}JEwL??`GWbBk-^kp;EcKLUoq<#7a72?W&GSefwhTY-d4g|%{a{j&f2yQ zj3oAYh92_}`yD%xpg?e4FpcNO{m`%Izu$ipJBEG4tVHWNri)gV z8I;W}Gr)|ood09C{?{brAYppMi9ipHKvU5O7}EhFA|%`%*Te0B1`S01XD)YBkz>pc zu$f3BG!yyj^&64!p2#6MP2~T^O8j9MAwt6Gcq2}SOvr)EPb=0x`BdwFS>PB-%+`Yi z-h!7|*4dM?T>GE)FYLdkm^Z|gm9s}VMJ$Jc_&izeqO3{j*{NE&ikwa9nz0>m?AX=R zNz{qd1)^Vx@$+yV?A73%G{947V;g|BbVY|MNUbnuE`sMY%*8W`kRkdH4R~f6^ zvD~ksqJmnHQ1PhpLWN7kqUr+`+7-6dhbz7*E2*p~*DGIB`JuY1y09v^X0Uc*Raxzp z+SRqH8d^2IuB%#9tyj0RI-|O`&a0YIbAw`7>r>-UJE_jE{%YNmdL7D(x=ECV`f!S0 zJ)d&8c7XDhGF11rej_EU&VtfZJG=UN?QQXh_*C}-T!eqX*sxY(@hNZEq2BbXJI zjaiMa^}k-vfK%}%ycWqt%8@K`4YM)ufem~_i2x$UcjE`~9f%U0iVWk9_+8u)i^dYM zXmlC68eNKu@lITdhM_Of0CWyI2~ps>_*wiskdOvMfHdIOa6=*vxEt<(R3L3gIgqf! z*iK9f=|>cZ5HUpD5Mz{qcA{-S!p>tiu=7YaGD>6%aunHubzuEi2WY}#&_pbeyND%> z8Gu0|B#cFT6$_n=EEIA8a$W8`}(HV&EN-nJ|aQIG7s$3{xW`=yYTRNJt3s z9(jh%!@k4jK|9cZgVNDTw2IibA5l$oCwdCq4I&VN5OctMFlR)9^dL;s3H3m2;3b4W zFf>3TG@)H6ALXD^h~v|VJ;MC4hj=2MgePLv7zHaQwn!W)gpEjv_#&?mUm%U$16Uze zg%!e>Rq!=@hq|I(s3CC_Fk}#>!51(UMqv;TRE7%C4s<2CZ@;2PQ7v!+XK;k^r@&7> z`A=%yn(fEQ732!|f{Lt@8Iv;$#BLI5_l&-I{b7A)Sw;*ly(%|7cWQ<_BQ$+Yl288Z z9H$g!Ecab$6pcEu{s48DxCC=si2nPv9w(U1kE_rupIfm&nV_s6UNLxSAhb^+nj$uq zVNEkxhh!6mzfl&-TYEY(vQ9K!*r#z@IP@Y$rj!sk=BSz{a$Zhp26+<1PbWr}TJOKbhz#NVmIWU~5TCTpS zTB2Aa-=tWiYE~apHIFPA**dZWn}i<4CMhJtKPn`OOl6EB6WxI2qZ_&>cRc8xOw{^h zvexteX04AwVQe5oNN5q-f))`K?1DS|)oVEaUl?0iPr{q{u$nnW1|G(C{KHG&8Do_bEgsaml(q^V8i4|hI zh5Ne0yK}m{TYjMLYTlfqk>#6ikhU>JnOK!A&*)5zjyoRnG}5Vlq~4o4QKTihAkzAy zwLZ0cM#a35sl$?CGwIHr1-+t>ptSv*qOj~-i7+*vUf#>dptsTEn#wu5c&)tp))#b6 zn@;`2#sF$_x4P?HcZOs_S5o&|>a+TNRG-EZw35ctogJN3ouc-6%!KxN^po^m^ppHx z!6^R)GrxV5S*i?Gij^ttI~k?zztMeKM(IAh0PZzjfYeA*EH$FDTAJxBK`_5i5X|1d z-^|`1&h6lf>-%Br&hvf$c;) zP(IoL=3qo*%n&(9s0y2ks!)IQHR=y1;4~2u#>5y{D+=f^+K&Zd!PpZZ;X^P1m!l4- z9cqEE!oR~cfrM3K4OkU1r~QVAK6VAuhaX@y{6Or1*T%|-@2!BB5D1S^Pt+DQ!{-u(KLN%xKqC=S?~dLly*uDK+yZ@=53^u8>WG@4 zH*pKx0KW!fg5ebeqesw7=n-rywinw9G-!qf=}TW}#R#)UB63RnXxFda-6(?Mw{6{QhdyodV(3Gc-bydSketxyx<;l35$092qs z6FwhbjL(N~2!SwsDyGB~-~q1SiLWK@jkV-kG9Z{WW`bQq-B2gg1uw#L@Q*MC2aFh< zAOSHJj)h>4QESv3wIJ_Je;^TiIgCqCdt%Gx#PL{(e+wk+BUXo%U>=w~W{#dj523%} zY@CI2frN+QuZd(6Tg$?Ga24K%UPCXTXK{Dj@h@*X@gB>-;;@z2B5V%Uj8$Vr%o z{_FjdT0g|L-~sglZ&~Jxq%#qj?V=>0&e5Q#!zZQMpQ;-N@F6Q_;jRi zQUvKIM-+z;~ELv~6XZXs(r2kH0fUD+QacJ2OBe4kQXcb`(j zGvaFUj2Ke3B}2-1&A7>U%}wS`r7!$Qiv#toEv z+%}$)+eRC1zCau1G_jv@np#e@9Bnz#LA6t^(pR@_3S0h7ZtfH7adTp|cL{@x4dYoMNp5ITvB#{e0T zGLR4%PYfIofdy=W%P^^t64qhV!Rs)BaTwYRHAB0ONy)8a0tpMi zBCvqJ>{~6A0SnmBhF9VBcr}nr+Rb%M+SNPM=hQo3ybn+WS7BtgmUgu^mOw&KbPj@!Kb4O7 zjUAk7h-C3s>58+TOBW!K>mY@RtDz zKY$;{55U-^wp7T46d02aA0PvIi6I^Td0YR|^9d%fi8KM7$iMbK1UTUT;D{4-{gfeXAa*wbr zxkq>}1w_Q}B_Mt;ej>Rmej=SET|=Z>Dv@^gE$Kbnw`6FS{LauWAa(ESjO*T4wysoA zwhnW^N->A=wLbmn9Z;Qwg9Q%@`vbx-*ct;`*Kwyr$;y=_kctbG#k_I=U8T6%qnWA zZ@kp_8IT5F%N7jQWNpYS%(NE$Ec6l#0jXtWOK{7)g0=bbf;A;8O1>`f0MZ#7-92Ya zog-ZOoTDU{BofIbAbGCxRC=z`O*vbn8^isLr^mHbSF5L}#0lTVwZv^@ENE4?x&aCE z!8|d0AekSvOg7(%`{ES5W?)~xP5<|C)v=E6JA~f}mh&@!q*N;TN^W9R@|DEcTB|z$ z+Lw?H7&HK>>Q+@?rDYTnWfxiO@xZIjg90RVg}PZ?2&5W=n#Ag-acyx+V_obAosK(f z1`<`&kWQ_SyB-g5=M=-r5as3?2jY_(UAFdZE7{sxu;W8S!A=0E1;{DL)65C|@J*rd zho9Bk)%j{m0B8pxkgl~}bJc6{vG+;v?vik&ucW(a&dp}DHyH=gB&mViV(t!3;?P9d z5!sCQtam;!(#{(l4jnUq)V!-@Uh{_hY5Df~)2ioH1y#=zo)%6Jo=SL=P?iumSUlJ_ zhyV#2CFam7Al0yI8*55aR;0$JEF1h~kUwY*;|!b_$QsbD-c-H6dTp9<+Lg4RuGd|b zT|+<`oHpn?xFxeG^GfEIolzZw9k(z8tQgB@e$BLD&LviSwW-RBaCl&%C)lbAL{TUDSgSe(~YbEJAx)m|Xs7jPZ?2>jRk zC$+wreS%{lEEe*Fy@_k%ed1zUw$h(8A7*S}_OyD{uCMj4d6Gd(pOtbc=~(jq1h))4 z!#>p~?oiB~C{aCHZ$w3il`z+Y>OWHJ8bqysS-z;^D`m2>g}AmJK9JbIi_yn{&dv(! z<{07cd@a#09Gq%IF`y>4l(e+B#PZvD2>(%g9p`R)>mWteJXk9Ym#&tEbA5RWxxQji zr>9uN+s^r(x1ERZFdo7>&hcO!Cw`dU8C$rq{7`P}h?cr@M2oq*{V;R4$WJs$Qc^?hlB>w(lgEPl{ET-seqFYU&Bu?o!h z@6Fcnz4$(SFOV``X1vOH6_fF9YE1NF&SQ&!Wk7O6J&fEq);`uX)~&u~Uih73VRzpy zdaDN{i|nPfn0c7AcS!&tgIR=^i}4nT2+R}7anRiwqMo#u6y0K_iSsdBNVR{0!7?#{4jO6 z((8=(eJ|E=gA)bE%4u7hLYv}h^lG=)oOIZ1Uu%ar_qoh;%C_I-7-N42#;M#^DXpAo zf7*VL-FB}iFL%#lMy4jCMwvj$yOyt!S9dk*`i-kXhjk8z>^Ii&DNpNK_ZJ@$?yt;w zkei<43Z&;vFDhSL*13He*ZDeJ86FY7493xqyA^$Niff<8HMfgY>xM&Ay}kGLZQA=G zYC)J`xB;V`k=Rz}ndp`1@&111y-MFo+XTDMtZ6_x{iE)ereo=XQ}OQ z%AwieY*19d+@QyIHN8vjtOgRY9Qhtu3Z&H1G{-cpODes0mnS*%o%@|6FwRGV;@Kay zkFGhEaP)b^rZ9;2sn4O(|y76>MXL}dY+2(T3ZIeqt zu}|^*VuvGl4xTu)uJBI5vBKGb4+0_sUojD8R{PgFU+FB-whws`(i@VWW{`FyjRxbi zn6|jJgr4y{b5o}$cv`S+@bo0@q~DTyf#hQ5a^3ONtzU237!2w>(tfPH8A(U-ktkiw zvyr;Df;=AY2`GJ-_i&}(E#FJ`!hHjPbYJWKIp3EVhUp2ZCq4Ii=DBs=cy#mnjn6NQ zUO+B<#JAv1_@2X+M;9NN6VUp|%fF)WTWUXb5s+|Q{1|=!{_FjdTHne(#jzHa2^)m! zM9p|g+)c(r#xjN`e*>q1cfWCR!)uBizVFlnV*5d7W8EYE>-ny-xHR>U8Ql zYNTk3Xq{;8AF1`ZM6J&$pH(qu*mu}x*tGZN!0TQ~>v6hK^OLscEvuV!+bmk2we__6 zwSL>G+d9%JZhO}FhPIMMZ`GvlrB_PVNEFiTJ-_r+^!!3Q*f^bbP*}vMj~%pjuO#tac|M51=9D> z0-HqiADTo}lPkqllY8Gv{CeL4sk=~opu4bCQrc1~!F-5X_xT&OJ`AIv!X6NR?+PUS zxB6}RW!495Mb=LaG#zr^zpIJYbiT3a`9Sd87yH`G7+2dK^KS9ha>G2UJd50)!Z`W) z9~R{I1u_CBJig_%z%#+~f$yJuvhK9jB~rw7nE#Z=j7M@HnccBGV-`0c8PM)mAKbQo z`<|uI@$cWo^!vZ^U-i&S)u&pfT4@{Yu->*3Nbj=WCB^*o;aUNb&kTutogC6oX;XEf zQX9reKa&xjzV($s=)zZffpoR;M(ovQAU(@^`Y2Uwy%kv5aS+2k310$;Rp0)UZ!7a_&Dw3>Y|yU-@kGRcA;q0 zO{<;K;nx}6IoT!Jt;OXjjPo?%$n|Iq#b5de(KZdz`z2 zt09aNc7hRB?h3sruU3a#4GF)v z^a6e^3&tt*D~c@idyx_R;zcTu%m*yKG>sUT-f!Hmaq!Ci8+#7_?<4N#KmVcCsOHGA z5$%_uA;B*T%=9dNHp}x|>3-Q`qEXK6pxes|EI&Lhs8W3~>ZMxp?#R2DF?bk+)xYWe zSXp$UFbBqw8Oi3$JQC){7sPjmuL_^>cEQIfg~^3KD!)=#%hCNodqa15$1IBd`rQqG zj=$c+kLuYf3)OLlZ=B{h92;_wMGRR3$)(-(z6-C?vwA{hsrU7}iQeHI$AlY&w);Qd zo43n4R-Pb>KlyaelPiJZ(LarhjO5zn+qv7c0l+aL<$w_B0RfRu{*_ukMqGpm5?&An z39qFbO!`xDD`zifALp{@D^a*`V>7*ZPt$^|^qj_w@{Ee~sfp@5{T!b(-Pp?b<*`-u zZ0cd^Jkf5^O3~&&QtRInwLZ73lBjhP6;bVd8ZygX+g9(k(~Nw!9h=U(%g+=Xs| zwQXO^FZ|=e-TWuaj`rK_EN(7W$bBxO58yIRx0Ym5w^lA)-n1! ztvl(R);w3PBhQtVL)$>hY3-&TZtVs@ngF@3l9eS0)Xl z*d-~b+v>~edz`j7-FBSn`kmV(XUuH5nZWEij8k@}>}J_E$4*Doq1Jo0ceiJqDc`Kh zYcX}9<9TX8TVpCyz+)9LmAazK}O`bu{O%{;e$im*fk;PiXCB#|)N&l#U zmHtr}r=+1|V=-=g#3=Lj)Wpd6io}eQp4x3EJz<>eE7>*KSNy;5kM*Acq-$9>)YsaO ziRxl?;@$n8cRVO@2C=QNmC5o%-<0b>ikup;J#ymd<2tpccMTjGcsg)@8$&BeYZ2X^ zj?xW!`g_**2qI@i3`Smgpy?CsV`+KR;0VilMPcEshGLzlxhDYueYDa$hQ)6Qis8=WvZe^levcEg3Yw19-o#ipYu zjBk?vdjF)>kFsqz&xD4;148{|-IVO)P{Czkwcu5^z0|SWn0bS-j6NrSY2LidzKn#7 z)#?0PPF{JgPTZ3?R;-x%mO7jInP`q^gXq^kQtL~ITAx`~Rxx*2OZDCGPyGjms`^>g z2Gx6N=2VO7KC9W@db_2)Ri{~#7S{YdCyHIec|y}|Os0KZBcd#;>FqY|+17nqTGyu| zt)o}8%%WGc)7q=sX|4NOpSA95_hO>$UJP#A5(c+@B4bbcMAkw^9cv)~Vg4C_Yn007 ztyBJ1t>bMt6|Vu3C{$P}JS93OnkGCBBvgv+$Nm?4=K&Pexj$?*u_tOQv0;t9m)Lv3 zXpG(1d#^;XBq(AbDt1MUU9i%mcb4uVMOYTL_dR=9*aj>ecF%c#|JTEemzmsq=f>~< zWxU^ap5Hw046ATZ@!`zQySzhWhooC2p~CB;j>3(Vb1TnRF0Wc&+h0jKK>E0bkZ}jdCFyvn1FUq|P4CD<`PoCB9)LZpy>-=hMbwSVFpOtz0tFtxk z>H=V(%BWO&O6Y))?!h}VZfBm$I8JBKU8s_m_g+kVep{F#xFKi*3}^}(gN6Y^?kWC^ z++Tpf;%bgCJH8nA?ESMbnicA~sw`kI4Kdy{Eb__px#y!wa7@^gFiD?X__IC&7&xOi zL*fQSXGZa&PDrHE=h9xlK;NSM=}W+nJS+K!q-{~-A_qpq3%d!+gns}->f97ErDdFO8m*ze!Mw$b)YU5miGfr|r&MH{0PG0o+T<@aRK&LoKhpi=K`2BHx`(|K^Hoqj*i(Gdp@9<-$Or5@sMH{OAs`G{)PUI-jr}G{$)a%ueZ+_pZf-X zgS3bS2Fq!4u(_|dk9WD3Z+e&Xfb^Nb!1u_T!@o?8q{dJ~fuSb9=2p$wXs5_C5vQwu zsA^m(0|x!Yf?%@dY4@jBpRDJ%tq-@qaay;z;hj-0``8nA(=CH?=hlDEZkmK-p4tTG`Ul(d1_7D7PtF z$!+8Vau4~SAX5K*L8Rf2!UDq|q*K8f(h0!64M6!+<*$6IKCa!RJ}!$_^^(QwUJxgA zFUkd0x?E7)v}8(gQ%iZtC`)d_*8j9vxWmSpKwAg1IhM3R-MEBpxMH;i-UzAjWzMu^Q&rOaHX*1AV+MFxQ0!*Jb=0 zsf9YM47AvKba)g{gQx0m-IcEZ9Y|paEQc=8@L#;)4y$L(Two@SnFe2_pLSj5&n||c zumZXQL&FoS>S=gh6(XPy_C1KIT=>X{ZRX3Ci^i`izOTGfkyS87w@~{eIW$oh()NAnzV~>Vj3-b0g z=Wga%@##oPUCpOtJ%H$=jY;eOHj&G#s;Zk~iQ@~(sYuICKi?i3(G;NhyyO*48KudQBCc&C>TYd5UA%3 z_(KqNo;pD}(|NRl=Ho25!6|Cyhr2Mkm?mfi)Kkd}1O18)reA%0aeofVq$W^6`Qo>LukzJ%X*U5mrz) zsH>C*-G=^(Zi_mjUZ^uP0F6N7(Fo`VgRn6W3o&#)zD_%13;<=+K zLLM{qJcm2*l=7l3QEvF!C*XHrK=H^A1yEz~*T_)R9rgOS^W6u33?E=%&)5CSSwE$G zq)H`j5w8fp$a|6dBPW)fw^CM*y0vu%TVK3aYF$BEa!7Kw_<@{r(RU-;r8*^cjqMQX z7u7S)U(b3p(M_AfefL(u{iWtc-F#|6^}+J~)ulq2P$9gd+^#lj zhRX959*Uu|meT9ea^-zxTjj%|M*7^MMhbzvgF;X&x4IR}O%E)uOb>F0^H1guC%lO; z!dvYuuTwiKyyT?9OIJ>;(3LBD%H}A0YLZnYnq***@02f>?=%-!I-3g$W)xtHRu&B_ zTB*+{_+6h-9AO?&96=l+mJ^4{Ta~+%w*m&^*M<$ouTzcqZr%7#vi|qb@aN2|rnk|n z?Cb0c?BmcM=mNR|^;F@h1Qr&S|39Kr-0tW)KUvhDV1^h(=L z1xEc_a#FH2`E|U1d{UfqABB@FTOm;ft1FcO8YQt%qtdol4biqQ2rhtv;G#^@T9j$pXY?@b z0|tdaF&*0)f7@s+8ztE-8>P6YFe)wzRtT30Rv237-x^w&SDLq*SJo(Mw$&(r!MMoq z*tqClJ_*CZ!ou=7Wc`ZLPnAJzCUz26qC=yXN4iv zd5H((hD3HuA(Mv2-U{6xG%cVNbLd?Rll29EJ?nToG0wV6+H&p*>Py>6>dWfgmAk7a zs|z)g6%mDN3SSn^Gfy&aH6JjvHTEccYixlJYIiW8 zyB|ul3D7;warFt!3H^LSuztRJiKX0f47}V<&TJ<{RVbyHq;lew5 zP2rsae|z&ds=|?D$epKXMFrdi&?WVVF-fp6u z={VY%%{mJU%V(E$FPwD)(UcfKG>%ip4(0T-+3g+dZGpk^%yh@lJo#zL#pHqUbK|x| z)TAy-a*J(&&obTM8vBQ>a zK7|MGez$Z|59elMuJxXx60hq{>;;{^G?LOt5=&x*53JP#q8k@SG&(T zw8LXn*AoXacLN4>mkZ>_6)lS@i`EvE7sr)|isQ69v@5hbRIcj1DpvqF4ImmKoFf_{ zcFwOBJM+3^x94@qJISlcJNZ8G{mS==xodgXa@VH0r_D@r*E{G%dI$Ry`xW~XqC2^S z=#EcE8i7wo+Dxa=o1vZrNW!wPu&{g{M^r~LM^t~OHFHMCZsc6DpQ=&R-3LIm07b2e z&g&m1pGh*rOX4rbwv2q5nv=kfDGhZAyzkdK*OPZT2kCOjZ^?kh4Vs&NV! zJU!RT_we*zU*>Pm-4^Dw;jkq99aI2Dy;@nV`q5BhC^L2`>{>Lluq$zo3?uHT`Y5NX z`cSFVF)CGFE$bq$CacNeWVNiLWQ44vdc6Fhdc4ps?jp1+E+`xn7kHEN+VLjq!V6mJ z!mBS;r&nJh^T{`4KKI_+!Q6ZFR(cn`_2b=oJbSm!!ou>IWZhF4sCrM#{jljHc7M$L zsEo>aRr||b>=#hux@QHq^v?=HQ`RPJh&>or9IXwH;Mh46V-E+#1SI)db24+pyhh|) za`vZXUCCtKz};tGPJd^gY-?6iW?Q*GXWO!^n-{zOuwZ=CCAXJvAGqKr)aAm`+b(y% z+PGuyS=4|1uZkHXcduM}?DZq=M(5&))fasn1&@bbO^ohIwS*i1Su@!HiGyyBZj{bb zZd2Ws+ti)a^VFR+k?LET$eKO2jG8_CwfqtMwSp>fu%JrDlirr`L?Ob3q7cyyp+$5< zHAd;68k4q#>zB4gQ>411DYEo2e`D#Bmi?}8T6Uf_-!sojUqVjwrH?z)bFnNeEG(bL z=}0eBDMTQ#hX{?JqTC}iWoIkYWf8XPw$1jg`lDnzaXh780xvo-N*1*yyl1QfXK3sS z|2RLZuO#OgZ#3@|6I%af$1MGjTogPSowl>t#zyGpv=>x2q;p7&S{YY05VVKAhiT?2(-# z`h^C^SfgKs4fYN4AM4kf*Pb_v*OVMf4*c}2@8VjxC+v6W_V(ZFlB(ZS?{NIWW1{-< z!p?(ojxE@`xKX18)S|rB1DC9vdTXe3%aO;=h7cZeB>BxazL)bOk1vmFzwy@NMj2z4 zc%DC`<^Y3ukXRxXNcKoPB%ZqQL^IuZiIZfz#EGxYYs^bW9SR#f9jcgL}L*!;-o;k&|(7M8UxOAh^L3UbtA$C>F(#W5~ z%fr46`XP2p^y-jRzU97MUp3)1%5mY{Cnu5{$PS;L^sX{qfJAaZ0LA=_Ki))c6mp}7mq*Sx?Ywua^A0IRl&w~ ze;m>0!v|#2Txq)OabcuBtWZOAQ&(YQ)IHTP!U(~8VMHmv_(mz;xW>5IxF-Evh9do( zuByOGS5+;q9#bt>HB*GDnk6nv?4GzRyklvGSzC+hc57Yhn#0vgSKPkaLbrBY zKlcY3H1*W)50!d+uC&{>QBy(naB`jl?o#m@1Zwh)$ z6nX)JfG^xDe5%^3;Ho?oJ>@eLJ@b0zAHxO+Jn{p~JuH*VJ#+-|L`S5`x!$Sr!e#j7 zPRq2xYOXdoX-OiTv?TCkuqN=NdYIBrJq$Y0uFwhc0I)uI4n)o`=s;m=*K{A9|+4B1tFFLd)u;#4Oj*DnZY%01uF$+! zS7~asj+!K`qrfwtFYwHh=Do_37H_aTDc&GnD|;zk`#vkD_I(!7Qj3U|iR%+e64!@x z4$TeetZt|3qHae^ZK<>r>IsDBSQZu*md`)y)08V!Cv`r$L|ts)wE))uLTrIMXydOZ3p*XzUH^# zSlsI$$dX@Iy?I&c|Ld556~()La|@hS(&q3)zpWPgjpX>YW%b$JYwBl`nWD^Z%n!|3 z24|zt;7pQ*H%L;NA!#qoD4t>3QanQvBJz}kh;Ivd;@b%$5~n1Lh*%dDAF+~B3f5O*6)Iz*k^o^XWn4{?Mesy+k zmL&Yg@D;(eA&x-@17^qk645ur6CZjv;Ke9j6W$?SJ~@GOAe(;3dJ`t=&HicDOPP5L zFR%<+A1$dp&%K$}ByEU&1HIC2tr}W2u4*_+rOh?J1E!`Olz`bb8va2Yv)bTaVTEce z1FE85qbhu;S558ND!0l$Rc_Y()`Qml=8lGg=8mc{>RGBWoMQ=-ILBh|aaPCP)2}Ql z)2~E_X&X8W1^{dnD6s&DeGZ+4h2=BO`V1!P%b2W>VxENAq`Is-tUIgo2nq{49q6LG zr21ZYoZLp1>y}EMi@xA*jlLHBRhT(EDD>f**w{PK`p}tiSk}^nKgcG+WE&W|e{qYI5i#6A6U226jHv8DxBlb!5 zCAE|6>9#d>-nMGn9Qu+y8ItHO_9{@)!|gthPmiU~frM^_o`8ssMjX(>2g*SMYA8fD zv)V*DFT+`PRGv`bqj7cNx|cyq0~f#9t+*+VP*f84 zw1n*Y{`ErK8{L)ms~(=zAG_;hjR(Ui?eZ&JD@W+gRFlyl^X297j-Y0{DS zrSXnQ)_BMGmPw-cmYkhQa?Vc9u_P(y7{@t@YS_NDFlB;J}VAKqNDuO^)wjI-|l2b1;jf6n?xDxer! z&q`DdODhjlDl1*h26Mi-#(dtg-h9@aX<25@GDlnbo1@JUmgeRNbE@TQbE?^3`3nEO z0=yQO^_Esp*7h|8XtCj}(~|_`MY7M9Wa@Aj=5LAoCr|LGztrkCGF`9`<&& zrS^6JP=T#axf)FD3yWA-SpG}0KIk7=Pk+l~z0-%R>&kG}dr1eWwvb)aVsfT$>gzVX zYk0x$hh&||AI^8mU7B{8J1_Y~@Qc6+0rCEC{2u#E3GEtm?#)#1aPN*!!*Vuep3H7d z?$8V%IUlkf#$^5Hf0OlwCl;1KIri`0{oU|dDNuveKmmJ)&ced-UzPR24YICbvOa>j zTkk{sAuo~+R1GfpRwFN%;obUaw%6vQ2}u(Z_}rFhj%hPFSK{Jg$Gw^F%X=O4`u)qP zFS@^Z>gVo5-{;@2xi>01G4q@EIpj@^8=3PV>w!$xm;RfqBWCI;h63zc@W)<*E7)^z z5qlmyu)l)`_J3=w|Cs0C67~)}!@hxZY%Q3emVIFn3k%DCb=Kuf)`u`z?~1ejP$E!j z3L6tCh28H6Z=bz$z}MH$%ZDFdnB;QofY%dVe)a5Vz$O2DpM~y} zd)nz@?w*vlY5nwJ#7h17|19f&#tt<0?;ijfSXfwC{$I>GEgh|zQLs`wykK9LBy@O~ z+ljzmPaV(leC(NjBir-lE&I)T*I!;6dv(V3n5*_nx7>7Yy-wDi@^`6l9Vc?)Ulm;? zj;ef!-Tyl4EG#T6EPrFx6){qay0NyI!lbFq>X+F)YiD{;3NQV)yR&YsyJfzh`F+NP zaF4d%vv02-Bm&<)$6RD1C;hhv{D7I8!lSkvWnd zAv&JdROCwhpxQ#r`zu+efX3G23TLoAp+9y3{DhqebFs6)5&Jo;vtb@~D$K+VhSAs} zX6iwJ*k|NfSXe%*tVb|e=PM(%{4@pECrzn1D@s?KQZ0~OP>ojhl6F$|RF0KSP>xqT zkwhx4X+ROIDaoCY{ycY%xJ%w`ac9CoHJNZ=@76z+UeWEv7ZACKhJQ8#411u3icZ+3(J3Ww=Ph|Xa#Ax+=pqo%5h>B%(|3Wr{VRyc$VnA$V2?S$U}5lWW>6POxX1z6Lz!6gk2*l!bp#n$k10qQK0PR4IXb03I1`+npP|P%d9zc(!2T;CLH029w&|>ScG4r|6@bf1= z4CO*yq+HM=bRRte20HD-P&D0&qJaS=p%j!vJ)k_P$JBIc2sH%tK%G%%sHcpX|8r<& z>d|}{Y9Dn1+kxtX^`Tx-J}?IyvGvq4!@|O5{r^hVtDqYDB{at}By~tqBz1`2nJ_MX zr#MkEQk=W zC)_3UlB5g0MExZhqW+?9@%pW3wnQnKE%cOl3q576Wp8Dz{d)Qx^y_JDV|B8&fqJIF z4D7$h!VF@j;Uf(6IC>^M4!T1R?2j-Xend{_Fmi%8;6NOTMq$`(XdT)HSD0ZykI{4N zWHcL1K}+z=#}S(aGoYRwup4%u0caTZBD#bw!b^CKt!EJQgFa{n+Jm;zvU%#-kV%5B2zhKlq~AXaSlH49Ejr zM;`DaEW+M~yKo!zMuV^`KD;;RfrLVs2h(8^8i!{XzFro32lY6?VQ@mL(JE{Ria;Tt zgFgf)>v42JkQmiLj zhdaQK(LM8W#;SzAiB}WG<2Wb61z;GSG5V+B84-%85fKVtpa9?h0?ytm35tq~9d z-b}xH-`qQQ`Pu7-u6C)%mi2WT3L`)cI3F@S;Qa8o*Vc!}89mHsMvtn_Y*keq7z8J9 zz{$Aegz5291snM+!4W`MUs&a=_7vZ*<5ReRh-1a&;+&ZNn2WKSLu*5LA)Sm`#(c(B zK&WIYfqwo0CFnTQ#|Z}XqBFo0tiY7CBwa`gKr|<8yrMbb6&7~)6$S{=Lk^|4p41}O zC)RwPn^+Ta_SM^vvx)OQy-l1e+alAK{h#dmQ>Juw2l#+1J!MJ^$}j*!mkkn~0TB=m z;ZzPC`|Q7@{575Obn161{ck)i@;9z80~BE*z3H%!-e3XX3C}1ll}wevfHx3GZ#Y#C z!(o6Fdg*=l-M>NBm0<)uBuS8zpCrgL$bFS(z#Gro${SzTQ`cG7!@0t-p?8{N$T`P} zC1kqv11hGW4NGuV1 zix!H##X&-rI7s|eG?ZSqaFnRKdq>yq?j3KoM!tHpRcJ5d3hiOQTv$kNFcb)?h$^6p z0O`{0TGwT`AaUVO3w{_a9IG_?J$p=DJ$pJJb`GbF+1i?P}{Td>+0IpXXcq{JC!} ze1j}VgF!a@v|+|CKUzmx_urlX$j-4lhV7hU?0?$>LjJx@6zwl@1ih&M3};Scd>_` zyLg+ycK>Y#W+@h~W+{MRDVQym!ZKs6W|_4WwTat`U=Z;F5h2#iv&u8e^r`ISB|iL4#%^8 zGCQP!#ZhZX=Kj!F%c1H$TNeGUSMa52QLJlL4cq@~Rf%bRC~pP7l|Mvqt!*{0hM(HD zS+GDjwXIV)PBKGyK{7+KLZmENA+Z%}NNgqc;!=sdL_wTOk0F^OW{9^)R*AQW_e$o9 z_lnO-u8Yr#Pj~JUpBCTiyehsY^6y}X{JZ^nM|S(YS^HY{X01?1$Q0_pfZ4Es-XD

7W7b?fU}9`d|)Ju(ACb^0uU>Kwm$FP`}+9yLAEe{|_B zS5wIv<_{5u=O|~WHTus*`yA6nA`(MmddwIrMksfT8J zvpX zM{T0k;iGU>eAKx5NwwqZS+13PSPzU3n4C6Ve{sh}_C;Gj3=^-;z8reE^hn~7MXP!> z!d5k9yJnYWo7M+4BsYv+8nYyN(MrqHmQ&2?-xq%T@ZlFg%!19;%#seb>Ww>GtG!P< zMtfIDbIGd`7K>TgR;fHPOYyZr8X#(0e-^5pdEpVT?!|+2?F@@_?IDOw%$*fB{``-; zvw|7NB>MA?tUX?PB1M1mJI;Hhn0tWSvbx1I*``-_IOXsP4aU0b8tEU;$4&fr9uQKW zJW1-qci;V6W<4vnKQD{t$K&w)*!FBSwmpYYf1bl&n>G$*S2bHUe`-=?4`pX@o&1J> zy2oB(QyYGmWqxb^OEzQC(92Nyj0wrU_`SkeW zR(ie?uGm+6y<@WYx>&vAvshjHq3f>rL+8P+{hbFpcXw^?+}*jSYhC9a`mKb1d!c(& z=Y@{cF0+o*UOg%1xQSo&{ue>Dou?Q-_r*+#XtTa?bIk$5#Tfd%Je)_Apb9xsU0pS^-CJ3#w#-K7Hb=W?w`A8aK(3cvtGT*Qx8R_L?p#3dbND6v6d>3q z?1FTng`U|`vymQU9<3hhX64NdnKj0v!b8jLVA;0vJ7v-1jVJ#&eiI;>6Vj8@?kX=E z=RWovAZo!Y71XT#zXo3OPX&b9N$sO{_Eq(D_EpVRoL4zpY2VTPi}vkb_~Rnyg*ysU z3lA0Y1zG$M!TAvZBabSu?@V{#-?2Ywa_Y;;^PeuoSARO(bfale)2Bts^GfD98Qeed z*dUO(tg?ye(3{oc((_0mS)q7{qj0_Gk+AB-oRb0r%CP8ilHtdec3vv?)ELXrmP)e# z(J21KRYMwF6mljwFNOJacnVY1U{;x`0U+-L(VFiHC#$FgtBi+1T6YLG2tcWJ^lqgw z7M1jC05_a}cyYpc=U38@ZLj(OA*zXdq8h&Y?%yu!=jg1b=8efqI9u4G z>cg5E>cg7nH|=lvO|(ED;d4H3EbwkfwhfQSW4PLDM!aCwnZFCkVx71`ybv){Z*~Tx zvL-X&2`=a!j)U#)W)KmHp8zX%vec1b!etWOCNP17|-@8oe-)Gz(-De!B8JQEQDRdRS z61x7qtWz_nnbb%?8gd%4IL}l!s#Z?3vS&FKJIwL*c>cvVd7^02+6l#l>+|2|-I;k{ z=G~c@7pGi0ba9-<%eA*Oie6TRh+c{vqz>ooef9?)n5G--5_5OEv(bj|jSDtp0x~&O zWyIvvFpaS7VH%MOBh4ZgPRyRXd}1~r6BQ$EdBFW-A|p#=6t^j!4?G@d7kDgTSHhu$T}l&_ zmMKjDq@=H?uT*E@=!I+M)kXgH`a*a-cN%v&*G=|SW+F?`h+Si@!Ek-+D!k__xYF7t zu$1IJyUem=7k9};}$>YDQi!$ zYxQfv!5*icuM&5$Lgx<2rmmOW%3UvemUoZoSuWM?xhB;tso-W;UrenF8V=d&u#SRjjM%s#wP?Vc9cFJcfHV zx$^+Yc$(If=4-EIFS`?GQDlAIa>?5n(S}idfMkSa#%G9aw%z&7X063Gs{o6gId!=O zIdwb%Uymnno_2SY^E4QQdPY5`o&i#%UA(PG+t$ub$JUO!m|MYJ41+K+W*xK5<9D|+ z?v)Oc>^Izbo9vT3^z#frKDfnxh>_mWuzO*<+bqp;qZyWWC4WoaVQwi;i(Brz_wE^I z9dM>YW(i2b!%w*h1-G}`p1%EywUW(Y>oKK*ve;4q444E{VG?OW-Xm?C^j(ad4s-o^ zdfeA7t=#!7{*DIDR~)yAqs95+?D|B`^7;!d%bnLcp1tW{rgbwp#~^n>&QAUV!9;$t zy|tr)U8whcZ}lfS)$^)*sx1Mj8&juP_rkH({@WedtJsiNFS}qY=z%$zM&2Q&qO6ZaSwDla{uX6D9%X$h%KBTB^|SJ$yIT^yi+ry<@xlv31hu-c!1FIZy>2WM>y}QE zD$*M%tDm#T;?L8pd)q8;0kCVjy zsJck?sJiwR;kWjdKVF33Uf5pp4#spO@PVeuhSL zgy;;YL1(}y7)@{B#Z3c9bpC(C(HY@~k)PkY~N#Q=avDACz@m zp7kbAdDcs}q1$>kx~*5DtQT)aSugk#S-+LEGwD{oO5Tfn6;VK2vM7Mt-J;L!=2h?n zyoweX|0%anC=-2bU0;$?Neb@Yz7W>NbhkJjLeeSk3d&~!3uT$!q)ny5M$gnkYa%0#wWHea>`KaC&3catB<7%~tLqK3#LGU%r=#Pwv&GMS7^ zwvZyS1%|^Y7>@6vzhoB-LWB@8L`eUaerEp{duE^k6Vwcn-e(zWpEjePJ8y zgl$wA#iB~_Q}_-1B%Fp*aE5wJ*;5bkk9ZXBhb!PS@R5`PHHK1vxv&`K<0|+hN?-wtuqE6GTl&A=z)#57vThlh`i(k9 z&zB0Md~pZ-DeeG+kSXLx@)bS{UxTX>U;p}ZL0MCVloeitXX0`AWco*`LfVn{$vZGW z0Il@?mAcSDsJGMy>Mdx(MtXx#ZIp;=BiE7}$+eIHS@aOfgtDYez!~n+8$h7*-M=8~ z%_rqq&oz=~y;fPC^(s|))@$tKSk2SG?TGT7UV4ZzALJee9ij%>$Wgp9$uP zt;a$zz&L$Mmwy5Qn&FRsM`QCCIA%3!k_>e zWwFo*AbSS@r0g4zP$!!WbpTW%K$OCp$59HxNkS9hq`#N-zxNJJy?Y;S!ejkv(Ac_XFa`1p7n|a@~krxHMD@_No?uBma)St-u(Zt@Qqxo8ehWT3JFmabS zjBCpa<=P5QwiO9aN`i&il4eP(BvSH2yPqgW{PZ#RqhsCMn?6BqjIXAN!3$WX=gpp# zG1~oWKbtfSBhTX>`uuxm^smR}V+r_dEU7OCU)7f*E5Uz~mB2C?AC`fb{+yUjb}*0t zG9#ejM^Sippx53^0~~6IxljYu#A>L9csv2(0l)zuO7V?Rl%jB=&{{b0A7p(nE;@hs zg9}nbN=Xs%k_e`^iclj~!9LhUZ$Jk+2(gziAok)pcom)l3qXw?LY^W|kjIHb#BO2} zsX&e;M*>16P&rg09*w`jL(x4k3J`J^d63*iSQ8fXis%FsQBwK&-%p}Re><9#0de?oCaiexQcK07q~JN8AE`fLjoLL82`?1rTcaSh7>Argp8g8sS3HSpPV^n&QEqBN;hWHkAS zj3$N=6Nq7e5GurCLIoG&6fTCnaDd)`OYj>UqQCw3ly{2W??Y4SIN$1baXtYeYmk-6 zzEXOWE~QOXQ6*F%?u*~U?WpzC&vaf9@)vRkxsEU)Ea-6v7QvxZsEL#co{fLQv#GE0 z0|Oe-K}Z#HKBV<}R0#Ek3i+pA`Xm7{v_mkw09P15qASJVU$%3}ax#~`(EN%o zp%&6VvW0-q?=h<6IN}PSPw0@zj9zvYS)1~xpQ5ob>}ZIh-jMVv*`Eu z(1a85E&b;G(fx*iG&VJzZ*0PHFcFpm2ws4f<9URLXdo(~9mF7l0XRC}{by#~3uWC& zp7p9B@~ktD%d=j2SDy6@4V3i;dDdA@DC^xQ>oR%PnWs_KJLOr=el5>>(NcNVt2PW| z{WiL-5B(EaS56v{q+HZeIJ2llyt>0etif++wc%Gw0)#9{spzTrSFweV6t5AV?*4#{ z=wL<=X?Gi1j7k3zhWd@R;2Eql=ZAZLE|w;oE>z?NQb_>)BYMmFmHIXiX);aH5I|N9 z0DUq)kU<|l0s5c}*Ml-BCized9h3@6=TG!-Gy*Vs14wl0pphO8-PWh0+q(NdysgJT z9KBy*AwA@&zmMiq|655Hg>vA@hL=RrChkj)_L1 zg7^kV!rHjFkH!wscEz?AT20zEwAKMa1<=V2fd9C`AICHq(&)uNJiP(M=peWWeiJu| zSRIxZu^Mm3tMGO}m^sW@%$(o7QX+o$!shpW!RFtebpONsNmlW<=UK%UEH021ECz&9 zqNY$v|4dTxBluPP2q13h9*J)0$+jsW$+j?vbG4(M{mfXqn1*);b4KN!%rTWf$4W`7 zeX?VNeR9PYhD}8>AXF(;MwP&K|B9?XL0PvzxAiKN^>rxgr%~4DqpW{LSwD%gUW~HN zL|I>tvR;U??u4>F0%d*j|7+HVCDtbmD={w&EwSo|=ps8F3bk8%glXbdu~agx>q*xv zNeK3#j7Kgy0mFnAJ1@C^Xgqv-X6TnGGBJIEE&qoA8!KL9-`kn~v4NDqk`L1%r$Kgc@D zH04EkQ(k~H<1NdZ@y%zp`ffhssO@;jQ5%qA{gMU6$KE-=JN)iWh$!Uas}ViRy4gME zfKVk=8C4EQ%$(TLm=Ujki|l@Vsbg*DkPat6GLL5TXS9Wm3+aAIq-_3uWQrXi?c;@u z+MU8qhgODOXT4;;XD$FF!y$7`#?{yEuQgsf#Jr8U98(9#-e2~e-K%z`+vuk&SYuL? zZetQ4=1(mTm_OBCu-jOBT3L0)$nsg(oZdyfKRzvgqVw1y&mvbV=N%yAP4Y6S56BJ6 zo8>nyK{C*Ii@7Q-nNLo5m3j?jmNN1fy8-Fh*kjN$Gr&2>Dd57FlgTMxyoo0Smxu!- zPAAUsRW%Xj0PlIEk{3l zicg_mwogX;aiL24`?QMminOW~OIJQyu_Q7mYI$TZAa8Qsdc28>Du_NG#p`bCaqQ86 zL5|!%QmHq@(%I^Yc>w2Wy$Po=^=O)R>Vx2bz|4SMl|d{iYfo#KAWmT6HT>ZXPv7d4 zs@$prfTT5MD5T{@TzNe-!Z5)tpv*cCs93ZK; z(sEOe2aXDA2pq|K%0I_zg+V;{9vL2jFWls+FD-yL)j7X(>dURjY0tgl{`38$`w6k! z4~p-X)Ui2Qbw%kvr1z&Q0#dZ4cyG}P-|FY(z6EubY{R-DKvKxB#=jp8{}8t8l`zLC zd-1oGVo!;O-DtlUPzh6fG(dVmrHsU<+jncWEq zVR+wM5tPkZeYM(uEYtiF_#7+}To~?ipkk8af;UsE-4hNJ=gS_Zn+Bd>Zrm&w{bG^((az zkhhj^AHB7Xc8q35n|XfoboT_xfYPTP+g-NvwA*cGWp~=H;#v9QRZkpzZhAL+^Z#;h zQpa@w2}}z*6{No3^g#0d*}ji_7x-ukm$#P-uU(&W>AGgmZ*e_~PBz9=$!g05N`Q z{L@w8#idtU_0epoS@*ZNtd4I=*2`>~ ztc^TJJ!E)bk_(czlJkH(o&9X+(=|pN#^;Pg!cD?>;W?)Ymn}}y{CB>nd@eG5X!`2r z{hDvJ<7*6I5RVNjZ{IoSba0 zZ^yG!e|-OOnAhVgT%$9Fs(^fXn|$F**O`)Y!_R=xN}~ye-kWWh~E*vUcX}9+yg)99`jT8ed$BMAjW5JsvBoNn(gZ(f@Uc)ZzY7TAMA#EHWE2@j#sZSHG>esG zcyaZmBNuO-@;LS8kmu$bbV_d|&&@Ev#E!xdqP2~>T9lGoTUN=O%k8n6* zu7CH4M)vg_m+U)$a8GiJxcYnM?0diGefi0X1?5&A-48h)qAaCvqq9aoQu1nkNYqC) z7}huXMfj?E7oIUc?|Np1_T`;hcI->9OxH`F3J5ul978I?cmHFv{vk0m@k2>RiDK#5 zKBhEU>cHF8sxQ3uw)B6ocOOtuo%`O$)nkb*u|>TmF)=nYb|cZms2DZI8cQ@bELgAt zf`|%=6-5P60l_XE4WNRffHdg>GXpaNLzx-sP^JOXXZG`dpZ&~wb6D$?dy;$aNjdxb ztlun>ZuH<2Qa?^95ogIw5A=Pg%G0`guo+X5j=tu#u0D=0KLSl_rO0g?eX9I$W#DA5TX%i z7#aX1+g`SFZ9m_uJ|Nh)*ygkOS#$RneDKyaU*Z2eP!ce(tVtLu>@1!j{!nza;75uOy!zaenaGj|!$1PA_o1^3d1e$_*eLlskqT8m>H} z&?{~%3157F;l1aY7q$r#9$4K!c5j902jeYcw97hIo{KY(!Y72ggbI@9CQnPGf_epK z-V$ez&Th-9g$_M!eBS%%ouHE2tAbpBv~9qShHc*h>1N*>-TcP9I{I=2s|$4KNUB@^ zBUuffGM=w`(t^A_mhQBm$xPM$U74`L;P`e?zEiDr-P+pN<4Nv{dLLh8>=%B!z_{(V2 z#lBvCUcMfF9+@ZZL5F-F_&NCc&hgNq332xcNY~!{LLY zfOMqM<=Elg5|&sY{^8gO_kPECKnh(MrU+g61Y*wq)Eh_#Hy@1MZw(zP$Sh1KT<*E@ zRJ%u5&T<;90|_7lRqdo&#W}Zn@>2p z9m+i@F)lMcG2Vbl@F{!@N5KPrsP(9uR5$3Tm?{cc65LZsN? zdc{rTdK*Z3SG|*d(Y{mm`*#HZ$(wVf-8(D2cSgVT-eAcd_JRF@9}m4cFb_z(5wTEEe0HJ(Tmg=^Nu1xxAk2N57oJtX6RgW zex@&Ue%fH8KpSi}p%2Wa=!~eI(HSkxGQXDQf5bZdV?^pakaC@JQghxp>3_1$faxvwHlrQ{O-#Bz#eA2@+^`w32%F@Qt9qFCHJ(7aD!Qb;6dZUe zuxmhYsQA%~FncPSx=vN?`FQW^J^uxyhrJ$|AI^AoHvYl$nF$5YUq4^5d(18?yZ35F z)c&Q$5lH#_@~-Di+`wI5{6jhSU~v)m_3gmhJA+80~wb*&M^L3|w(r=`((yh>; z-4%OR?|y6aGz~U#yi8s}7w)(u|y9f#}qRihHf|Cg^2FKt zpw{usp`&u4yr;a2?OWS{1ehPg!SUz)T$f7t{bPd`Ohvi(Qf>@9QRoS#mOeSLrH zy_mZ>r>A*tKJ~;ihWi8dw~UVFzi~nr*zj`Al<+H(CPl|sJzFcqH=(= zYm>dxu3YF)UYFb-a+>#A+uzwU{Jzay$NR3U?boKPW&;WTAbXSfTOeh#a$B<}Yzf=) zbW>0B0P_>`ejr7351SLd*v--HzT3HL^RF+t_QiA8_|oV1n@!Ezo7Zg(-P&i%w^7*< z%~3^5t}ffLL@ysA&ytVW_wIqa``(2PdFOlQc^6+wxs>De{M*mJTfazl%>I zesmb^-xhCYyDc6_evvmk{US5FXZ6VJe({pG(?z$6LzR~+a-VOGy%rmC>Fh;Uud^X- zkKPMCFL4%&CE-9qXORcG08D>c)?--rSqYrpMHJ^NeVc*Ol|`ehv$b%OmvE@6{o1Yg z;UeLM`X@o6I?s@(UZvNMzY5vLD|T9acfDYl{n+40?&KXiZtN}icDJiPzL(RXE%)Vo zdyiw`BT8E?kBB)V7W>6N9aQO&dXQ`1yaPz)0K+jV&v3^y-*887XAIQaY5N;Jwf(iT z40`P>?HV1@uF*>L>jt*VT7ZPKFdsgtcc}}ld%tFFO=Qhv{TTgP{S>`GFVVAWC3WA_7J#Jz zUMsrNH3T($ z1eRVwIHp9JJvHko&z(Psw-o~61Js)Ap0p`ZUOibdSu6$;%0Nko1tjqdF-trSNNwj@ zHLW+3jwCL6)ztJs<5$uaAgOz{UvBeH-j}>S*@+v+<#59lHx#7`45>MKdc1mz zdc1j}d9Hb4H6Q=ffe#&0Ml0V_L?l_g(!RPa>(=r`vkXY0RU*D<0g&)@Q=4Uj&UX&chQD*IP%uV^vVn>(9o zB_0x!!~;6io}-@Eu6z^r=ADe${NcPRUX@y)nWAn0OI09(Dv?fnUZew3qp8WZ(bRsV z{c-yd>8H|5(&4(Gt^0C)~T_<6o5tvqLaZoeE)^a8b^J79?giSPoL{10sJ7CJQ>!MC6mrCw7 z#sUCkKB_J-WN7r-b%r(Cby}5f5nh(oSC^%GPt!~Ho>rlLtW{{U)T!DmZHBr`o1tZ? z$7osl$$F!Ha^&vF+{oQBciAJEdq>v)o9m7n$e9FF@jT!Z-miPH1_BBY8iGDSgMjoV zAv5PqRA5oSf}86a%Ii1Oi+;X)qKQfeOFGcw{m-Mw`3~0LSvn2p;UqkU7>N2i*YHT8 z5pN}Iz_YX)4uBoxk_#C-zE2~V@GQAtat}iiu;JA|{4<^q1|IMeWWnQ(SC#x*uO%0O z1aB>Dfi+-hE9`>JPz*fa{x82y*Z_JwOKV^b-Zbco7X<+j4K1LA7NkZpr1~HIHYhz9 z@z%h3SoteIeiJz)l#d$GGiU}GGy&6p9P8Dr{j3^x5N8tmt})$o$QZ72SAC$)Qgv_J zuhuIi?FTeNlrGx-nrWKFn%?Fq+HvZg)Hj+C%~Eru=78Rp`a&J4f7iT3U8?sq+iG4I zCYWDor)lp{Ryu2S1LdVYtcf;`Xbab+YqZ)>!wcRV6*H@7jbnG?+fG_1kU7;>g z<&==BM84=I@&QXpkc?+ZOOB=0un_2L)Ynkop)F_|+6pAJ46R1XfP^UMjQ2hagTZhg zg5VxCni@e3!C(x4cfryd$bc-$gE~Svpo7R69e@aU1P`et)I91d!XYND#~-;JkUq7Tr=XxPus^yi?jshd}rsh*K zsp+T|RiZK=p#^9O`UXfygA7Om@Ba0(idLd^XeD$^43$ty-^!Uv#ml5}(QIUcX8-Ny z7UCcQ;w&EL=gyyE=Y}wcI-sNT>jDQ33o28{tP3k6xkYa22kB50pUx zlu);+Ta+&)q3Wm_5JCms&tLwT>(>9wtlwlEXWitKv)^!pXssy}ZPbm_7&Qugvpz@X zr@v<^)eSQFny(sH0+=rXK%@Bu)BwN(_-jhAWQHbG0d4pVcf60yQO1Y*q2{jIQ~H(q zd%FF)`5IT9ttLl3O_SDoq0PDVf`X?CQ}7fXszC}5AE{-gt)}IsSttWNM~Prb zLk<%Y6T|v%z`8T*5X+gnk<-TA1U^&@T-Ef`Owz2-o;SE^i!@;RK)W2RGTuf0@Wx2L zuN}w+-oSnU;KD9A1jVodQXvzLnT?POWyWCuh6Sd-7-BWI^l=)6wnckhH%m8LJwvyz zeL|aC`&h+1m5*Y+LZ$oxZ8#tI>G(Aq!0INSOTkHiROTI z7@ov5SSvAG8TDo>0AmsW$jswG1{44u0ui1W08fR!1{E|KcR-_oZ<=M`Ylj-TYlrGs z+Cm*mldV3i$yV~(e3d+fMAcm(QSMjFRqmHhXmOBFXxi6wwrQVqNJG4INUgeIPOZ9D zT=y3|hiZA9L*1jAAL<^}x=B)N-I}ADhc-t?E{yCRxlraUE0uXOtTQn&F|7YR*4^P#Uk!H*bLFISu;;RS~E|#4PRoo4TMG$2r=tfG~mdMKyC@))j$v)n79DcQKOMM zs_kohqwT9(q3f+%p@~!P)Wj*R+X9u=3azqGp_L01W8{KHXyPBEfx0_?-%#2dQl~+dLi0dF-NplbiJZobiHbjP*XLi!K&_FgH_~|i1f%QGCvu< z#F1g0iHV6}{qM2viCOpLv=rBHTEGb+!AYH}SEy5s6HVic6EWy90H89D2NexD85gg` zGIId{ax@-Aj=B@3&bkv?fvy5isNSa&s*bjuR~=PwlnMn$FA*%jC zLsSi`#VLB^r&cB(IfWaLJ#)h0{+{*1^n`< z6}IJ5Bg_#_5oTG0ysIpNVV#MIiDCWkvA%^hleMKdr`V@B2OI&wQFG4Vr8$QN8VzV5 zW<3o6>QOP&gC&Yg3jc)%MkU@)rn$zSwC@}4Yv0#-YZN-~_7QE*+D9mmvPFU9QpFE) zsq{dThxCASWc_{V$QlQUyvCtwVAbKOfr1l)F9jzG9u$32@F06u)<@a1Qc}|bQ&N*Y zN|NDyp13yYbCw4GSXYzpj$du)9`Qx^{fIAQAU`1kurvb3;4v{VG5sl7kNOp?4`Nxf z1_`o?0|i;+mbA-t$%etQQKm3AuT> zpXTPhUjO>p>-F)T@q6MuqhCMfN52k#Kde*u`*)8#`0VbHyZ!D)-0hdTASEz$LHM|^ zE#c#2m+1qRhLgj@#KiPlvu;ILA3#{IA*??mtluQ8I}_GZ3G1&2>m0)R_k{K1g!LN2 z`gy{-wT1N*!upC|#QI=X57uBoY{_atEVMP{pHL=_ei_&@sW17Ck2<>o^*}n>u`$%+s>yd=@4TSZ`2*Uc+U&Q+RnDzHD>uWIUVE_eNje!6bK4vgBe_w1OmYJ(t%9%cG+<<|5NBdy z`Yl<{9Za)+a{$e{ct6cL--l*hxRPc)eJag*Nk5wPvduK>rR!+cOApel3*I-zEL<*JI3%2h=Z z*t3czq})qMPq`Q66Ok9?6KfOGIo1Y9Xd`-wHp-=n?s6%w3mfseynL4>c=>M7&|q26 z(CX!qKGn;?(o#5y*9I+kOiWBnzcuTlXx4A=Xx2r+H0wML&3f5Yn)NgXn)TupH0!0o zH0y$!H0y%Lg!Rv8)`dUNtiO3pvtGE3u-@Oo`uBwOz+c4rK+O6;K>}ty0o(w<&1^DE zGn+8$4gjD4H6DNruii3BfM+yDQAVw!alF=1cSLhtcSPmgDpPqYrzuL5(`4J5%Vpc^ z9yiGA9*ZYRe8m%m8Kp;s89DXY1v&Kz#R&}w#dl`j8GL6Z{-pyKpT1-2VH$1fAw4dQ zlOAX9dApgtCu!iT*+~O~6N2{#CrErmqa{8-LZK)Mg+e_v;#op~nBNx9#KiQQvi=ES zy_T?ENLWXN^<={OJ;J&NVLgwq{)Vt#PFUYaSU*Qtml4)|3G4GLtS=?3-}*(Y567$z z7o5VZp8^u)ZJ0)RW7hWrKpWKKS*nLDJTRRBKz&SBsE>BLK2W<|=cbveb5l9Aid7B@ zkwUEywH#}TYdKceR@c3*O{5aN5vfXiO5QE;Nh`}xq?N_Yj@=M5+joUer0*O9u z){0?@#fo9FOXbcfCzbX}yj3V5NunU~yN@<>Ic%ve!?OWsgImcRmieY=3#~W&7NzIiqr?3RuNW z0#stp5CH6)l;LIPAlAzl@j@bcj-9vI~S`r)QQ`r%qzeWcb__r2y*-S?_Xt+}d8 zijNd*#YfVNrZ#Ct{g@hQ{TT7WN{M)3@hVQ2;#JA1$?qknMvMt-j2IK>;{QXS%bVWm zyYbRfZltFN&kUR$Jd-aIRPkj%Lba$A)dCyd0-Je;&cwv@J7qnYu&ySoXA;)q26vFy>!a9erE+nkaA*^!<>lX;?Jqhc>EUcFi*7N=)*1P@8x-ZL?<;zbmnZZy0 z|Cs897}IN}_u(3(!UMciVBk>t^744#0?i98ccS>?*azS!t$csmYkQd>d!q$a%%J?kZC*w1J zae$M*m}kr5@NB`-EEtFP6cXSm13D8E)9;$~M8dk6ur492=MmPM2{+TxM%I6L7%9kTw`Mn$&C=3b-6tcUqCG2is=>ROmBX_lC zKxblN`U9|@Mp%~+*3S~wj}X><2lHe}`nQDj8-(>cg!RsZ^&5os?`YQf1vKlD zD#H53e42Il9siVd4LO~m3mz#|o)(@e50(Ze2TPw^eZqZmHOejOgDAJ?=~36Br$>Jp zRT2GZ^r$FN^eFtc0KYvc>QeNiXqTufc%D&qc>YmVcq1c~c*e+7JfBEAJp0I@k@gYN z2r*t@#K4Ha@a^Ga!ncRb3~dRU8L%LrDPRHnYxa8f*I;Qm^uqfI;_)mYpcv4Zn3#Uw ztjB#%vu%yHh>p8<{ z)(h{_te3@DSYJn2cl@WUo5}Hk06dbMuW*v{Ge>3SWsXYglNy@VC#@~53GZ5(AD(~O z3A~$W$MO8rJn;O|Zs46uyN;KUb{Fq;S`1!QS_)ocT0vT4dXKdF^d9L0(oE?C(mzk{ zi?=3yar&B!i5WdICZ=`5|BO!MHRb;0HJI?_c$px^yA2_5n|X%L#KiOmW8Idp-i2np zMntn-oJCllMpz$6vtANFSf5I>&JUqk=SLIP-=kSCdq}gMy`E;hpqysCw8g^u5yJX` zzhV9N7DbMwX!r>)AKpSf13D8E(;t%cjfC}qzfGc{Rr!Q2y@;^RC#-)- zST7)~pCPPwBdibprL6yL)W7>M&&rvYm>AZ7E$d87OiWDwL98QkOn;DLCWncMiRnLr z^)rO^eZTN~>ki`PcbMOdFoSg#CWduOaXV?&chr#!9NoyX^?fw!0yfQhx}327HO)Go zOITk>SYJl7&U;E&|AuBg%Rnx0bSD=$UL~wwDkiLd^DkVt?hSr;q!w@4M=jn(gV+v5 zgSg$f5^i@+DmQ_X%8BJZJ|;YriE>cp&;O#78BF#jcauG&K{{T?FHZl?WFW^Qp_Aww zI%yhbnuq6Q@-=x;J*ci!52%F(ypA6s6Vo4^^=ahUI-6#_{u<3Xe>%;&kg)z{2+cZQ zPP1N?M6+J@Db0G>c$)Q6Uz+ub5j5+$r8Mj8jWp}!`w8nlW68|}(*BiKnUX_F5+=JP z2~)~aSErPfD=QY4D~nr8Ru#AMKI3`gjpKRYjpJR$8_Ns8o5H(-w~lAaTUQdz?^Y7c z>sxBY>&x>J=y_hHR|Sput4ww2Zeg|1QD}#EyZjyDZDFABJf5HMF`i%fcS651S^2~= zS(&i>Y?-hutMYbPme9J=Uua!EuhPGKUinD;II?_wWo-F+p=;#=p{uy3*iqawVnf87 zhz*K3Sb>b!0vEDb4!su{7&)N1FA@B{b{R zE;Q@^kG=Z#Q4JYMtdw3=d#@sr9}aUuwPeXJRHy0x=V&V00!7nW*)q|K3a( zGL!5}W;V`ln9?|#H+cyLHW9UL0;7qJKZ2V3CzgDsL8VV$H#ae7#y zIGtFOkegT}s1dFg)PPZjU;&&9qUH>d?Qc>eAb#0asNfPv@Ve{c8z zszDWU8+m~^0m8NeJOA8f=T3S4*_{I2q%MYT&gwQOnbi$OSpZ8sgmxw+texqt?IZTq z?tk7t)c+h13hfK`7TO0)dP@nIq%u?OR+)iO??5P?CeV!MD+aFf4;i>NSvLRBWa;bo z`o6E9wc)07)`ozfIIMW2_ze*3S`L=lwcM88+jCpmN9#Y-N7LoZWa@H8bVf`^bik-T zz$H8c{9d)ib^!vN&$vtH`{=#0_R)Kp=WY8kujEwmP>BN|U`#9mYXZOH^=%7Aodav} z|g5fM_1N*VOvIYQ2=03BxC5!o(0WVcrlkVbX}1Fcn0t z>k%_yE)p|gvWS^5MMSOtM9hTAB4)x|B4)w}h?y|xe}CS(79K)=SAJ1`SKZ~>^t#KO zpE-v(KQpxGfT6{7X8Pf|$8^H;p6SSZ&uC+%Fxr^!d&zjt_L=sc<)rj4=A>}-*;QP9 z&MVFyJSRCpoRe$;cN<&4G3J?bjJXnCBA#acDqgebw!lGjTc{Ga3srEBz!dHgFA}Q7 zi^SVSAI00nI-)?a4(ufgguUPp(NQ=AE)com;R*dkyrF~A_d^F0Hzs%|ZWKBT`-IM5 z)B>;w&sT^tK(S0L9m@m+*Z|gp4;Z-{IsUX_(uK+X6NhNyshu<poOV*C~jYN(3QFgSSr|iJbjwMxy6xj-QMK&N%hM@oow`5sqamlq( zb=jK6bL75Wq5bkNW=P?)%nGR zv-68vUw^vL`Wk#Csy~Vr)juUfAp$e{?Q8%7#Ha>#>L9n zm0FdvC95QlB$j}H%tCyTn@xs|UX5qc)}*OY-TDIh7xa|_LeC?XsOM1;Qb;O71g?T< z0#`twPo$^NbvrM1UGI!iTc}s6O{>q3&movP`VYpiEdRmx9EDIe#4jsd4nDx zbVSiMK~?F->78jM9B=MyPCOv=4)*@lJ6Lh8!nWd?h!3w4@xiEL;5Z(Q|L^@j=B?*Z z6nL$F>d2-!e3X~xRIa8Qx7}z@W%LB{l>S^$bXrCKL2H1RV`9i#R=ooa>5uF8Fv{MnNnsVQ_32{jKs6A zH;}onPru)!PoG`Tzl2@Deg9d?ea``$6L{P>ay(CXTAU}`xxAy?xdR-2+5kr=6igQi z#W#d$;v3@QqUYk{;-m0+@lo*+xDSs|JP{Vc72@k~1w3Eu0MCb)iS6KJ@IG-cybsFod!R`P9nEKE0NpKOPS@+%fz+u;>5K=8)1jg=3o1@pYi+~b&@(=oeT(3O46z5 zjH@|UmR=S-vUQ1ZNpzq1?2dalAQ-+fI%^o}(eLHrkzmthGs(unjs3LbsTL`o)I++w z$746xbKCypu{Qhtmz%CUx!ep0i&~eaEh^i<+f=-9!*4f#+hW&#Fyc2C+x>vhv85x9 zR`mGM&B z4HJf)hf{~WhCU265B>SV#cRDMcWdi5x2-Y<=hJUb)&hdgN!_bDr|Z({1MAYfdwd(c zd#v;}9J87T2>YCD&g}6!b^NrIgTEog2%D$n()VPnOE-hfbY`%}&zkt__*sCk!qHN1 zg`)|5DKMc|AFhV04}-7ZKYmpS-o~~jy2iGgtK9jVt0s<151Sl;-azM|5vU%T(0iGSYJHvLHZs=TxkEp^TLN+q}@fkA_3vd*>go_v~5jnt!zFIP8HCe=>Ns>;}t-7D_Cb} zF;q)tB{cymVOCR1pmOFy>N?1Rr;gxx?sxR_h;XRdpXY z!5k(hnBm6wiQ&eqV0~a#Fb#SWm$z{ql18OWa{tmp@u`d4qrL+*R{m!$TgYO!gVj!2mxW`wKe7|bMWtfJSQ*Mr`ksY z!p@)_OSY8VZoBJqdoKM?hL9ew?WDax+Yu1VTrIYmJxf8;_N4GK_*s6LI~D{j8n<8x zAY9vhwd`t!hsE>3=h|D7cero$J(g|1{m2~|W7uG?eO?5lYw$yV)CO*Z~4cMf*`IAow16z1a82mXaMix0H~=qwkQym((u#eMv1o zF~6MrYUcAfu`?f|GtqPC0YBkur`O7xYn=YLVRyOP@#+=(MXMLDT6hx>^taDlp>Nl? zs!6wTm9O@z&pyc3jGdO-bO69;SnXKlxT|BvNKJ><4UMyxoP26ub}RMnlw0Y2i~HjH z7Ek>+t$pf8K(LgpJ7OsdAO+qCAOV6xrktga0YZvpDmlecU(3K*U+XusgMa;IW^HN> zSuX|zH@?yM_rAvi^{$yym$Lv$B32k{m7rB9Z~P2?aT+wYR%qVIebm~ ziWohl7NNaC!Ef)r88iu7I&aZIK+u}4g=vjt&1W2Bt-n=&M|$hz$%x~6_UQ12Vde1C z$-_GECaaE7x05H_x~ZrXVF+Ljy7u3_LSzJ)$}eYfcj&PdQ5 zh^!!eh??dc;B4>wj1$HV;zR-hmWd@|nLtBR>wj14nZC9r#aT%uDn+{-2H3Y zfwq<&j}B*=4eJPP9d$G7PufZ9Y1TNZ6yo-2byjvrnxj9At=6V?H5)hadIX(6(&*n_ z>*w%VFRhzgSIVj5+~m~Jxy(p9w`W2BcRdT(*ZViHuXFX;m0W#}g7X#+i93r!;?(l; zIkf{R{K$b6Q8j;9R0H=5OW}I>4tx!E6|cqD46&*BgLng+I9LY5;-}IuahhbeG(=(} z)sjvc?3aBvv}#x_3mxLh!W2nE*@`4Z?C^C(tRhZ+L=lG{C*a30xrri7kt=(p$W=ZZ z;VK^{tVyU!SR?cll7yarSL^Byd{qEKXJDrn?U;VuvhVwS%lA~@?7cPV zhSFxWO^eMrK$v-M=KY!HNa>_%QhI!2yehs?&v(WLJzqdDh@Tr^5dS33b^4RIHD}f~ zt#Q~rY0sYB^8sOL{xY2(AQLx3^(W-|riaJo~z4%*y#Xc zJ!1!B{nwXbWN&oD$lf#Av(hse5b(3|4;7C9A#zXTzR2Cvc2C8)9g=A>+jTgGhRZcDG99g zG%Kpfx7Pae{OkG8>o~RTbsUcIz>geb=KbD3nfE!5`-V7=xwklmcm_BKp2UI6oJ39y ze<7z%@QR-+@DxuLWQ)HOli^%(zVw2$NZL8*GPrrr7Iv2mi!ES@R2OcLo*%3p{C#-2 zbmNe#a+54sxfOv&n$ZB&*pV%&S@H$4KKX39tNf>#_ZS08uDEQD(i#4%q1byWg8P zS$=(`)nx^x;ebHNg|1Ns{PzUR^56W%BtZRUDWnAzLt4W7!Uo~}h~1Iwh~0pIWn%?c zHXyW>wY#>JCEKMGCfk9ph<1yDMZ3echxdf7SM#vd*wTUR91hp|-K}>Eg0H^L^5^?a zPy0RHJ8c;tM7l)Jh;-qH@|FD1Pvo}8pYqc@()-e$0zzkL*SSvFo7Dl>8#8}SK#BiA zGu%?%3=eioXAO3TE(wD|mjFVtI`w?Ay8Y+&&+R`00-AxAq8WfdKTPM*50gS;tx2JP zQ2o9pt~xZ;GVN08B0#8~_~G7%K;Pb1r+w`3lT;KhmGY_5MR~4dY3cKl`~Ft`4_@CW z-dF5fbV}8RY)8HWgj)NWu$p&oQv5!=9@kOXaiAj@5a1>7QFvuk6)8A++8ejGc5fa; zY>(`Z*a;lK8Q_rfFt;@4VV!;*sGAN5s00l|&Bz|)n%r}q-+8U|Jl6f3KDGM-Ak^v9 z1k}{M-tzi|uZPz`Z(px;euqGxUjqmj3+u;NKtogOe^cvpQ|K%d!0e#Xpa#}i+D7W_ zPW!g^t)-p9wu(HovCXaH^rwy%3S9~vqZqJW(448anPh4P zl=ZE(9+|&4KeFyton756ZYsZuo64Tt>&%|a-M}%&gK*mLSPevRtay_J#=I%;Cc#YD zQfw@mBDTeg4@hkUyM(g^`iwSa0sT@l)O@f0U1@b?bzxb-!9qBzH@houPFiHaj|Io_ zW>yw|j4quDRZ=6{Oaw^+vEX;45M?6Gifbc>75*~jNTG}>PgSwyX>uo3hTKUWuF8>z z%RE&inJ4Oqu16h{XbBccGyz-qQNaFBwf=X8h(*AjVK4E_#f-7JU=&O&0#*0GqIx*eq-&5CJ|O!ar!Z!q?Y!H`sxP z42tlKS_>}VNdpylzIvb={5ynLqevheyaSb>3>1S=u^jGy zumK;>*Kc~h3*5pJ4s!7jG$SiC{@b;_96ApLGJB|0h{nvP=0oYMAzA{J%97D4sO?M! z^#fGLoB}DJNvv?%UsRW#r8F9C63d!qKsCYte-@O*<6CR}QvQzoOZA)UD(W}G z27E4Tz=l3&vLQ}0r;F3f<`0Ch`Mhy_4sRUn&QF3LNv4X!CF>+-#17)v&IMhkI>r?> zRk#$Jr`cpqPRdWq%?ye^U$DAhZ|2PWX@yp~$8(JG=jWbJ%}ZyeRK!Z-x#8B~-cez} z#R=Z&KP5=(jy06mJ(n*~Fk~-PC)DPu*GePRTIFm^7ukSK$HpMPU}My2s$6v%0GJa1 zNsAIUBrOu^2;GG`e_QJkViB+ctPCqaxX2K~1#iJCJP42j1YLz%qpPrAu$9;^s0q3n zHNmp65WZ;^OByO86^E^Hz;4Vwsz zfH5#a5|JV#@!xNYT8ZBmO8lB{Lsx;XEdk5G62uQ7A$|Y`gFu1>;n&C;DMa$|%XdRU zkmu-j^dPzo5D+p_hmgT!paUkW1?pk706l>^q9*`BjbY=}81fED!LuJZjqF!fs2kK3 zqpvkZo*}`=GeAJsqkGZyYJg4nSBrKMi-4WOu3+a7E}}%ZfS|Tg?^Ii%{%8d12MFqO zYCH8I{Pw2e*YP2K3!Z>{e38|M@qamr?7;7vm1r88jgFRgG$5$|P@hseATh{mO}4fYZc)PJe3sc+$*pND^y2l!<@24-Ldp0B@;G&HsTceQRt)H;(&gCHiA+5|P> z70-pb8Hb^LloLIo4hb!XUBq}nU&HtbT0=2rhEXG-5N0j)BUHicqjo}_c=baN*Evslg0~Y2%Us8{!^`MXlQ8s(`vnw`Uz@f&8Gc8{gE*b(x({qr*;dv z96w#H)2$21+n7EzV+T8vF@b)Uu^8G*S&CPAD@0||sT8Q4$)w`F{;josn5@V@Tz|6h zc>PH@6dr&>*>T)4>^PAT9~CVSy9kA1XUSN}HOX{-nm~_VRgQcF6*JS0=lG;XmfiYr zsB|5V&OgteHzFUYS8Y;xs5mNtT%wZ6Im!*_Qspdkzxoh*9N*K*u@W$<5HIe(SaeoO z&+1g2wAO@|LEGNF3z(hjnctC(?<`fWNFxC17XX0!s15iNl%jh;DFEOTUh5MQ&m>L2 zYyF|7)-^OV{^7MQr4B$m#w#eC;?d{cwY)Qyxd3`ZiQ8_sjkM*8)|pBE+P<-o5xLl5DO;K~_cq(Y@7+pma_87Pg= zV1-=$Ggzj6g6Jp@BRfW5*`^VeDo~cL%u%0K!s>lstGW`L0ff(dF7NZbh-oDIus;Hz zpm_h3Hfzf5)81MUGPl9g4^tKxY&PHo?@2a>kL z)RNY4oSCMqS({5YFWnfk*mRM~IHO0KdWzb>a;7CvZ!@1#??8{fvDV2S$sOd6t<#%+ zZJn+#846RFh;9g9i>`}jO6pLA^Oq&pzgoAHnx=3RMZ5A zj}1%>j9|nzQB|<(%{+T|Z%3?Y??hjSY1hZ9ipQW@Xl@WcTn-kssf?zVC_Z3gz(=1QLM`ARt_2 z7?KfPAA2-1>t(VZ!~6TzE3J217=VBxN+(nS0G5Q;`t-y%Nz;Y8!W%+eO|5HaX#9gu z!UVFW(#BE8FeDI6QGSl6U+MmZ!(;x%@L)tiA(TQ!43tL6?mf_H+%aJwzvmk3H`W~5 zV(RzIN@_k-#QaR9LtWom>twPwncRA$CARg5!Wu93^};H#zA$s>n(W?CMx#y3ul0qg zkJF#T3^jk(JgGKH;g3#Ia4=`J5Od?v1}u4jsqv{nvA@PoO1u~SHbp0MQp#EZBnamH z0Y-_zJz$!nlk-RV_Mq0V%fZYjix`h6ONGslli~mX=sf_`uhfgwyVLJy)uul1EcZI% zk^RB1W_|f;umb=vG(26|HY7sh)RkylQcD6msYSpQh6uQtTG!Cf_(wlv+DQEXePFGn zEvK5&Ybo{Z)Naf6BW?D~8fq0(#xkVoQgs^dPk<6~sh#xzIa1Pa)M{ zGde*Xs5*$5SEN<0ERKxb6kiu9h>|88i2AepSNg*a6pYHllcjJ}K2TVYDigFx%|SUK z4R6Wm73rz*Ju-Wx*65Jh+MW!-Zk(qENp*Z1;@Ubvf z_*hfx8X6k^=yU5)czp-4tr)iSWd&z5_!;jLrbTCytT^V()eN)#!tQfjrY(Qg3|7a! zC%wHBFrASGZK9;G+-V-v`^*ArI+XdXwcbm1B=@#Nwwkv@s>rGlRkET-W}@6W=s9>^ ztj==H8BWcNf0BAJA(x%lpUFNB2>9UL7CwnIlC&viTV!9HYm`}TV&2BQX@G#f8?i_C z147%lPo}L$-tBrn*$)nU6FAG)vLdZ&L-~AU`iKkiP`*qyMLxIhe1Bfw-oVhedwd6c zg8VYQH`5<8cQJmFa)ysdtKl)?40zn|7TN6KEs2W~j1w0L?Su_NJ58-?XlVSSp9#~} z4z<;ZW`Az`JmrP>>98w#{2T4;aaZH3qD`X%xqq>YSzcS&TgGqPxJYZ!_Y31$4C+$q zd%V_jsZW@0)Z38Dmp8q0#%ujm%(-^{zg})0kJtL>Wse?*h+6;Qt6G0Sb|Js`G_EE8 z(|GJO;(?u(Q{*o4&x4<()WPU%ZeC+{WvoT~ds1AVUC*xGR6tlnakj+&$9s5$=c9Mz84A`AnB^#5q@OrM%K<2a61tVNC%9l$Cw zl!`jy0TqgaS}7G3rdGkDRNqU*)qipV`@&eerwqooAle=euXy`xU>0`;%tdFOGG{+q(Ot z?uPHybI^MTAm4$WM?4kwy;Z%V#{-X49U57!b@cu8OkinX!h!E?m#lEM`Ke-#6I1$1 zbysFBJt_Mf*$rv2uNYkr{FNmz`Rm>KsTn<=}NK7+65g;?-#m4EJ|ym zE!W+{3{=`zR#s*5YXrxH+qK>cfx1W2!86V?<(lO)e$!^vj~?TzH`Bc6X#Q!jBb*>~ z#tclkYWBaFqrYMLmgJTMQJ?2MqNEos&+Vu`)DT%OgnPw+XbsrTvZ2~!@6KKz1?4g+ z(YZ<-A1EehDx#~y|C&~hk57%=Zq~IWwtkczmKlp%oO<+LN7@U1D_v* zmy8)G59$0uQ5Qyi;L`qhj3a;8-6h?OgzIB{3LQpc6-+RpO z?ANE323RC4g=t_xEX7A%rtlp+D`mkI;wrfsRUiZMXNFUzq)%8mkV_mu*&-xi26WE4 zS}ft6e{Ig{uW4+tZVp?58EAO(_*`XQ@2l=~(bl(pc)-twEQP`25=Ds(z>>j<8J-CdMu@0$SH*QUnh)1w>4@bb41p`4hpG+-1k(_-|5lu zV`Wx(b=fHuUM7&1$-YFKU{`rJDnO1jcG8O6=|prztk2DL+Mm$LogMuc<;E>QE32r|&$m=-xrifM{E z50WAqC6TQp>nF_U&&b@C8_C=00dPsYRo=3v*TW}>Sc6prA!DMxIs7&;sNOEzJ|nbV zeLj-3uIcGjr{e3v-!54fe$Bm|_y=X4>zX)R;KmI}yhC{gX)mW2rt;0Ax*o)}_+A+= zdMP-U>_j(*$w~e+4{@DwI+IN{+2r@`6SM%mkJAo{smUF5XJkXjXvQ7Ls%p4_&1+Vz z>`O!kWgzN;fX2EmeOU7mUjN!|TXdx?6(Pbm2G;g2?|!3V$eg9|vZKg;c%NK?_8_QS zg-Q^KG0=_gnklV%2T+b^jz1uRfTq%&&?3AAqZi5}90qftoA_BUOz$}`9J)nV1ja#l zJPWjgyfROh3i;}c^{kt`3cMDKgwhFj!7;8q;bYJbA`|7TZ+SUD8CxXW0obv(2+{~1 zU>8)2Pi8!atcY<;nEMP+v9-cuKo+}KxEd(rSPQQK3XYFZkvYYV6?Ec7peOW0Mj}hX zn + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "%{APPNAMELC}resource.h" + +#include "settings.h" +#include "settingsadaptor.h" +#include "debug.h" + +#include + +using namespace Akonadi; + +%{APPNAME}Resource::%{APPNAME}Resource(const QString &id) + : ResourceBase(id) +{ + new SettingsAdaptor(Settings::self()); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), + Settings::self(), + QDBusConnection::ExportAdaptors); + + // TODO: you can put any resource specific initialization code here. + qCDebug(log_%{APPNAMELC}resource) << "Resource started"; +} + +%{APPNAME}Resource::~%{APPNAME}Resource() +{ +} + +void %{APPNAME}Resource::retrieveCollections() +{ + // TODO: this method is called when Akonadi wants to have all the + // collections your resource provides. + // Be sure to set the remote ID and the content MIME types +} + +void %{APPNAME}Resource::retrieveItems(const Akonadi::Collection &collection) +{ + // TODO: this method is called when Akonadi wants to know about all the + // items in the given collection. You can but don't have to provide all the + // data for each item, remote ID and MIME type are enough at this stage. + // Depending on how your resource accesses the data, there are several + // different ways to tell Akonadi when you are done. +} + +bool %{APPNAME}Resource::retrieveItem(const Akonadi::Item &item, const QSet &parts) +{ + // TODO: this method is called when Akonadi wants more data for a given item. + // You can only provide the parts that have been requested but you are allowed + // to provide all in one go + + return true; +} + +void %{APPNAME}Resource::aboutToQuit() +{ + // TODO: any cleanup you need to do while there is still an active + // event loop. The resource will terminate after this method returns +} + +void %{APPNAME}Resource::configure(WId windowId) +{ + // TODO: this method is usually called when a new resource is being + // added to the Akonadi setup. You can do any kind of user interaction here, + // e.g. showing dialogs. + // The given window ID is usually useful to get the correct + // "on top of parent" behavior if the running window manager applies any kind + // of focus stealing prevention technique + // + // If the configuration dialog has been accepted by the user by clicking Ok, + // the signal configurationDialogAccepted() has to be emitted, otherwise, if + // the user canceled the dialog, configurationDialogRejected() has to be emitted. +} + +void %{APPNAME}Resource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) +{ + // TODO: this method is called when somebody else, e.g. a client application, + // has created an item in a collection managed by your resource. + + // NOTE: There is an equivalent method for collections, but it isn't part + // of this template code to keep it simple +} + +void %{APPNAME}Resource::itemChanged(const Akonadi::Item &item, const QSet &parts) +{ + // TODO: this method is called when somebody else, e.g. a client application, + // has changed an item managed by your resource. + + // NOTE: There is an equivalent method for collections, but it isn't part + // of this template code to keep it simple +} + +void %{APPNAME}Resource::itemRemoved(const Akonadi::Item &item) +{ + // TODO: this method is called when somebody else, e.g. a client application, + // has deleted an item managed by your resource. + + // NOTE: There is an equivalent method for collections, but it isn't part + // of this template code to keep it simple +} + +AKONADI_RESOURCE_MAIN(%{APPNAME}Resource) diff --git a/templates/akonadiresource/src/%{APPNAMELC}resource.desktop b/templates/akonadiresource/src/%{APPNAMELC}resource.desktop new file mode 100644 index 0000000..d07f4f7 --- /dev/null +++ b/templates/akonadiresource/src/%{APPNAMELC}resource.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=%{APPNAME} Resource +Comment=An Akonadi resource plugin for %{APPNAME} +Type=AkonadiResource +Exec=akonadi_%{APPNAMELC}_resource +Icon=text-directory + +X-Akonadi-MimeTypes=text/directory +X-Akonadi-Capabilities=Resource +X-Akonadi-Identifier=akonadi_%{APPNAMELC}_resource diff --git a/templates/akonadiresource/src/%{APPNAMELC}resource.h b/templates/akonadiresource/src/%{APPNAMELC}resource.h new file mode 100644 index 0000000..0282d5d --- /dev/null +++ b/templates/akonadiresource/src/%{APPNAMELC}resource.h @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef %{APPNAMEUC}RESOURCE_H +#define %{APPNAMEUC}RESOURCE_H + +#include + +class %{APPNAME}Resource : public Akonadi::ResourceBase, + public Akonadi::AgentBase::Observer +{ + Q_OBJECT + +public: + explicit %{APPNAME}Resource(const QString &id); + ~%{APPNAME}Resource() override; + +public Q_SLOTS: + void configure(WId windowId) override; + +protected Q_SLOTS: + void retrieveCollections() override; + void retrieveItems(const Akonadi::Collection &col) override; + bool retrieveItem(const Akonadi::Item &item, const QSet &parts) override; + +protected: + void aboutToQuit() override; + + void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + void itemChanged(const Akonadi::Item &item, const QSet &parts) override; + void itemRemoved(const Akonadi::Item &item) override; +}; + +#endif diff --git a/templates/akonadiresource/src/CMakeLists.txt b/templates/akonadiresource/src/CMakeLists.txt new file mode 100644 index 0000000..c1e1e75 --- /dev/null +++ b/templates/akonadiresource/src/CMakeLists.txt @@ -0,0 +1,39 @@ +set(%{APPNAMELC}resource_SRCS + %{APPNAMELC}resource.cpp +) + +ecm_qt_declare_logging_category(%{APPNAMELC}resource_SRCS + HEADER debug.h + IDENTIFIER log_%{APPNAMELC}resource + CATEGORY_NAME log_%{APPNAMELC}resource +) + +kconfig_add_kcfg_files(%{APPNAMELC}resource_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/settings.kcfgc +) + +kcfg_generate_dbus_interface( + ${CMAKE_CURRENT_SOURCE_DIR}/settings.kcfg + org.kde.Akonadi.%{APPNAME}.Settings +) + +qt_add_dbus_adaptor(%{APPNAMELC}resource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.%{APPNAME}.Settings.xml + ${CMAKE_CURRENT_BINARY_DIR}/settings.h + Settings +) + +add_executable(akonadi_%{APPNAMELC}_resource ${%{APPNAMELC}resource_SRCS}) +set_target_properties(akonadi_%{APPNAMELC}_resource PROPERTIES MACOSX_BUNDLE FALSE) + +target_link_libraries(akonadi_%{APPNAMELC}_resource + Qt::DBus + KF5::AkonadiAgentBase + KF5::ConfigCore +) + +install(TARGETS akonadi_%{APPNAMELC}_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +install(FILES %{APPNAMELC}resource.desktop + DESTINATION ${KDE_INSTALL_DATAROOTDIR}/akonadi/agents +) diff --git a/templates/akonadiresource/src/settings.kcfg b/templates/akonadiresource/src/settings.kcfg new file mode 100644 index 0000000..8f548b6 --- /dev/null +++ b/templates/akonadiresource/src/settings.kcfg @@ -0,0 +1,14 @@ + + + + + + + false + + + diff --git a/templates/akonadiresource/src/settings.kcfgc b/templates/akonadiresource/src/settings.kcfgc new file mode 100644 index 0000000..b07d81d --- /dev/null +++ b/templates/akonadiresource/src/settings.kcfgc @@ -0,0 +1,8 @@ +File=settings.kcfg +ClassName=Settings +Mutators=true +ItemAccessors=true +SetUserTexts=true +Singleton=true +#IncludeFiles= +GlobalEnums=true diff --git a/templates/akonadiserializer/CMakeLists.txt b/templates/akonadiserializer/CMakeLists.txt new file mode 100644 index 0000000..c652e0d --- /dev/null +++ b/templates/akonadiserializer/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.16) + +project(%{APPNAMELC}) + +set(KF5_MIN_VERSION "5.82.0") + +set(ECM_MIN_VERSION ${KF5_MIN_VERSION}) +find_package(ECM ${ECM_MIN_VERSION} CONFIG REQUIRED) + +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_MODULE_PATH}) + +include(FeatureSummary) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) + +set(QT_MIN_VERSION "5.15.0") +find_package(Qt5 ${QT_MIN_VERSION} REQUIRED Core Network Gui) + +find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) + +set(AKONADI_MIN_VERSION "5.16.0") +find_package(KF5Akonadi ${AKONADI_MIN_VERSION} CONFIG REQUIRED) + +add_subdirectory(src) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/templates/akonadiserializer/README b/templates/akonadiserializer/README new file mode 100644 index 0000000..511c490 --- /dev/null +++ b/templates/akonadiserializer/README @@ -0,0 +1,66 @@ +How To Build This Template +-=-=-=-=-=-=-=-=-=-=-=-=-= + +--- On Linux & similar: + +cd +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=$MY_PREFIX -DCMAKE_BUILD_TYPE=Debug +make +make install or su -c 'make install' + +(MY_PREFIX is where you install your Akonadi setup, replace it accordingly) + +to uninstall the project: +make uninstall or su -c 'make uninstall' + +Note: you can use another build path. Then cd in your build dir and: +export MY_SRC=path_to_your_src +cmake $MY_SRC -DCMAKE_INSTALL_PREFIX=$MY_PREFIX -DCMAKE_BUILD_TYPE=Debug + +--- On Windows: + +cd +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=%MY_PREFIX% -DCMAKE_BUILD_TYPE=Debug +[n]make +[n]make install + +(MY_PREFIX is where you install your Akonadi setup, replace it accordingly) + +to uninstall the project: +[n]make uninstall + +Note: use nmake if you're building with the Visual Studio compiler, or make +if you're using the minGW compiler + + +Implementation hints +-=-=-=-=-=-=-=-=-=-= + +The code generated by the template can be compiled without any further +changes, so you can start with your own code right away. + +However, there are a couple of things you will need to change outside the +serialzer's code, i.e. in the serializer's .desktop file: + +- Name field + +- Comment field + +- Type field: MIME type of your data type + + +Documentation +-=-=-=-=-=-=- + +The Akonadi-KDE API documentation can be found here: +https://api.kde.org/kdepim/akonadi/html/index.html + +General developer information, e.g. tutorials can be found here: +https://techbase.kde.org/KDE_PIM/Akonadi + +The contact site can be found here: +https://community.kde.org/KDE_PIM/Contact diff --git a/templates/akonadiserializer/akonadiserializer.kdevtemplate b/templates/akonadiserializer/akonadiserializer.kdevtemplate new file mode 100644 index 0000000..00bd7a8 --- /dev/null +++ b/templates/akonadiserializer/akonadiserializer.kdevtemplate @@ -0,0 +1,75 @@ +[General] +Name=C++ +Name[ar]=سي++ +Name[az]=C++ +Name[ca]=C++ +Name[ca@valencia]=C++ +Name[cs]=C++ +Name[da]=C++ +Name[de]=C++ +Name[el]=C++ +Name[en_GB]=C++ +Name[es]=C++ +Name[et]=C++ +Name[eu]=C++ +Name[fi]=C++ +Name[fr]=C++ +Name[gl]=C++ +Name[hu]=C++ +Name[ia]=C++ +Name[it]=C++ +Name[ko]=C++ +Name[nl]=C++ +Name[pl]=C++ +Name[pt]=C++ +Name[pt_BR]=C++ +Name[ru]=C++ +Name[sk]=C++ +Name[sl]=C++ +Name[sr]=Ц++ +Name[sr@ijekavian]=Ц++ +Name[sr@ijekavianlatin]=C++ +Name[sr@latin]=C++ +Name[sv]=C++ +Name[uk]=C++ +Name[x-test]=xxC++xx +Name[zh_CN]=C++ +Name[zh_TW]=C++ +Comment=Akonadi Serializer Template. A template for an Akonadi data serializer plugin +Comment[ar]=قالب سَلسَلة «أكونادي». قالب لملحقة سَلسَلة بيانات «أكونادي» +Comment[az]=Akonadi kontaktları məlumatlarının saxlanılması şablonu. Akonadi kontaktlarının məlumatları saxlama modulu üçün şablon +Comment[ca]=Plantilla de serialitzador de l'Akonadi. Una plantilla per a un connector de serialització de dades de l'Akonadi +Comment[ca@valencia]=Plantilla de serialitzador de l'Akonadi. Una plantilla per a un connector de serialització de dades de l'Akonadi +Comment[da]=Skabelon til Akonadi-serializer. En skabelon til et Akonadi data-serializer-plugin +Comment[de]=Vorlage für Akonadi-Serialisierer. Eine Vorlage für einen Datenserialisierer für das Akonadi-PIM-Framework +Comment[el]=Πρότυπο σειριακοποιητή Akonadi. Ένα πρότυπο για ένα πρόσθετο γραμμικής διάταξης δεδομένων του Akonadi +Comment[en_GB]=Akonadi Serialiser Template. A template for an Akonadi data serialiser plugin +Comment[es]=Plantilla serializadora de Akonadi. Una plantilla para un complemento serializador de Akonadi +Comment[et]=Akonadi jadasti mall. Akonadi andmete jadastamise plugina mall +Comment[eu]=Akonadi serializatzaile txantiloia. Akonadi datu-serializatzaile plugin baterako txantiloia +Comment[fi]=Akonadin serialisointimalli Akonadin dataserialisointiliitännäiselle +Comment[fr]=Modèle de sérialiseur Akonadi. Un modèle pour un module externe sérialiseur de données pour Akonadi +Comment[gl]=Modelo de serializador de Akonadi. Un modelo para un complemento serializador de datos de Akonadi. +Comment[hu]=Akonadi szerializáló-sablon. Sablon Akonadi adatszerializáló bővítményhez +Comment[ia]=Akonadi Serializer Template (Patrono Divulgator de Akonadi). Un patrono per un plugin de divulgation (serializer) de datos de Akonadi +Comment[it]=Modello di serializzatore Akonadi. Un modello per un'estensione di serializzazione dei dati di Akonadi +Comment[ko]=Akonadi 시리얼라이저 템플릿. Akonadi 데이터 시리얼라이저 플러그인용 템플릿 +Comment[nl]=Akonadi sjabloon voor serialisator. Een sjabloon voor een Akonadi serialisatorplug-in +Comment[pl]=Szablon serializacji Akonadi. Szablon dla wtyczki serializacji danych Akonadi +Comment[pt]=Modelo de Serialização do Akonadi. Um modelo para um 'plugin' de serialização de dados do Akonadi +Comment[pt_BR]=Modelo de serialização do Akonadi. Um modelo para um plugin de serialização de dados do Akonadi +Comment[ru]=Шаблон сохранения данных контактов Akonadi. Шаблон для модуля сохранения данных контактов Akonadi. +Comment[sk]=Å ablóna Akonadi serializátora. Å ablóna pre plugin serializátora dát Akonadi +Comment[sl]=Predloga razvrščevalnika za Akonadi. Predloga za vstavek razvrščevalnika podatkovnih virov Akonadi +Comment[sr]=Шаблон прикључка Аконадијевог серијализатора +Comment[sr@ijekavian]=Шаблон прикључка Аконадијевог серијализатора +Comment[sr@ijekavianlatin]=Å ablon priključka Akonadijevog serijalizatora +Comment[sr@latin]=Å ablon priključka Akonadijevog serijalizatora +Comment[sv]=Akonadi-serialiseringsmall. En mall för ett Akonadi insticksprogram för dataserialisering +Comment[uk]=Шаблон серіалізатора Akonadi. Шаблон для додатка перетворення даних у послідовну форму Akonadi +Comment[x-test]=xxAkonadi Serializer Template. A template for an Akonadi data serializer pluginxx +Comment[zh_CN]=Akonadi 数据转换器模板。Akonadi 数据转换器插件的模板 +Comment[zh_TW]=Akonadi 序列化模板。用於 Akonadi 資料序列化外掛程式的模板 +Category=Akonadi/Serializer +Icon=akonadiserializer.png +ShowFilesAfterGeneration=src/%{APPNAMELC}resource.cpp diff --git a/templates/akonadiserializer/akonadiserializer.png b/templates/akonadiserializer/akonadiserializer.png new file mode 100644 index 0000000000000000000000000000000000000000..bad28b7b9826fa3b2c2dfe60ff5099e99803944a GIT binary patch literal 14537 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 literal 0 HcmV?d00001 diff --git a/templates/akonadiserializer/src/CMakeLists.txt b/templates/akonadiserializer/src/CMakeLists.txt new file mode 100644 index 0000000..004c5b4 --- /dev/null +++ b/templates/akonadiserializer/src/CMakeLists.txt @@ -0,0 +1,16 @@ +set(akonadi_serializer_%{APPNAMELC}_SRCS + akonadi_serializer_%{APPNAMELC}.cpp +) + +add_library(akonadi_serializer_%{APPNAMELC} MODULE ${akonadi_serializer_%{APPNAMELC}_SRCS}) + +target_link_libraries(akonadi_serializer_%{APPNAMELC} + KF5::AkonadiCore +) + +install(TARGETS akonadi_serializer_%{APPNAMELC} + DESTINATION ${KDE_INSTALL_PLUGINDIR}) + +install(FILES akonadi_serializer_%{APPNAMELC}.desktop + DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/plugins/serializer +) diff --git a/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.cpp b/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.cpp new file mode 100644 index 0000000..3696d60 --- /dev/null +++ b/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.cpp @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "akonadi_serializer_%{APPNAMELC}.h" + +#include + +using namespace Akonadi; + +bool SerializerPlugin%{APPNAME}::deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) +{ + Q_UNUSED(item) + Q_UNUSED(label) + Q_UNUSED(data) + Q_UNUSED(version) + + // TODO Implement this + + return false; +} + +void SerializerPlugin%{APPNAME}::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) +{ + Q_UNUSED(item) + Q_UNUSED(label) + Q_UNUSED(data) + Q_UNUSED(version) + + // TODO Implement this +} + +QSet SerializerPlugin%{APPNAME}::parts(const Item &item) const +{ + // only need to reimplement this when implementing partial serialization + // i.e. when using the "label" parameter of the other two methods + return ItemSerializerPlugin::parts(item); +} diff --git a/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.desktop b/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.desktop new file mode 100644 index 0000000..d389bd6 --- /dev/null +++ b/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.desktop @@ -0,0 +1,7 @@ +[Misc] +Name=%{APPNAME} Serializer +Comment=An Akonadi serializer plugin for %{APPNAMELC} + +[Plugin] +Type=application/x-vnd.kde.%{APPNAMELC} +X-KDE-Library=akonadi_serializer_%{APPNAMELC} diff --git a/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.h b/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.h new file mode 100644 index 0000000..b8e083f --- /dev/null +++ b/templates/akonadiserializer/src/akonadi_serializer_%{APPNAMELC}.h @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef AKONADI_SERIALIZER_%{APPNAMEUC}_H +#define AKONADI_SERIALIZER_%{APPNAMEUC}_H + +#include + +#include + +namespace Akonadi +{ + +class SerializerPlugin%{APPNAME} : public QObject + , public ItemSerializerPlugin +{ + Q_OBJECT + Q_INTERFACES(Akonadi::ItemSerializerPlugin) + Q_PLUGIN_METADATA(IID "org.kde.akonadi.SerializerPlugin%{APPNAME}") + +public: + bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) override; + void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) override; + + QSet parts(const Item &item) const; +}; + +} + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..a29b323 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(libs) diff --git a/tests/asapcat/imap-4.10-sync.asap b/tests/asapcat/imap-4.10-sync.asap new file mode 100644 index 0000000..d13ebf1 --- /dev/null +++ b/tests/asapcat/imap-4.10-sync.asap @@ -0,0 +1,4 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* CACHEONLY EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE) +4 LOGOUT diff --git a/tests/asapcat/imap-4.11-body-check.asap b/tests/asapcat/imap-4.11-body-check.asap new file mode 100644 index 0000000..c1b86d3 --- /dev/null +++ b/tests/asapcat/imap-4.11-body-check.asap @@ -0,0 +1,4 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* CACHEONLY CHECKCACHEDPARTSONLY EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE PLD:RFC822) +4 LOGOUT diff --git a/tests/asapcat/kmail-4.10-folder-listing.asap b/tests/asapcat/kmail-4.10-folder-listing.asap new file mode 100644 index 0000000..680781c --- /dev/null +++ b/tests/asapcat/kmail-4.10-folder-listing.asap @@ -0,0 +1,5 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* IGNOREERRORS ANCESTORS INF EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE DATETIME PLD:ENVELOPE) +4 LOGOUT + diff --git a/tests/asapcat/kmail-4.11-folder-listing.asap b/tests/asapcat/kmail-4.11-folder-listing.asap new file mode 100644 index 0000000..f5bd4f4 --- /dev/null +++ b/tests/asapcat/kmail-4.11-folder-listing.asap @@ -0,0 +1,5 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 471 +3 FETCH 1:* IGNOREERRORS ANCESTORS INF EXTERNALPAYLOAD (UID REMOTEID REMOTEREVISION COLLECTIONID FLAGS SIZE PLD:ENVELOPE) +4 LOGOUT + diff --git a/tests/asapcat/kmail-4.12-folder-listing.asap b/tests/asapcat/kmail-4.12-folder-listing.asap new file mode 100644 index 0000000..f3acf20 --- /dev/null +++ b/tests/asapcat/kmail-4.12-folder-listing.asap @@ -0,0 +1,5 @@ +1 LOGIN asapcat +2 UID SELECT SILENT 496 +3 FETCH 1:* IGNOREERRORS ANCESTORS INF EXTERNALPAYLOAD (UID COLLECTIONID FLAGS SIZE PLD:ENVELOPE) +4 LOGOUT + diff --git a/tests/libs/CMakeLists.txt b/tests/libs/CMakeLists.txt new file mode 100644 index 0000000..d89755b --- /dev/null +++ b/tests/libs/CMakeLists.txt @@ -0,0 +1,37 @@ +find_package(Qt5Test ${QT_REQUIRED_VERSION} CONFIG REQUIRED) + +if(${EXECUTABLE_OUTPUT_PATH}) + set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} ) +else() + set( PREVIOUS_EXEC_OUTPUT_PATH . ) +endif() +set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) +set( TEST_RESULT_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/testresults ) +file(MAKE_DIRECTORY ${TEST_RESULT_OUTPUT_PATH}) + +kde_enable_exceptions() + +# convenience macro to add akonadi demo application +macro(add_akonadi_demo _source) + set(_test ${_source}) + get_filename_component(_name ${_source} NAME_WE) + add_executable(${_name} ${_test}) + target_link_libraries(${_name} + KF5AkonadiCore + KF5AkonadiWidgets + KF5::I18n + ) +endmacro() + +# demo applications +add_akonadi_demo(itemdumper.cpp) +add_akonadi_demo(subscriber.cpp) +add_akonadi_demo(agentinstancewidgettest.cpp) +add_akonadi_demo(agenttypewidgettest.cpp) +add_akonadi_demo(pluginloadertest.cpp) +##REACTIVATE +#add_akonadi_demo(selftester.cpp) +add_akonadi_demo(collectiondialog.cpp) +add_akonadi_demo(conflictresolvedialogtest_gui.cpp) + +add_subdirectory(etm_test_app) diff --git a/tests/libs/agentinstancewidgettest.cpp b/tests/libs/agentinstancewidgettest.cpp new file mode 100644 index 0000000..1d263b2 --- /dev/null +++ b/tests/libs/agentinstancewidgettest.cpp @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agentinstancewidgettest.h" + +#include "agentinstance.h" + +#include +#include +#include +#include +#include +#include + +Dialog::Dialog(QWidget *parent) + : QDialog(parent) +{ + auto layout = new QVBoxLayout(this); + + mWidget = new Akonadi::AgentInstanceWidget(this); + connect(mWidget, &Akonadi::AgentInstanceWidget::currentChanged, this, &Dialog::currentChanged); + + auto box = new QDialogButtonBox(this); + + layout->addWidget(mWidget); + layout->addWidget(box); + + QPushButton *ok = box->addButton(QDialogButtonBox::Ok); + connect(ok, &QPushButton::clicked, this, &Dialog::accept); + + resize(450, 320); +} + +void Dialog::done(int r) +{ + if (r == Accepted) { + qDebug("'%s' selected", qPrintable(mWidget->currentAgentInstance().identifier())); + } + + QDialog::done(r); +} + +void Dialog::currentChanged(const Akonadi::AgentInstance ¤t, const Akonadi::AgentInstance &previous) +{ + qDebug("current changed: %s -> %s", qPrintable(previous.identifier()), qPrintable(current.identifier())); +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData aboutData(QStringLiteral("agentinstanceviewtest"), QStringLiteral("agentinstanceviewtest"), QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + Dialog dlg; + dlg.exec(); + + return 0; +} diff --git a/tests/libs/agentinstancewidgettest.h b/tests/libs/agentinstancewidgettest.h new file mode 100644 index 0000000..3c33fe7 --- /dev/null +++ b/tests/libs/agentinstancewidgettest.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "agentinstancewidget.h" + +class Dialog : public QDialog +{ + Q_OBJECT + +public: + Dialog(QWidget *parent = nullptr); + + void done(int) override; + +private Q_SLOTS: + void currentChanged(const Akonadi::AgentInstance ¤t, const Akonadi::AgentInstance &previous); + +private: + Akonadi::AgentInstanceWidget *mWidget; +}; + diff --git a/tests/libs/agenttypewidgettest.cpp b/tests/libs/agenttypewidgettest.cpp new file mode 100644 index 0000000..a70f4f8 --- /dev/null +++ b/tests/libs/agenttypewidgettest.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "agenttypewidgettest.h" + +#include "agentfilterproxymodel.h" +#include "agenttype.h" + +#include +#include + +#include +#include +#include +#include +#include + +// krazy:excludeall=qclasses + +Dialog::Dialog(QWidget *parent) + : QDialog(parent) +{ + auto layout = new QVBoxLayout(this); + + mFilter = new QComboBox(this); + mFilter->addItem(QStringLiteral("None")); + mFilter->addItem(QStringLiteral("text/calendar")); + mFilter->addItem(QStringLiteral("text/directory")); + mFilter->addItem(QStringLiteral("message/rfc822")); + connect(mFilter, qOverload(&QComboBox::activated), this, &Dialog::filterChanged); + + mWidget = new Akonadi::AgentTypeWidget(this); + connect(mWidget, &Akonadi::AgentTypeWidget::currentChanged, this, &Dialog::currentChanged); + + auto box = new QDialogButtonBox(this); + + layout->addWidget(mFilter); + layout->addWidget(mWidget); + layout->addWidget(box); + + QPushButton *ok = box->addButton(QDialogButtonBox::Ok); + connect(ok, &QPushButton::clicked, this, &Dialog::accept); + + QPushButton *cancel = box->addButton(QDialogButtonBox::Cancel); + connect(cancel, &QPushButton::clicked, this, &Dialog::reject); + + resize(450, 320); +} + +void Dialog::done(int r) +{ + if (r == Accepted) { + qDebug("'%s' selected", qPrintable(mWidget->currentAgentType().identifier())); + } + + QDialog::done(r); +} + +void Dialog::currentChanged(const Akonadi::AgentType ¤t, const Akonadi::AgentType &previous) +{ + qDebug("current changed: %s -> %s", qPrintable(previous.identifier()), qPrintable(current.identifier())); +} + +void Dialog::filterChanged(int index) +{ + mWidget->agentFilterProxyModel()->clearFilters(); + if (index > 0) { + mWidget->agentFilterProxyModel()->addMimeTypeFilter(mFilter->itemText(index)); + } +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData aboutData(QStringLiteral("agenttypeviewtest"), QStringLiteral("agenttypeviewtest"), QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + Dialog dlg; + dlg.exec(); + + return 0; +} diff --git a/tests/libs/agenttypewidgettest.h b/tests/libs/agenttypewidgettest.h new file mode 100644 index 0000000..8365de7 --- /dev/null +++ b/tests/libs/agenttypewidgettest.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2006-2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "agenttypewidget.h" + +class QComboBox; + +class Dialog : public QDialog +{ + Q_OBJECT + +public: + Dialog(QWidget *parent = nullptr); + + void done(int) override; + +private Q_SLOTS: + void currentChanged(const Akonadi::AgentType ¤t, const Akonadi::AgentType &previous); + void filterChanged(int); + +private: + Akonadi::AgentTypeWidget *mWidget; + QComboBox *mFilter; +}; + diff --git a/tests/libs/collectiondialog.cpp b/tests/libs/collectiondialog.cpp new file mode 100644 index 0000000..c4f767c --- /dev/null +++ b/tests/libs/collectiondialog.cpp @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2010 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "collectiondialog.h" + +#include + +#include +#include +#include +#include + +using namespace Akonadi; + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData aboutData(QStringLiteral("test"), i18n("Test Application"), QStringLiteral("1.0")); + + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + + parser.process(app); + aboutData.processCommandLine(&parser); + + CollectionDialog dlg; + dlg.setMimeTypeFilter({QStringLiteral("text/directory")}); + dlg.setAccessRightsFilter(Collection::CanCreateItem); + dlg.setDescription(i18n("Select an address book for saving:")); + dlg.setSelectionMode(QAbstractItemView::ExtendedSelection); + dlg.changeCollectionDialogOptions(CollectionDialog::AllowToCreateNewChildCollection); + dlg.exec(); + + const auto selectedCollections = dlg.selectedCollections(); + for (const Collection &collection : selectedCollections) { + qDebug() << "Selected collection:" << collection.name(); + } + + return 0; +} diff --git a/tests/libs/conflictresolvedialogtest_gui.cpp b/tests/libs/conflictresolvedialogtest_gui.cpp new file mode 100644 index 0000000..32f3d71 --- /dev/null +++ b/tests/libs/conflictresolvedialogtest_gui.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2017-2021 Laurent Montel + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "../src/widgets/conflictresolvedialog_p.h" + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData aboutData(QStringLiteral("conflictresolvedialogtest_gui"), QStringLiteral("conflictresolvedialogtest_gui"), QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + QStandardPaths::setTestModeEnabled(true); + Akonadi::ConflictResolveDialog dlg; + dlg.exec(); + + return 0; +} diff --git a/tests/libs/etm_test_app/CMakeLists.txt b/tests/libs/etm_test_app/CMakeLists.txt new file mode 100644 index 0000000..9dbd52c --- /dev/null +++ b/tests/libs/etm_test_app/CMakeLists.txt @@ -0,0 +1,16 @@ +kde_enable_exceptions() + + +set(etmtestapp_SRCS + main.cpp + mainwindow.cpp +) + +add_executable(akonadi_etm_test_app ${etmtestapp_SRCS}) + +target_link_libraries(akonadi_etm_test_app + KF5::AkonadiWidgets + KF5::I18n + akonaditestfake + Qt::Test +) diff --git a/tests/libs/etm_test_app/main.cpp b/tests/libs/etm_test_app/main.cpp new file mode 100644 index 0000000..fd00e3f --- /dev/null +++ b/tests/libs/etm_test_app/main.cpp @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2010 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include +#include + +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + KAboutData aboutData(QStringLiteral("etm_test_app"), + i18n("ETM Test application"), + QStringLiteral("0.99"), + i18n("Test app for EntityTreeModel"), + KAboutLicense::GPL, + QStringLiteral("https://community.kde.org/KDE_PIM/Akonadi/")); + aboutData.addAuthor(i18n("Stephen Kelly"), i18n("Author"), QStringLiteral("steveire@gmail.com")); + KAboutData::setApplicationData(aboutData); + + app.setWindowIcon(QIcon::fromTheme(QStringLiteral("akonadi"))); + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + MainWindow mw; + mw.show(); + + return app.exec(); +} diff --git a/tests/libs/etm_test_app/mainwindow.cpp b/tests/libs/etm_test_app/mainwindow.cpp new file mode 100644 index 0000000..47f5417 --- /dev/null +++ b/tests/libs/etm_test_app/mainwindow.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2010 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "mainwindow.h" + +#include "control.h" + +#include "entitytreeview.h" +#include "fakemonitor.h" +#include "fakeserverdata.h" +#include "fakesession.h" +#include + +using namespace Akonadi; + +MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) + : QMainWindow(parent, flags) +{ + auto monitor = new FakeMonitor(this); + auto session = new FakeSession("FS1", FakeSession::EndJobsImmediately, this); + monitor->setSession(session); + + m_model = new EntityTreeModel(monitor, this); + m_serverData = new FakeServerData(m_model, session, monitor, this); + + QList initialFetchResponse = + FakeJobResponse::interpret(m_serverData, + QStringLiteral("- C (inode/directory) 'Col 1' 4" + "- - C (text/directory, message/rfc822) 'Col 2' 3" + // Items just have the mimetype they contain in the payload. + "- - - I text/directory" + "- - - I text/directory 'Item 1'" + "- - - I message/rfc822" + "- - - I message/rfc822" + "- - C (text/directory) 'Col 3' 3" + "- - - C (text/directory) 'Col 4' 2" + "- - - - C (text/directory) 'Col 5' 1" // <-- First collection to be returned + "- - - - - I text/directory" + "- - - - - I text/directory" + "- - - - I text/directory" + "- - - I text/directory" + "- - - I text/directory" + "- - C (message/rfc822) 'Col 6' 3" + "- - - I message/rfc822" + "- - - I message/rfc822" + "- - C (text/directory, message/rfc822) 'Col 7' 3" + "- - - I text/directory" + "- - - I text/directory" + "- - - I message/rfc822" + "- - - I message/rfc822")); + m_serverData->setCommands(initialFetchResponse); + + auto view = new EntityTreeView(this); + view->setModel(m_model); + + view->expandAll(); + setCentralWidget(view); + + QTimer::singleShot(5000, this, &MainWindow::moveCollection); +} + +void MainWindow::moveCollection() +{ + // Move Col 3 from Col 4 to Col 7 + auto moveCommand = new FakeCollectionMovedCommand(QStringLiteral("Col 4"), QStringLiteral("Col 3"), QStringLiteral("Col 7"), m_serverData); + + m_serverData->setCommands(QList() << moveCommand); + m_serverData->processNotifications(); +} diff --git a/tests/libs/etm_test_app/mainwindow.h b/tests/libs/etm_test_app/mainwindow.h new file mode 100644 index 0000000..04ea290 --- /dev/null +++ b/tests/libs/etm_test_app/mainwindow.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2010 Stephen Kelly + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Akonadi +{ +class EntityTreeModel; +} + +class FakeServerData; + +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + MainWindow(QWidget *parent = nullptr, Qt::WindowFlags flags = {}); + +private Q_SLOTS: + void moveCollection(); + +private: + FakeServerData *m_serverData = nullptr; + Akonadi::EntityTreeModel *m_model = nullptr; +}; + diff --git a/tests/libs/itemdumper.cpp b/tests/libs/itemdumper.cpp new file mode 100644 index 0000000..d5cae5b --- /dev/null +++ b/tests/libs/itemdumper.cpp @@ -0,0 +1,102 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + SPDX-FileCopyrightText: 2007 Robert Zwerus + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemdumper.h" +#include "collectionpathresolver.h" + +#include "item.h" + +#include +#include +#include + +#include + +#include +#include +#include + +#include "itemcreatejob.h" +#include "transactionjobs.h" + +#define GLOBAL_TRANSACTION 1 + +using namespace Akonadi; + +ItemDumper::ItemDumper(const QString &path, const QString &filename, const QString &mimetype, int count) + : mJobCount(0) +{ + auto resolver = new CollectionPathResolver(path, this); + Q_ASSERT(resolver->exec()); + const Collection collection = Collection(resolver->collection()); + + QFile f(filename); + Q_ASSERT(f.open(QIODevice::ReadOnly)); + QByteArray data = f.readAll(); + f.close(); + Item item; + item.setMimeType(mimetype); + item.setPayloadFromData(data); + mTime.start(); +#ifdef GLOBAL_TRANSACTION + auto begin = new TransactionBeginJob(this); + connect(begin, &TransactionBeginJob::result, this, &ItemDumper::done); + ++mJobCount; +#endif + for (int i = 0; i < count; ++i) { + ++mJobCount; + auto job = new ItemCreateJob(item, collection, this); + connect(job, &ItemCreateJob::result, this, &ItemDumper::done); + } +#ifdef GLOBAL_TRANSACTION + auto commit = new TransactionCommitJob(this); + connect(commit, &TransactionCommitJob::result, this, &ItemDumper::done); + ++mJobCount; +#endif +} + +void ItemDumper::done(KJob *job) +{ + --mJobCount; + if (job->error()) { + qWarning() << "Error while creating item: " << job->errorString(); + } + if (mJobCount <= 0) { + qDebug() << "Time:" << mTime.elapsed() << "ms"; + qApp->quit(); + } +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData aboutData(QStringLiteral("test"), i18n("Test Application"), QStringLiteral("1.0")); + + QCommandLineParser parser; + KAboutData::setApplicationData(aboutData); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("path"), i18n("IMAP destination path"), QStringLiteral("argument"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("mimetype"), + i18n("Source mimetype"), + QStringLiteral("argument"), + QStringLiteral("application/octet-stream"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("file"), i18n("Source file"), QStringLiteral("argument"))); + parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("count"), + i18n("Number of times this file is added"), + QStringLiteral("argument"), + QStringLiteral("1"))); + + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + QString path = parser.value(QStringLiteral("path")); + QString mimetype = parser.value(QStringLiteral("mimetype")); + QString file = parser.value(QStringLiteral("file")); + int count = qMax(1, parser.value(QStringLiteral("count")).toInt()); + ItemDumper d(path, file, mimetype, count); + return app.exec(); +} diff --git a/tests/libs/itemdumper.h b/tests/libs/itemdumper.h new file mode 100644 index 0000000..32afe46 --- /dev/null +++ b/tests/libs/itemdumper.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2006 Volker Krause + SPDX-FileCopyrightText: 2007 Robert Zwerus + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "collection.h" + +#include + +class KJob; + +class ItemDumper : public QObject +{ + Q_OBJECT +public: + ItemDumper(const QString &path, const QString &filename, const QString &mimetype, int count); + +private Q_SLOTS: + void done(KJob *job); + +private: + QElapsedTimer mTime; + int mJobCount; +}; + diff --git a/tests/libs/pluginloadertest.cpp b/tests/libs/pluginloadertest.cpp new file mode 100644 index 0000000..7efd20e --- /dev/null +++ b/tests/libs/pluginloadertest.cpp @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2008 Tobias Koenig + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "itemserializerplugin.h" +#include "pluginloader_p.h" + +#include +#include +#include + +using namespace Akonadi; + +int main() +{ + QApplication::setApplicationName(QStringLiteral("pluginloadertest")); + + PluginLoader *loader = PluginLoader::self(); + + const QStringList types = loader->names(); + qDebug("Types:"); + for (int i = 0; i < types.count(); ++i) { + qDebug("%s", qPrintable(types.at(i))); + } + + QObject *object = loader->createForName(QStringLiteral("text/vcard@KContacts::Addressee")); + if (qobject_cast(object) != nullptr) { + qDebug("Loaded plugin for mimetype 'text/vcard@KContacts::Addressee' successfully"); + } else { + qDebug("Unable to load plugin for mimetype 'text/vcard'"); + } + + return 0; +} diff --git a/tests/libs/selftester.cpp b/tests/libs/selftester.cpp new file mode 100644 index 0000000..2085ea6 --- /dev/null +++ b/tests/libs/selftester.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2008 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include <../src/selftest/selftestdialog.h> + +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + KAboutData aboutData(QStringLiteral("akonadi-selftester"), QStringLiteral("akonadi-selftester"), QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + parser.addVersionOption(); + parser.addHelpOption(); + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + SelfTestDialog dlg; + dlg.exec(); + + return 0; +} diff --git a/tests/libs/subscriber.cpp b/tests/libs/subscriber.cpp new file mode 100644 index 0000000..4c04994 --- /dev/null +++ b/tests/libs/subscriber.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2007 Volker Krause + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); + KAboutData aboutData(QStringLiteral("akonadi-subscriber"), QStringLiteral("Test akonadi subscriber"), QStringLiteral("0.10")); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser parser; + aboutData.setupCommandLine(&parser); + parser.process(app); + aboutData.processCommandLine(&parser); + + auto dlg = new Akonadi::SubscriptionDialog(); + QObject::connect(dlg, &Akonadi::SubscriptionDialog::destroyed, &app, &QApplication::quit); + dlg->show(); + return app.exec(); +} diff --git a/tools/clang-tidy-to-junit.py b/tools/clang-tidy-to-junit.py new file mode 100755 index 0000000..6823d44 --- /dev/null +++ b/tools/clang-tidy-to-junit.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# SPDX-FileCopyrightText: 2018 PSPDFKit +# +# SPDX-License-Identifier: MIT +# +# Originally taken from https://github.com/PSPDFKit-labs/clang-tidy-to-junit + +import sys +import collections +import re +import logging +import itertools +from xml.sax.saxutils import escape + +# Create a `ErrorDescription` tuple with all the information we want to keep. +ErrorDescription = collections.namedtuple( + 'ErrorDescription', 'file line column error error_identifier description') + + +class ClangTidyConverter: + # All the errors encountered. + errors = [] + + # Parses the error. + # Group 1: file path + # Group 2: line + # Group 3: column + # Group 4: error message + # Group 5: error identifier + error_regex = re.compile( + r"^([\w\/\.\-\ ]+):(\d+):(\d+): (.+) (\[[\w\-,\.]+\])$") + + # This identifies the main error line (it has a [the-warning-type] at the end) + # We only create a new error when we encounter one of those. + main_error_identifier = re.compile(r'\[[\w\-,\.]+\]$') + + def __init__(self, basename): + self.basename = basename + + def print_junit_file(self, output_file): + # Write the header. + output_file.write(""" +""".format(error_count=len(self.errors))) + + sorted_errors = sorted(self.errors, key=lambda x: x.file) + + # Iterate through the errors, grouped by file. + for file, errorIterator in itertools.groupby(sorted_errors, key=lambda x: x.file): + errors = list(errorIterator) + error_count = len(errors) + + # Each file gets a test-suite + output_file.write("""\n \n""" + .format(error_count=error_count, file=file)) + for error in errors: + # Write each error as a test case. + output_file.write(""" + + +{htmldata} + + """.format(id="[{}/{}] {}".format(error.line, error.column, error.error_identifier), + message=escape(error.error, entities={"\"": """}), + htmldata=escape(error.description))) + output_file.write("\n \n") + output_file.write("\n") + + def process_error(self, error_array): + if len(error_array) == 0: + return + + result = self.error_regex.match(error_array[0]) + if result is None: + logging.warning( + 'Could not match error_array to regex: %s', error_array) + return + + # We remove the `basename` from the `file_path` to make prettier filenames in the JUnit file. + file_path = result.group(1).replace(self.basename, "") + error = ErrorDescription(file_path, int(result.group(2)), int( + result.group(3)), result.group(4), result.group(5), "\n".join(error_array[1:])) + self.errors.append(error) + + def convert(self, input_file, output_file): + # Collect all lines related to one error. + current_error = [] + for line in input_file: + # If the line starts with a `/`, it is a line about a file. + if line[0] == '/': + # Look if it is the start of a error + if self.main_error_identifier.search(line, re.M): + # If so, process any `current_error` we might have + self.process_error(current_error) + # Initialize `current_error` with the first line of the error. + current_error = [line] + else: + # Otherwise, append the line to the error. + current_error.append(line) + elif len(current_error) > 0: + # If the line didn't start with a `/` and we have a `current_error`, we simply append + # the line as additional information. + current_error.append(line) + else: + pass + + # If we still have any current_error after we read all the lines, + # process it. + if len(current_error) > 0: + self.process_error(current_error) + + # Print the junit file. + self.print_junit_file(output_file) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + logging.error("Usage: %s base-filename-path", sys.argv[0]) + logging.error( + " base-filename-path: Removed from the filenames to make nicer paths.") + sys.exit(1) + converter = ClangTidyConverter(sys.argv[1]) + converter.convert(sys.stdin, sys.stdout) diff --git a/tools/run-clang-tidy.sh b/tools/run-clang-tidy.sh new file mode 100755 index 0000000..4c8c42d --- /dev/null +++ b/tools/run-clang-tidy.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# SPDX-FileCopyrightText: 2020 Daniel Vrátil +# +# SPDX-License-Identifier: LGPL-2.0-or-later + +if [ $# -lt 2 ]; then + >&2 echo "Usage: $0 SOURCE_DIR BUILD_DIR" + exit 1 +fi + +set -xe + +source_dir=$1; shift +build_dir=$1; shift 1 + +function sanitize_compile_commands +{ + local cc_file=${build_dir}/compile_commands.json + local filter_file="${source_dir}/.clang-tidy-ignore" + + if [ ! -f "${cc_file}" ]; then + >&2 echo "Couldn't find compile_commands.json" + exit 1 + fi + + if [ ! -f "${filter_file}" ]; then + return 0 + fi + + filter_files=$(cat ${filter_file} | grep -vE "^#\.*|^$" | tr '\n' '|' | head -c -1) + + local cc_bak_file=${cc_file}.bak + mv ${cc_file} ${cc_bak_file} + + cat ${cc_bak_file} \ + | jq -r "map(select(.file|test(\"${filter_files}\")|not))" \ + > ${cc_file} + + task_count=$(cat ${cc_file} | jq "length") +} + +sanitize_compile_commands + +run-clang-tidy -p ${build_dir} -j$(nproc) -q $@ | tee ${build_dir}/clang-tidy.log +cat ${build_dir}/clang-tidy.log | ${source_dir}/tools/clang-tidy-to-junit.py ${source_dir} > ${build_dir}/clang-tidy-report.xml + -- 2.30.2